//go:build !windows package safeio import ( "errors" "os" "golang.org/x/sys/unix" ) func createPrivateDirectory(path string) error { return createPrivateDirectoryAfterParentOpen(path, nil) } func createPrivateDirectoryAfterParentOpen(path string, afterOpen func()) error { parents, err := openCanonicalUnixParent(path) if err != nil { return ErrUnsafeFile } defer parents.Close() if afterOpen != nil { afterOpen() } return createPrivateDirectoryAt(parents) } // createPrivateDirectoryAt performs every mutating operation relative to the already-opened // parent. An attacker can rename or replace any lexical ancestor after the open without // redirecting mkdir or chmod into a different directory tree. func createPrivateDirectoryAt(parents *unixParentHandles) error { if parents == nil || parents.parent < 0 || parents.target == "" { return ErrUnsafeFile } if err := unix.Mkdirat(parents.parent, parents.target, 0o700); err != nil { if errors.Is(err, unix.EEXIST) { return os.ErrExist } return ErrUnsafeFile } descriptor, err := unix.Openat(parents.parent, parents.target, unix.O_RDONLY|unix.O_CLOEXEC|unix.O_DIRECTORY|unix.O_NOFOLLOW, 0) if err != nil { return ErrUnsafeFile } defer unix.Close(descriptor) if unix.Fchmod(descriptor, 0o700) != nil { return ErrUnsafeFile } var stat unix.Stat_t if unix.Fstat(descriptor, &stat) != nil || !privateUnixDirectoryStat(&stat) { return ErrUnsafeFile } return nil } // ProtectPrivateDirectory sets the private directory mode used for local authentication state. func ProtectPrivateDirectory(path string) error { parents, descriptor, err := openCanonicalUnixPrivateDirectory(path) if err != nil { return ErrUnsafeFile } defer parents.Close() defer unix.Close(descriptor) if unix.Fchmod(descriptor, 0o700) != nil { return ErrUnsafeFile } var stat unix.Stat_t if unix.Fstat(descriptor, &stat) != nil || !privateUnixDirectoryStat(&stat) { return ErrUnsafeFile } return nil } // ValidatePrivateDirectory requires a canonical, non-symlinked directory with no group or world access. func ValidatePrivateDirectory(path string) error { parents, descriptor, err := openCanonicalUnixPrivateDirectory(path) if err != nil { return ErrUnsafeFile } defer parents.Close() defer unix.Close(descriptor) var stat unix.Stat_t if unix.Fstat(descriptor, &stat) != nil || !privateUnixDirectoryStat(&stat) { return ErrUnsafeFile } return nil } // ProtectPrivateRegular sets the private file mode used for local authentication files. func ProtectPrivateRegular(path string) error { parents, descriptor, err := openCanonicalUnixPrivateRegular(path) if err != nil { return ErrUnsafeFile } defer parents.Close() defer unix.Close(descriptor) if unix.Fchmod(descriptor, 0o600) != nil { return ErrUnsafeFile } var stat unix.Stat_t if unix.Fstat(descriptor, &stat) != nil || !privateUnixRegularStat(&stat) { return ErrUnsafeFile } return nil } func createCanonicalNewPrivateFile(path string, mode os.FileMode) (*os.File, error) { return createCanonicalNewPrivateFileWithFlags(path, mode, os.O_WRONLY, false) } func createCanonicalNewPrivateParentFile(path string, mode os.FileMode) (*os.File, error) { return createCanonicalNewPrivateFileWithFlags(path, mode, os.O_WRONLY, true) } func createCanonicalNewPrivateParentReadWriteFile(path string, mode os.FileMode) (*os.File, error) { return createCanonicalNewPrivateFileWithFlags(path, mode, os.O_RDWR, true) } // createCanonicalNewPrivateFileWithFlags resolves and validates every parent once, then creates // the final name through that retained parent descriptor. No later lexical ancestor replacement // can redirect the create, mode repair, metadata check, or failure cleanup. func createCanonicalNewPrivateFileWithFlags(path string, mode os.FileMode, flags int, requirePrivateParent bool) (*os.File, error) { parents, err := openCanonicalUnixParent(path) if err != nil { return nil, ErrUnsafeFile } defer parents.Close() if requirePrivateParent { var parentStat unix.Stat_t if unix.Fstat(parents.parent, &parentStat) != nil || !privateUnixDirectoryStat(&parentStat) { return nil, ErrUnsafeFile } } NotifyPrivateDirectoryTestHookForTest("after-canonical-private-file-parent-open") // The final 0600 is applied with fchmod rather than trusting process umask or a path lookup. // mode remains accepted for the existing helper contract; private files are always exactly 0600. _ = mode descriptor, err := unix.Openat(parents.parent, parents.target, flags|unix.O_CREAT|unix.O_EXCL|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0o600) if err != nil { return nil, err } failed := true defer func() { if failed { _ = unix.Close(descriptor) _ = unix.Unlinkat(parents.parent, parents.target, 0) } }() if unix.Fchmod(descriptor, 0o600) != nil { return nil, ErrUnsafeFile } var stat unix.Stat_t if unix.Fstat(descriptor, &stat) != nil || !privateUnixRegularStat(&stat) { return nil, ErrUnsafeFile } file := os.NewFile(uintptr(descriptor), "tht-safeio-private") if file == nil { return nil, ErrUnsafeFile } failed = false return file, nil } // ValidatePrivateRegular requires a canonical, single-link private regular file. func ValidatePrivateRegular(path string) error { parents, descriptor, err := openCanonicalUnixPrivateRegular(path) if err != nil { return ErrUnsafeFile } defer parents.Close() defer unix.Close(descriptor) var stat unix.Stat_t if unix.Fstat(descriptor, &stat) != nil || !privateUnixRegularStat(&stat) { return ErrUnsafeFile } return nil } func openCanonicalUnixPrivateDirectory(path string) (*unixParentHandles, int, error) { parents, err := openCanonicalUnixParent(path) if err != nil { return nil, -1, ErrUnsafeFile } descriptor, err := unix.Openat(parents.parent, parents.target, unix.O_RDONLY|unix.O_CLOEXEC|unix.O_DIRECTORY|unix.O_NOFOLLOW, 0) if err != nil { parents.Close() return nil, -1, ErrUnsafeFile } return parents, descriptor, nil } func openCanonicalUnixPrivateRegular(path string) (*unixParentHandles, int, error) { parents, err := openCanonicalUnixParent(path) if err != nil { return nil, -1, ErrUnsafeFile } descriptor, err := unix.Openat(parents.parent, parents.target, unix.O_RDONLY|unix.O_CLOEXEC|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0) if err != nil { parents.Close() return nil, -1, ErrUnsafeFile } return parents, descriptor, nil } func privateUnixRegularStat(stat *unix.Stat_t) bool { return stat != nil && stat.Mode&unix.S_IFMT == unix.S_IFREG && stat.Nlink == 1 && stat.Uid == uint32(os.Geteuid()) && stat.Mode&0o7777 == 0o600 }