//go:build linux package privatefile import ( "io" "os" "path/filepath" "strings" "golang.org/x/sys/unix" ) // Walk using retained directory descriptors so a renamed ancestor cannot turn // a checked path into a symlink traversal. func parent(path string) (*os.File, string, error) { if !filepath.IsAbs(path) || filepath.Clean(path) != path || path == "/" { return nil, "", ErrUnavailable } parts := strings.Split(strings.TrimPrefix(path, "/"), "/") fd, err := unix.Open("/", unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) if err != nil { return nil, "", ErrUnavailable } for _, part := range parts[:len(parts)-1] { 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, "", ErrUnavailable } fd = next } return os.NewFile(uintptr(fd), "private directory"), parts[len(parts)-1], nil } func valid(f *os.File, max int) bool { var st unix.Stat_t return unix.Fstat(int(f.Fd()), &st) == nil && st.Mode&unix.S_IFMT == unix.S_IFREG && st.Mode&07777 == 0600 && st.Uid == uint32(os.Geteuid()) && st.Nlink == 1 && st.Size >= 0 && st.Size <= int64(max) } func read(path string, max int) ([]byte, error) { if max <= 0 || max == int(^uint(0)>>1) { return nil, ErrUnavailable } p, name, e := parent(path) if e != nil { return nil, ErrUnavailable } defer p.Close() fd, e := unix.Openat(int(p.Fd()), name, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK|unix.O_CLOEXEC, 0) if e != nil { return nil, ErrUnavailable } f := os.NewFile(uintptr(fd), "private input") if !valid(f, max) { f.Close() return nil, ErrUnavailable } b, e := io.ReadAll(io.LimitReader(f, int64(max)+1)) ok := valid(f, max) ce := f.Close() if e != nil || ce != nil || !ok || len(b) > max { clear(b) return nil, ErrUnavailable } return b, nil } type durableFile interface { Write([]byte) (int, error) Sync() error Close() error } func finish(f durableFile, p interface{ Sync() error }, b []byte) error { n, e := f.Write(b) if e != nil || n != len(b) { f.Close() return ErrUnavailable } if f.Sync() != nil { f.Close() return ErrUnavailable } if f.Close() != nil { return ErrUnavailable } if p.Sync() != nil { return ErrUnavailable } return nil } func create(path string, b []byte, max int) error { if max <= 0 || len(b) > max { return ErrUnavailable } p, name, e := parent(path) if e != nil { return ErrUnavailable } defer p.Close() var st unix.Stat_t if unix.Fstat(int(p.Fd()), &st) != nil || st.Uid != uint32(os.Geteuid()) || st.Mode&0077 != 0 { return ErrUnavailable } fd, e := unix.Openat(int(p.Fd()), name, unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0600) if e != nil { return ErrUnavailable } f := os.NewFile(uintptr(fd), "private output") if !valid(f, max) { f.Close() return ErrUnavailable } return finish(f, p, b) }