//go:build unix package host import ( "errors" "fmt" "os" "path/filepath" "strings" "golang.org/x/sys/unix" "syscall" ) // errRegistryFileShape refuses a registry path that is not a plain, // single-link regular file. /data belongs to the service user, so anything // there may have been planted: a symlink or a hard link to a root-owned file // would otherwise hand that file to the service user when root chowns it. var errRegistryFileShape = errors.New("not a plain single-link regular file") // ownRegistryFile gives path to uid:gid without following a symlink and // without touching any file reachable by another name. It opens the path // O_NOFOLLOW, checks the opened file itself, and changes ownership through // that descriptor, so a swap between the check and the chown cannot redirect // it. A missing file reports os.ErrNotExist; changed is false when the file // was already owned as wanted. func ownRegistryFile(path string, uid, gid int) (changed bool, err error) { parent, name, err := openRegistryParent(path) if err != nil { return false, err } defer parent.Close() fd, err := openRegistryDescriptor(int(parent.Fd()), name) var f *os.File if err == nil { f = os.NewFile(uintptr(fd), path) } if err != nil { if errors.Is(err, syscall.ELOOP) { return false, fmt.Errorf("%s: %w", path, errRegistryFileShape) } return false, err } defer f.Close() fi, err := f.Stat() if err != nil { return false, err } st, ok := fi.Sys().(*syscall.Stat_t) if !ok || !fi.Mode().IsRegular() || st.Nlink != 1 { return false, fmt.Errorf("%s: %w", path, errRegistryFileShape) } if int(st.Uid) == uid && int(st.Gid) == gid { return false, nil } if err := chownRegistryDescriptor(f, uid, gid); err != nil { return false, err } return true, nil } // Walk each parent without following symlinks, keeping the final directory pinned. func openRegistryParent(path string) (*os.File, string, error) { absolute, err := filepath.Abs(path) if err != nil { return nil, "", err } parent, err := os.Open("/") if err != nil { return nil, "", err } for _, part := range strings.Split(strings.TrimPrefix(filepath.Dir(absolute), "/"), "/") { if part == "" { continue } fd, err := unix.Openat(int(parent.Fd()), part, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) parent.Close() if err != nil { return nil, "", err } parent = os.NewFile(uintptr(fd), part) } return parent, filepath.Base(absolute), nil }