//go:build linux // Package securefile provides Linux-only, no-follow filesystem primitives for // protected credential material. package securefile import ( "errors" "fmt" "io" "os" "path/filepath" "strings" "syscall" ) var ( // ErrUnsafe identifies a path, mode, or file type that cannot be trusted. ErrUnsafe = errors.New("unsafe filesystem object") // ErrTooLarge identifies a file that exceeds its caller-provided bound. ErrTooLarge = errors.New("file exceeds size bound") // ErrChanged identifies a file whose size changed while it was read. ErrChanged = errors.New("file changed during read") ) // Dir is an open, protected directory descriptor. Operations are rooted at the // descriptor rather than reopening attacker-controlled path prefixes. type Dir struct { fd int path string } // Lock serializes cooperating writers using an advisory lock on a protected // regular file. type Lock struct { fd int } // OpenDir opens an absolute directory without following any path component and // rejects unsafe modes on the protected final directory. func OpenDir(path string) (*Dir, error) { clean, err := absolutePath(path) if err != nil { return nil, err } fd, err := openDirectoryPath(clean) if err != nil { return nil, err } if err := validateDirectoryFD(fd); err != nil { _ = syscall.Close(fd) return nil, err } if err := compareWithLstat(fd, clean, true); err != nil { _ = syscall.Close(fd) return nil, err } return &Dir{fd: fd, path: clean}, nil } // Close closes the directory descriptor. func (d *Dir) Close() error { if d == nil || d.fd < 0 { return nil } err := syscall.Close(d.fd) d.fd = -1 return err } // OpenDir opens one direct child directory without following it. func (d *Dir) OpenDir(name string) (*Dir, error) { if err := validName(name); err != nil { return nil, err } if err := d.check(); err != nil { return nil, err } fd, err := syscall.Openat(d.fd, name, directoryOpenFlags, 0) if err != nil { return nil, err } if err := validateDirectoryFD(fd); err != nil { _ = syscall.Close(fd) return nil, err } path := filepath.Join(d.path, name) if err := compareWithLstat(fd, path, true); err != nil { _ = syscall.Close(fd) return nil, err } return &Dir{fd: fd, path: path}, nil } // OpenOrCreateDir opens a protected child directory or creates it with a // restrictive mode. A collision is reopened and fully revalidated. func (d *Dir) OpenOrCreateDir(name string, mode os.FileMode) (*Dir, error) { child, err := d.OpenDir(name) if err == nil { return child, nil } if !errors.Is(err, syscall.ENOENT) { return nil, err } if err := validMode(mode); err != nil { return nil, err } if err := d.check(); err != nil { return nil, err } if err := syscall.Mkdirat(d.fd, name, uint32(mode.Perm())); err != nil && !errors.Is(err, syscall.EEXIST) { return nil, err } child, err = d.OpenDir(name) if err != nil { return nil, err } if err := d.Sync(); err != nil { _ = child.Close() return nil, err } return child, nil } // ReadFile reads one protected regular child file, bounded by max bytes. It // verifies file type and mode before and after the read and detects size races. func (d *Dir) ReadFile(name string, max int) ([]byte, error) { return d.readFile(name, max, nil) } // ReadSecret reads an absolute secret input whose final parent is protected and // whose file mode is exactly 0600. func ReadSecret(path string, max int) ([]byte, error) { clean, err := absolutePath(path) if err != nil { return nil, err } parent := filepath.Dir(clean) name := filepath.Base(clean) if err := validName(name); err != nil { return nil, err } dir, err := OpenDir(parent) if err != nil { return nil, err } defer dir.Close() mode := uint32(0o600) return dir.readFile(name, max, &mode) } // CreateSecret creates a new absolute output file with O_CREAT|O_EXCL and an // exact mode of 0600. The caller owns and must close the returned file. func CreateSecret(path string) (*os.File, error) { clean, err := absolutePath(path) if err != nil { return nil, err } parent := filepath.Dir(clean) name := filepath.Base(clean) if err := validName(name); err != nil { return nil, err } dir, err := OpenDir(parent) if err != nil { return nil, err } defer dir.Close() return dir.CreateExclusive(name, 0o600) } // CreateExclusive creates one direct child with O_CREAT|O_EXCL|O_NOFOLLOW and // the exact requested restrictive mode. The caller owns and must close it. func (d *Dir) CreateExclusive(name string, mode os.FileMode) (*os.File, error) { if err := validName(name); err != nil { return nil, err } if err := validMode(mode); err != nil { return nil, err } if err := d.check(); err != nil { return nil, err } fd, err := syscall.Openat(d.fd, name, syscall.O_WRONLY|syscall.O_CREAT|syscall.O_EXCL|syscall.O_NOFOLLOW|syscall.O_CLOEXEC, uint32(mode.Perm())) if err != nil { return nil, err } if err := syscall.Fchmod(fd, uint32(mode.Perm())); err != nil { _ = syscall.Close(fd) return nil, err } if err := validateRegularFD(fd, uint32(mode.Perm())); err != nil { _ = syscall.Close(fd) return nil, err } return os.NewFile(uintptr(fd), filepath.Join(d.path, name)), nil } // Exists reports whether a direct protected regular child exists. An unsafe // collision is an error rather than an absent file. func (d *Dir) Exists(name string) (bool, error) { if err := validName(name); err != nil { return false, err } if err := d.check(); err != nil { return false, err } fd, err := syscall.Openat(d.fd, name, syscall.O_RDONLY|syscall.O_NOFOLLOW|syscall.O_CLOEXEC, 0) if errors.Is(err, syscall.ENOENT) { return false, nil } if err != nil { return false, err } defer syscall.Close(fd) if err := validateRegularFD(fd, 0); err != nil { return false, err } if err := compareWithLstat(fd, filepath.Join(d.path, name), false); err != nil { return false, err } return true, nil } // Rename atomically renames a direct child into another protected directory. // Callers that need no-replace semantics must serialize writers and verify the // destination is absent before calling Rename. func (d *Dir) Rename(oldName string, destination *Dir, newName string) error { if err := validName(oldName); err != nil { return err } if err := validName(newName); err != nil { return err } if destination == nil { return fmt.Errorf("%w: nil destination", ErrUnsafe) } if err := d.check(); err != nil { return err } if err := destination.check(); err != nil { return err } return syscall.Renameat(d.fd, oldName, destination.fd, newName) } // Remove removes one direct child file without following it. func (d *Dir) Remove(name string) error { if err := validName(name); err != nil { return err } if err := d.check(); err != nil { return err } return syscall.Unlinkat(d.fd, name) } // Sync makes prior directory entry changes durable. func (d *Dir) Sync() error { if err := d.check(); err != nil { return err } return syscall.Fsync(d.fd) } // Names returns direct entry names from the protected directory descriptor. func (d *Dir) Names() ([]string, error) { if err := d.check(); err != nil { return nil, err } fd, err := syscall.Openat(d.fd, ".", directoryOpenFlags, 0) if err != nil { return nil, err } file := os.NewFile(uintptr(fd), d.path) entries, err := file.ReadDir(-1) closeErr := file.Close() if err != nil { return nil, err } if closeErr != nil { return nil, closeErr } names := make([]string, 0, len(entries)) for _, entry := range entries { names = append(names, entry.Name()) } return names, nil } // Lock opens or creates a protected 0640 lock file and acquires an exclusive // advisory lock. Close releases the lock and descriptor. func (d *Dir) Lock(name string) (*Lock, error) { return d.lock(name, syscall.LOCK_EX) } // LockShared opens an existing protected 0640 lock read-only and acquires a // shared advisory lock. It never creates or changes registry state. func (d *Dir) LockShared(name string) (*Lock, error) { if err := validName(name); err != nil { return nil, err } if err := d.check(); err != nil { return nil, err } fd, err := syscall.Openat(d.fd, name, syscall.O_RDONLY|syscall.O_NOFOLLOW|syscall.O_CLOEXEC, 0) if err != nil { return nil, err } if err := validateRegularFD(fd, 0o640); err != nil { _ = syscall.Close(fd) return nil, err } if err := compareWithLstat(fd, filepath.Join(d.path, name), false); err != nil { _ = syscall.Close(fd) return nil, err } if err := syscall.Flock(fd, syscall.LOCK_SH); err != nil { _ = syscall.Close(fd) return nil, err } return &Lock{fd: fd}, nil } func (d *Dir) lock(name string, lockMode int) (*Lock, error) { if err := validName(name); err != nil { return nil, err } if err := d.check(); err != nil { return nil, err } for { fd, err := syscall.Openat(d.fd, name, syscall.O_RDWR|syscall.O_NOFOLLOW|syscall.O_CLOEXEC, 0) if errors.Is(err, syscall.ENOENT) { fd, err = syscall.Openat(d.fd, name, syscall.O_RDWR|syscall.O_CREAT|syscall.O_EXCL|syscall.O_NOFOLLOW|syscall.O_CLOEXEC, 0o640) if errors.Is(err, syscall.EEXIST) { continue } if err != nil { return nil, err } if err := syscall.Fchmod(fd, 0o640); err != nil { _ = syscall.Close(fd) return nil, err } if err := d.Sync(); err != nil { _ = syscall.Close(fd) return nil, err } } if err != nil { return nil, err } if err := validateRegularFD(fd, 0o640); err != nil { _ = syscall.Close(fd) return nil, err } if err := compareWithLstat(fd, filepath.Join(d.path, name), false); err != nil { _ = syscall.Close(fd) return nil, err } if err := syscall.Flock(fd, lockMode); err != nil { _ = syscall.Close(fd) return nil, err } return &Lock{fd: fd}, nil } } // Close releases an advisory lock and closes its descriptor. func (l *Lock) Close() error { if l == nil || l.fd < 0 { return nil } unlockErr := syscall.Flock(l.fd, syscall.LOCK_UN) closeErr := syscall.Close(l.fd) l.fd = -1 if unlockErr != nil { return unlockErr } return closeErr } func (d *Dir) readFile(name string, max int, exactMode *uint32) ([]byte, error) { if err := validName(name); err != nil { return nil, err } if max < 0 { return nil, fmt.Errorf("%w: negative size bound", ErrUnsafe) } if err := d.check(); err != nil { return nil, err } fd, err := syscall.Openat(d.fd, name, syscall.O_RDONLY|syscall.O_NOFOLLOW|syscall.O_CLOEXEC, 0) if err != nil { return nil, err } defer syscall.Close(fd) var before syscall.Stat_t if err := syscall.Fstat(fd, &before); err != nil { return nil, err } if err := validateRegularStat(&before, exactMode); err != nil { return nil, err } if err := compareWithLstat(fd, filepath.Join(d.path, name), false); err != nil { return nil, err } if before.Size < 0 || before.Size > int64(max) { return nil, fmt.Errorf("%w: %d bytes", ErrTooLarge, before.Size) } size := int(before.Size) data := make([]byte, size+1) n := 0 for n < len(data) { read, readErr := syscall.Read(fd, data[n:]) if read > 0 { n += read } if errors.Is(readErr, syscall.EINTR) { continue } if readErr != nil { return nil, readErr } if read == 0 { break } } var after syscall.Stat_t if err := syscall.Fstat(fd, &after); err != nil { return nil, err } if err := validateRegularStat(&after, exactMode); err != nil { return nil, err } if before.Dev != after.Dev || before.Ino != after.Ino || before.Size != after.Size { return nil, ErrChanged } if n != size { return nil, ErrChanged } return data[:n], nil } func (d *Dir) check() error { if d == nil || d.fd < 0 { return fmt.Errorf("%w: closed directory", ErrUnsafe) } return validateDirectoryFD(d.fd) } func absolutePath(path string) (string, error) { if !filepath.IsAbs(path) { return "", fmt.Errorf("%w: path must be absolute", ErrUnsafe) } return filepath.Clean(path), nil } func openDirectoryPath(path string) (int, error) { fd, err := syscall.Open("/", directoryOpenFlags, 0) if err != nil { return -1, err } if path == "/" { return fd, nil } for _, part := range strings.Split(strings.TrimPrefix(path, "/"), "/") { next, err := syscall.Openat(fd, part, directoryOpenFlags, 0) _ = syscall.Close(fd) if err != nil { return -1, err } var stat syscall.Stat_t if err := syscall.Fstat(next, &stat); err != nil { _ = syscall.Close(next) return -1, err } if stat.Mode&syscall.S_IFMT != syscall.S_IFDIR { _ = syscall.Close(next) return -1, fmt.Errorf("%w: non-directory path component", ErrUnsafe) } fd = next } return fd, nil } func compareWithLstat(fd int, path string, directory bool) error { var opened syscall.Stat_t if err := syscall.Fstat(fd, &opened); err != nil { return err } var linked syscall.Stat_t if err := syscall.Lstat(path, &linked); err != nil { return err } if directory { if linked.Mode&syscall.S_IFMT != syscall.S_IFDIR { return fmt.Errorf("%w: path is not a directory", ErrUnsafe) } } else if linked.Mode&syscall.S_IFMT != syscall.S_IFREG { return fmt.Errorf("%w: path is not a regular file", ErrUnsafe) } if opened.Dev != linked.Dev || opened.Ino != linked.Ino { return fmt.Errorf("%w: path changed while opening", ErrUnsafe) } return nil } func validateDirectoryFD(fd int) error { var stat syscall.Stat_t if err := syscall.Fstat(fd, &stat); err != nil { return err } if stat.Mode&syscall.S_IFMT != syscall.S_IFDIR { return fmt.Errorf("%w: not a directory", ErrUnsafe) } if stat.Mode&0o5022 != 0 { return fmt.Errorf("%w: unsafe directory mode %04o", ErrUnsafe, stat.Mode&0o7777) } return nil } func validateRegularFD(fd int, exactMode uint32) error { var stat syscall.Stat_t if err := syscall.Fstat(fd, &stat); err != nil { return err } var required *uint32 if exactMode != 0 { required = &exactMode } return validateRegularStat(&stat, required) } func validateRegularStat(stat *syscall.Stat_t, exactMode *uint32) error { if stat.Mode&syscall.S_IFMT != syscall.S_IFREG { return fmt.Errorf("%w: not a regular file", ErrUnsafe) } if stat.Mode&0o7022 != 0 { return fmt.Errorf("%w: unsafe file mode %04o", ErrUnsafe, stat.Mode&0o7777) } if exactMode != nil && stat.Mode&0o777 != *exactMode { return fmt.Errorf("%w: file mode %04o is not %04o", ErrUnsafe, stat.Mode&0o777, *exactMode) } return nil } func validMode(mode os.FileMode) error { if mode&^os.FileMode(0o777) != 0 || mode.Perm()&0o022 != 0 { return fmt.Errorf("%w: unsafe creation mode %04o", ErrUnsafe, mode) } return nil } func validName(name string) error { if name == "" || name == "." || name == ".." || strings.Contains(name, "/") || strings.ContainsRune(name, 0) { return fmt.Errorf("%w: invalid path component", ErrUnsafe) } return nil } const directoryOpenFlags = syscall.O_RDONLY | syscall.O_DIRECTORY | syscall.O_NOFOLLOW | syscall.O_CLOEXEC var _ io.Closer = (*Dir)(nil)