//go:build linux package provisioner import ( "errors" "io" "os" "path/filepath" "strings" "golang.org/x/sys/unix" ) var ErrRefused = errors.New("provisioner: refused") var ErrUnavailable = errors.New("provisioner: unavailable") // privateDirectory retains an opened directory so later operations cannot follow // a replacement path or a symlink supplied in a file name. type privateDirectory struct{ file *os.File } func openPrivateDirectory(path string) (*privateDirectory, error) { if !filepath.IsAbs(path) || filepath.Clean(path) != path || path == "/" { return nil, ErrRefused } fd, err := unix.Open("/", unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) if err != nil { return nil, ErrUnavailable } for _, part := range strings.Split(strings.TrimPrefix(path, "/"), "/") { next, e := unix.Openat(fd, part, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) unix.Close(fd) if e != nil { return nil, ErrRefused } fd = next } var stat unix.Stat_t var fs unix.Statfs_t if unix.Fstat(fd, &stat) != nil || unix.Fstatfs(fd, &fs) != nil { unix.Close(fd) return nil, ErrUnavailable } if stat.Uid != uint32(os.Getuid()) || stat.Mode&0777 != 0700 || (fs.Type != unix.EXT4_SUPER_MAGIC && fs.Type != unix.XFS_SUPER_MAGIC) { unix.Close(fd) return nil, ErrRefused } return &privateDirectory{os.NewFile(uintptr(fd), path)}, nil } func (d *privateDirectory) close() error { return d.file.Close() } func leaf(name string) bool { return name != "" && name != "." && name != ".." && len(name) <= 128 && !strings.ContainsAny(name, "/\\\x00") } func (d *privateDirectory) create(name string, body []byte) error { if !leaf(name) || len(body) > 128<<10 { return ErrRefused } fd, e := unix.Openat(int(d.file.Fd()), name, unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0600) if e != nil { return ErrRefused } f := os.NewFile(uintptr(fd), name) // A failed exclusive write remains present: recovery must validate or refuse // the partial record instead of replacing evidence of an interrupted write. _, e = f.Write(body) if e == nil { e = f.Sync() } closeErr := f.Close() if e != nil || closeErr != nil || d.file.Sync() != nil { return ErrUnavailable } return nil } func (d *privateDirectory) read(name string, limit int) ([]byte, error) { if !leaf(name) || limit <= 0 || limit > 128<<10 { return nil, ErrRefused } fd, e := unix.Openat(int(d.file.Fd()), name, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK|unix.O_CLOEXEC, 0) if e != nil { return nil, ErrRefused } f := os.NewFile(uintptr(fd), name) defer f.Close() var s unix.Stat_t if unix.Fstat(fd, &s) != nil { return nil, ErrUnavailable } if s.Mode&unix.S_IFMT != unix.S_IFREG || s.Mode&0777 != 0600 || s.Uid != uint32(os.Getuid()) || s.Nlink != 1 || s.Size > int64(limit) { return nil, ErrRefused } body, e := io.ReadAll(io.LimitReader(f, int64(limit)+1)) if e != nil { return nil, ErrUnavailable } if len(body) > limit { return nil, ErrRefused } return body, nil } func (d *privateDirectory) lock() (*os.File, error) { fd, e := unix.Openat(int(d.file.Fd()), "lock", unix.O_RDWR|unix.O_CREAT|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0600) if e != nil { return nil, ErrRefused } f := os.NewFile(uintptr(fd), "lock") var s unix.Stat_t if unix.Fstat(fd, &s) != nil || s.Mode&unix.S_IFMT != unix.S_IFREG || s.Mode&0777 != 0600 || s.Uid != uint32(os.Getuid()) || s.Nlink != 1 { f.Close() return nil, ErrRefused } if unix.Flock(fd, unix.LOCK_EX|unix.LOCK_NB) != nil { f.Close() return nil, ErrRefused } if f.Sync() != nil || d.file.Sync() != nil { f.Close() return nil, ErrUnavailable } return f, nil } func checkRegistryFile(d *privateDirectory) error { fd, e := unix.Openat(int(d.file.Fd()), "registry.db", unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK|unix.O_CLOEXEC, 0) if e != nil { return ErrRefused } defer unix.Close(fd) var s unix.Stat_t if unix.Fstat(fd, &s) != nil { return ErrUnavailable } if s.Mode&unix.S_IFMT != unix.S_IFREG || s.Mode&0777 != 0600 || s.Uid != uint32(os.Getuid()) || s.Nlink != 1 || s.Size > int64(maxRecords)*(128<<10) { return ErrRefused } return nil }