//go:build linux package host import ( "crypto/sha256" "encoding/hex" "errors" "fmt" "io" "os" "path/filepath" "golang.org/x/sys/unix" ) // ownByService hands one directory, and nothing beneath it, to the service // user and its group, as bootstrap's ownership step does // (bootstrapOwner). It opens the directory as bootstrap opens one // (bootstrapOpenDir: no symlink anywhere on the path, not a symlink // itself) and changes the open descriptor, so a path swapped for a // symlink is refused, never followed. func ownByService(path string) error { uid, gid, err := bootstrapOwner(serviceUser, serviceUser) if err != nil { return err } return ownDir(path, uid, gid) } // ownDir is ownByService for a given owner. func ownDir(path string, uid, gid int) error { fd, err := bootstrapOpenDir(path) if err != nil { return err } defer unix.Close(fd) return unix.Fchown(fd, uid, gid) } // openChannelPolicy reads each operator-staged file install-channel --policy // takes, the --policy file and the --identity file (readChannelInputs): // opened with O_NOFOLLOW (a symlink is refused, never followed) and // O_NONBLOCK (a FIFO never blocks the open), fstat'd to be a regular file of // at most limit bytes, and only then read, from the descriptor, up to limit. func openChannelPolicy(path string, limit int64) ([]byte, error) { fd, err := unix.Open(path, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC|unix.O_NONBLOCK, 0) if err != nil { return nil, err } f := os.NewFile(uintptr(fd), path) defer f.Close() st, err := f.Stat() if err != nil { return nil, err } if !st.Mode().IsRegular() { return nil, errors.New("not a regular file") } if st.Size() > limit { return nil, fmt.Errorf("larger than %d bytes", limit) } raw, err := io.ReadAll(io.LimitReader(f, limit+1)) if err != nil { return nil, err } if int64(len(raw)) > limit { return nil, fmt.Errorf("larger than %d bytes", limit) } return raw, nil } // openat2 is unix.Openat2, a seam for the test that a kernel without it // refuses the removal. There is no path-based fallback. var openat2 = unix.Openat2 // slotState opens one protected slot with O_NOFOLLOW, so a symlink is never // followed, and hashes what it holds from the descriptor. A slot that is // absent is slotAbsent; one holding want, slotHolds; a symlink, a // non-regular file or any other content, slotForeign. Any other failure to // open or read it is an error. func slotState(path, want string) (slotKind, error) { fd, err := unix.Open(path, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC|unix.O_NONBLOCK, 0) switch { case errors.Is(err, unix.ENOENT): return slotAbsent, nil case errors.Is(err, unix.ELOOP): return slotForeign, nil case err != nil: return slotAbsent, err } f := os.NewFile(uintptr(fd), path) defer f.Close() st, err := f.Stat() if err != nil { return slotAbsent, err } if !st.Mode().IsRegular() { return slotForeign, nil } h := sha256.New() if _, err := io.Copy(h, io.LimitReader(f, systemArtifactLimit+1)); err != nil { return slotAbsent, err } if want != "" && hex.EncodeToString(h.Sum(nil)) == want { return slotHolds, nil } return slotForeign, nil } // removeCacheEntry removes the agent's cache entry for version, the one // directory install-channel made for the fetch, and what is in it. It never // resolves a path name through a symlink: the router directory is opened // as bootstrapOpenDir opens one (no symlink anywhere on the path), the // version directory is opened beneath it with no symlink followed, and // everything goes by unlinkat on a held descriptor. The service user owns // the parents, so a path-based RemoveAll could be walked out of /data. An // entry that is not there is already removed. func removeCacheEntry(dataDir, version string) error { parts := systemCacheParts(version) router, err := bootstrapOpenDir(filepath.Join(append([]string{dataDir}, parts[:len(parts)-2]...)...)) if err != nil { if errors.Is(err, unix.ENOENT) { return nil } return err } defer unix.Close(router) return removeBeneath(router, parts[len(parts)-2]) } // removeBeneath removes name, a directory, and everything under it, by // descriptor. A symlink is unlinked, never entered. func removeBeneath(parent int, name string) error { fd, err := openat2(parent, name, &unix.OpenHow{ Flags: unix.O_RDONLY | unix.O_DIRECTORY | unix.O_CLOEXEC | unix.O_NOFOLLOW, Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS, }) if errors.Is(err, unix.ENOENT) { return nil } if err != nil { return err } dir := os.NewFile(uintptr(fd), name) names, err := dir.Readdirnames(-1) if err != nil { dir.Close() return err } for _, n := range names { err := unix.Unlinkat(fd, n, 0) if errors.Is(err, unix.EISDIR) { err = removeBeneath(fd, n) } if err != nil && !errors.Is(err, unix.ENOENT) { dir.Close() return err } } dir.Close() if err := unix.Unlinkat(parent, name, unix.AT_REMOVEDIR); err != nil && !errors.Is(err, unix.ENOENT) { return err } return nil }