//go:build unix package workspace import ( "errors" "io/fs" "os" "path" "strings" "syscall" "golang.org/x/sys/unix" ) func platformFileIdentity(info fs.FileInfo) (uint64, uint64, bool) { stat, ok := info.Sys().(*syscall.Stat_t) if !ok { return 0, 0, false } return uint64(stat.Dev), uint64(stat.Ino), true } // writeParent pins one validated directory descriptor. All write effects and // the final rename are relative to this descriptor, never to a re-resolved path. type writeParent struct { entry *catalogEntry relative string fd int device uint64 inode uint64 } func openOrCreateParentNoFollow(entry *catalogEntry, name string) (*writeParent, string, error) { if entry == nil || entry.directory == nil { return nil, "", errUnsafePath } parent, err := openParentNoFollow(entry, path.Dir(name), true) if err != nil { return nil, "", err } return parent, path.Base(name), nil } func openParentNoFollow(entry *catalogEntry, relative string, create bool) (*writeParent, error) { fd, err := unix.Dup(int(entry.directory.Fd())) if err != nil { return nil, errUnsafePath } unix.CloseOnExec(fd) closeFD := true defer func() { if closeFD { _ = unix.Close(fd) } }() device, inode, err := directoryIdentity(fd, entry.device) if err != nil || device != entry.device || inode != entry.inode { return nil, errUnsafePath } if relative != "." { for _, component := range strings.Split(relative, "/") { next, openErr := unix.Openat(fd, component, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) if openErr != nil && create && errors.Is(openErr, unix.ENOENT) { if err := unix.Mkdirat(fd, component, 0o700); err != nil && !errors.Is(err, unix.EEXIST) { return nil, errUnsafePath } next, openErr = unix.Openat(fd, component, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) } if openErr != nil { return nil, errUnsafePath } nextDevice, nextInode, identityErr := directoryIdentity(next, entry.device) if identityErr != nil { _ = unix.Close(next) return nil, errUnsafePath } _ = unix.Close(fd) fd, device, inode = next, nextDevice, nextInode } } closeFD = false return &writeParent{entry: entry, relative: relative, fd: fd, device: device, inode: inode}, nil } func directoryIdentity(fd int, expectedDevice uint64) (uint64, uint64, error) { var stat unix.Stat_t if err := unix.Fstat(fd, &stat); err != nil || stat.Mode&unix.S_IFMT != unix.S_IFDIR { return 0, 0, errUnsafePath } device := uint64(stat.Dev) if device != expectedDevice { return 0, 0, errUnsafePath } return device, uint64(stat.Ino), nil } func (p *writeParent) close() error { if p == nil || p.fd < 0 { return nil } err := unix.Close(p.fd) p.fd = -1 return err } func (p *writeParent) targetIdentity(base string) (targetIdentity, error) { var stat unix.Stat_t err := unix.Fstatat(p.fd, base, &stat, unix.AT_SYMLINK_NOFOLLOW) if errors.Is(err, unix.ENOENT) { return targetIdentity{}, nil } if err != nil { return targetIdentity{}, errUnsafePath } return targetIdentity{ exists: true, device: uint64(stat.Dev), inode: uint64(stat.Ino), mode: unixFileMode(uint32(stat.Mode)), }, nil } func unixFileMode(mode uint32) fs.FileMode { permissions := fs.FileMode(mode & 0o777) switch mode & unix.S_IFMT { case unix.S_IFDIR: return permissions | fs.ModeDir case unix.S_IFLNK: return permissions | fs.ModeSymlink case unix.S_IFIFO: return permissions | fs.ModeNamedPipe case unix.S_IFSOCK: return permissions | fs.ModeSocket case unix.S_IFCHR: return permissions | fs.ModeDevice | fs.ModeCharDevice case unix.S_IFBLK: return permissions | fs.ModeDevice case unix.S_IFREG: return permissions default: return permissions | fs.ModeIrregular } } func (p *writeParent) createTemp(base string) (*os.File, error) { fd, err := unix.Openat(p.fd, base, unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0o600) if err != nil { return nil, errUnsafePath } return os.NewFile(uintptr(fd), base), nil } func (p *writeParent) remove(base string) error { if err := unix.Unlinkat(p.fd, base, 0); err != nil && !errors.Is(err, unix.ENOENT) { return errUnsafePath } return nil } func (p *writeParent) rename(oldBase, newBase string) error { if err := unix.Renameat(p.fd, oldBase, p.fd, newBase); err != nil { return errUnsafePath } return nil } func (p *writeParent) sync() error { if err := unix.Fsync(p.fd); err != nil { return errUnsafePath } return nil } func (p *writeParent) revalidate() error { current, err := openParentNoFollow(p.entry, p.relative, false) if err != nil { return errUnsafePath } defer current.close() if current.device != p.device || current.inode != p.inode { return errUnsafePath } return nil }