//go:build linux package apply // The descriptor half of walk.go and library.go (novox/hq ADR 0328 §6, §7, §9). import ( "errors" "fmt" "io" "os" "sort" "strings" "time" "golang.org/x/sys/unix" ) // walkStats is how a walk reads an entry and an open descriptor: the machine's own calls, or a test's stand-in // for a device it cannot mount. type walkStats struct { at func(dirfd int, name string, st *unix.Stat_t) error fd func(fd int, st *unix.Stat_t) error } var machineStats = walkStats{ at: func(dirfd int, name string, st *unix.Stat_t) error { return unix.Fstatat(dirfd, name, st, unix.AT_SYMLINK_NOFOLLOW) }, fd: unix.Fstat, } // What a visit did to one entry, as flags: nothing (it had what it needs), or changed, and beside it whether it was a // file with another name (linked, changed; shared, left), and whether its setuid or setgid bit went. const ( visitKept = 0 visitChanged = 1 << iota visitLinked // a file with another name, owned by an account: given the group visitShared // a file with another name, owned by root: left as it is visitStripped // a file whose setuid or setgid bit the group change took, and which stays taken ) // visitFunc is what a walk does to one folder or file, open on fd, whose stat is st: what it did, or the error // counted on that entry alone (errStopWalk stops the walk). type visitFunc func(fd int, st unix.Stat_t, folder bool) (int, error) // errStopWalk wraps an error that stops the whole walk, never one entry's alone. type errStopWalk struct{ err error } func (e errStopWalk) Error() string { return e.err.Error() } func (e errStopWalk) Unwrap() error { return e.err } // treeWalk is one walk under a library's root, from the descriptor of the root itself. type treeWalk struct { dev uint64 deadline time.Time limit int stats walkStats visit visitFunc counts WalkCounts // at is the names of the levels below the root down to the entry being walked; Cursor, once the walk met its // bound, where the next goes on. at []string Cursor []string // Failed is the first failure on one entry, in words with no path. Failed string } // openRoot opens a library's root as a mesh directory is opened (no link another account could make, the root // itself no link) and starts a walk from it on the root's own device. func openRoot(path string, limits WalkLimits, stats walkStats, visit visitFunc) (*treeWalk, int, unix.Stat_t, error) { var st unix.Stat_t if err := keptPath(path); err != nil { return nil, -1, st, err } mw := &meshWalk{want: path} fd, err := mw.open() if errors.Is(err, errNotThereYet) { return nil, -1, st, errors.New("the root is not there; nothing was changed") } if err != nil { return nil, -1, st, errors.New("the root is not walked: " + pathless(err)) } if mw.At != path { unix.Close(fd) return nil, -1, st, errors.New("the root is reached through a link, and the engine walks no root through " + "one; nothing was changed") } if err := stats.fd(fd, &st); err != nil { unix.Close(fd) return nil, -1, st, fmt.Errorf("the root cannot be read: %v; nothing was changed", err) } return &treeWalk{dev: uint64(st.Dev), deadline: time.Now().Add(limits.Time), limit: limits.Entries, stats: stats, visit: visit}, fd, st, nil } // walkRoot walks path whole, or from resume, with visit: the walk, with what it did and the cursor where it stopped // when it met its bound (with ErrWalkBound), and the walk's own error otherwise. func walkRoot(path string, limits WalkLimits, stats walkStats, resume []string, visit visitFunc) (*treeWalk, error) { w, fd, st, err := openRoot(path, limits, stats, visit) if err != nil { return &treeWalk{}, err } return w, w.dir(fd, st, resume) } // read counts one entry, and says the bound when it is met, keeping where the walk stood as the cursor. func (w *treeWalk) read() error { if w.counts.Read >= w.limit || time.Now().After(w.deadline) { w.Cursor = append([]string(nil), w.at...) return ErrWalkBound } w.counts.Read++ return nil } // dir is one folder, open on fd, whose stat is st: the folder itself (unless the walk resumes below it, which an // earlier walk visited), then every entry in it in the order of their names. It closes fd. func (w *treeWalk) dir(fd int, st unix.Stat_t, resume []string) error { f := os.NewFile(uintptr(fd), "") defer f.Close() if len(resume) == 0 { if err := w.read(); err != nil { return err } if err := w.visited(fd, st, true); err != nil { return err } } names, err := f.Readdirnames(-1) if err != nil { return fmt.Errorf("a folder could not be read: %v", err) } sort.Strings(names) for _, name := range names { var below []string if len(resume) > 0 { if name < resume[0] { continue } if name == resume[0] { below = resume[1:] } resume = nil } w.at = append(w.at, name) err := w.entry(fd, name, below) w.at = w.at[:len(w.at)-1] if err != nil { return err } } return nil } // entry is one name in the folder open on dirfd: a link passed by, a folder on this device walked, a file visited // through its own descriptor, anything else left. func (w *treeWalk) entry(dirfd int, name string, resume []string) error { var st unix.Stat_t if err := w.stats.at(dirfd, name, &st); err != nil { if errors.Is(err, unix.ENOENT) { return nil } return fmt.Errorf("an entry could not be read: %v", err) } switch st.Mode & unix.S_IFMT { case unix.S_IFLNK: w.counts.Links++ return nil case unix.S_IFDIR, unix.S_IFREG: default: w.counts.Other++ return nil } if uint64(st.Dev) != w.dev { w.counts.Mounts++ return nil } isDir := st.Mode&unix.S_IFMT == unix.S_IFDIR flags := unix.O_RDONLY | unix.O_NOFOLLOW | unix.O_CLOEXEC | unix.O_NOCTTY | unix.O_NONBLOCK if isDir { flags |= unix.O_DIRECTORY } fd, err := unix.Openat(dirfd, name, flags, 0) switch { case errors.Is(err, unix.ENOENT): return nil case errors.Is(err, unix.ELOOP), errors.Is(err, unix.ENOTDIR): // Swapped for a link, or for something else, after it was looked at: passed by. w.counts.Links++ return nil case err != nil: return fmt.Errorf("an entry could not be opened: %v", err) } var opened unix.Stat_t if err := w.stats.fd(fd, &opened); err != nil { unix.Close(fd) return fmt.Errorf("an entry could not be read: %v", err) } if opened.Dev != st.Dev || opened.Ino != st.Ino || opened.Mode&unix.S_IFMT != st.Mode&unix.S_IFMT { unix.Close(fd) return errors.New("an entry changed while it was walked, and the walk stopped there") } if isDir { return w.dir(fd, opened, resume) } defer unix.Close(fd) if err := w.read(); err != nil { return err } return w.visited(fd, opened, false) } // visited visits one entry and counts what it did. An error that stops the walk stops it; any other failure on // one entry (an immutable file, say) is counted failed, its words kept once, and the walk goes on. func (w *treeWalk) visited(fd int, st unix.Stat_t, folder bool) error { did, err := w.visit(fd, st, folder) var stop errStopWalk if errors.As(err, &stop) { return stop.err } if err != nil { w.counts.Failed++ if w.Failed == "" { w.Failed = err.Error() } return nil } switch { case did&visitShared != 0: w.counts.Shared++ case did&visitChanged != 0 && folder: w.counts.Folders++ case did&visitChanged != 0: w.counts.Files++ default: w.counts.Kept++ } if did&visitLinked != 0 { w.counts.Linked++ } if did&visitStripped != 0 { w.counts.Stripped++ } return nil } // groupVisit gives each entry the library's group gid, group read and write, and on a folder group enter and the // setgid bit, so what is made beneath keeps the group (ADR 0328 §6, §7). The owner, the owner's bits and others' // bits are never changed — but for what the group change itself takes: // // - **A file's setuid and setgid bits stay taken.** The kernel drops them when a file's group changes; a setgid // file in the library's group would hand anyone who runs it the group, and a setuid one is no library data. They // are never put back, setgid is taken off a file the group change left it on, and each such file is counted. // - **A file with another name** (the *arr apps hardlink downloads into the library) is given the group when an // account owns it: the kernel's protected_hardlinks already keeps an account from linking a file it does not // own. One root owns is left as it is: its other name may be anywhere, and the group would reach it. Both are // counted. func groupVisit(gid int) visitFunc { return func(fd int, st unix.Stat_t, folder bool) (int, error) { did := visitKept if !folder && st.Nlink > 1 { if st.Uid == 0 { return visitShared, nil } did |= visitLinked } mode := uint32(st.Mode) & 0o7777 want := mode | 0o060 if folder { want = mode | 0o070 | unix.S_ISGID } regroup := int(st.Gid) != gid if !folder && regroup { want &^= unix.S_ISUID | unix.S_ISGID } if !regroup && want == mode { return did &^ visitLinked, nil } if regroup { if err := unix.Fchown(fd, -1, gid); err != nil { return 0, fmt.Errorf("an entry's group could not be set: %v", err) } } if err := unix.Fchmod(fd, want); err != nil { return 0, fmt.Errorf("an entry's mode could not be set: %v", err) } if !folder && mode&(unix.S_ISUID|unix.S_ISGID) != 0 && want&(unix.S_ISUID|unix.S_ISGID) == 0 { did |= visitStripped } return did | visitChanged, nil } } // folderEmpty is whether the folder open on fd holds nothing yet: a root the mesh may keep without asking. func folderEmpty(fd int) (bool, error) { dup, err := unix.Dup(fd) if err != nil { return false, err } f := os.NewFile(uintptr(dup), "") defer f.Close() names, err := f.Readdirnames(1) switch { case len(names) > 0: return false, nil case err == nil, errors.Is(err, io.EOF): return true, nil } return false, fmt.Errorf("the folder could not be read: %v", err) } // FolderFacts is what the engine reads of a library's root before keeping it: its owner, group, mode and whether // it holds data. type FolderFacts struct { UID, GID uint32 Mode uint32 Empty bool } // readFolder reads a folder's facts through the descriptor a mesh directory is opened by. func readFolder(path string) (FolderFacts, error) { if err := keptPath(path); err != nil { return FolderFacts{}, err } mw := &meshWalk{want: path} fd, err := mw.open() if err != nil { return FolderFacts{}, err } defer unix.Close(fd) if mw.At != path { return FolderFacts{}, errors.New("the folder is reached through a link, and the engine keeps no folder " + "through one") } var st unix.Stat_t if err := unix.Fstat(fd, &st); err != nil { return FolderFacts{}, err } empty, err := folderEmpty(fd) if err != nil { return FolderFacts{}, err } return FolderFacts{UID: st.Uid, GID: st.Gid, Mode: uint32(st.Mode) & 0o7777, Empty: empty}, nil } // keepFolders keeps a library's root and each of its requested sub-paths — the folders themselves, never what is // in them — in the group gid with group read, write and enter and the setgid bit (ADR 0328 §6). A sub-path that is // missing is made, each level with the root's owner, the group, and the root's owner's and others' bits beside // group rwx and setgid (§9). No link is followed (the root is reached as a mesh directory is, and every level below // it by O_NOFOLLOW), nothing is made or changed on another filesystem, and the owner and the owner's and others' // bits of a folder that was there are never changed. How many folders it changed and made. func keepFolders(root string, paths []string, gid int) (changed, made int, err error) { _, fd, rst, err := openRoot(root, WalkLimits{Entries: 1, Time: time.Minute}, machineStats, nil) if err != nil { return 0, 0, err } defer unix.Close(fd) keep := func(fd int, st unix.Stat_t) error { mode := uint32(st.Mode) & 0o7777 want := mode | 0o070 | unix.S_ISGID if int(st.Gid) == gid && want == mode { return nil } if int(st.Gid) != gid { if err := unix.Fchown(fd, -1, gid); err != nil { return fmt.Errorf("a folder's group could not be set: %v", err) } } if err := unix.Fchmod(fd, want); err != nil { return fmt.Errorf("a folder's mode could not be set: %v", err) } changed++ return nil } if err := keep(fd, rst); err != nil { return changed, made, err } madeMode := uint32(rst.Mode)&0o0707 | 0o070 | unix.S_ISGID for _, p := range paths { n, err := keepPath(fd, rst, p, gid, madeMode, keep) made += n if err != nil { return changed, made, err } } return changed, made, nil } // keepPath walks one sub-path down from the root open on rootFd, making each missing level, and keeps the last. func keepPath(rootFd int, rst unix.Stat_t, p string, gid int, madeMode uint32, keep func(int, unix.Stat_t) error) (int, error) { made := 0 cur, err := unix.Dup(rootFd) if err != nil { return made, err } defer func() { unix.Close(cur) }() const dirFlags = unix.O_RDONLY | unix.O_DIRECTORY | unix.O_NOFOLLOW | unix.O_CLOEXEC levels := strings.Split(p, "/") for i, name := range levels { next, err := unix.Openat(cur, name, dirFlags, 0) if errors.Is(err, unix.ENOENT) { // Made for its maker alone (the engine, as root), so it is nobody else's until it is the root's. if mkErr := unix.Mkdirat(cur, name, 0o700); mkErr != nil && !errors.Is(mkErr, unix.EEXIST) { return made, fmt.Errorf("a level of the path %s could not be made: %v", p, mkErr) } if next, err = unix.Openat(cur, name, dirFlags, 0); err == nil { if err = unix.Fchown(next, int(rst.Uid), gid); err == nil { err = unix.Fchmod(next, madeMode) } if err != nil { unix.Close(next) return made, fmt.Errorf("a level of the path %s made could not be given the root's owner "+ "and the library's group: %v", p, err) } made++ } } if errors.Is(err, unix.ELOOP) || errors.Is(err, unix.ENOTDIR) { return made, fmt.Errorf("the path %s is not a folder below the root (a link, or a file), and the "+ "engine keeps no path through one", p) } if err != nil { return made, fmt.Errorf("the path %s could not be opened: %v", p, err) } unix.Close(cur) cur = next var st unix.Stat_t if err := unix.Fstat(cur, &st); err != nil { return made, err } if st.Dev != rst.Dev { return made, fmt.Errorf("the path %s lies on another filesystem than the root, and the engine keeps "+ "nothing there", p) } if i == len(levels)-1 { if err := keep(cur, st); err != nil { return made, err } } } return made, nil } // MountFsid is the kernel's id of the filesystem at point (statfs f_fsid), in hex; empty when it cannot be read. // A variable so a test can stand in for a filesystem it cannot mount. var MountFsid = func(point string) string { var st unix.Statfs_t if err := unix.Statfs(point, &st); err != nil { return "" } return fmt.Sprintf("%08x%08x", uint32(st.Fsid.Val[0]), uint32(st.Fsid.Val[1])) } // MountUUID is the UUID of the filesystem at point when it is on a block device: the name in /dev/disk/by-uuid of // the device the mount's own device number is. Empty for one on no block device (NFS, FUSE, ZFS, a btrfs // subvolume's own device). A variable so a test can stand in for disks it cannot make. var MountUUID = func(point string) string { var st unix.Stat_t if err := unix.Stat(point, &st); err != nil { return "" } const dir = "/dev/disk/by-uuid" entries, err := os.ReadDir(dir) if err != nil { return "" } for _, e := range entries { var dev unix.Stat_t if unix.Stat(dir+"/"+e.Name(), &dev) == nil && dev.Mode&unix.S_IFMT == unix.S_IFBLK && dev.Rdev == st.Dev { return e.Name() } } return "" } // SampleLimits bound the sample that judges whether a root's data is already the library's: so many entries a // folder, so many levels down, for so long. A sample that runs out of time is not in shape: it asks. var SampleLimits = struct { PerFolder, Levels int Time time.Duration }{PerFolder: 20, Levels: 3, Time: 2 * time.Second} // sampleInShape is whether the first entries of each folder under root, a few levels down, are all in the group gid // with group read and write, and folders with group enter and setgid (ADR 0328 §7). Reads only, by descriptor: no // link followed, no other filesystem entered. A file with more than one name is not judged (the take act leaves it). func sampleInShape(root string, gid int) bool { _, fd, st, err := openRoot(root, WalkLimits{Entries: 1, Time: time.Minute}, machineStats, nil) if err != nil { return false } deadline := time.Now().Add(SampleLimits.Time) return sampleFolder(fd, uint64(st.Dev), gid, SampleLimits.Levels, deadline) } // sampleFolder judges the folder open on fd's first entries, and the folders among them levels further. It closes fd. func sampleFolder(fd int, dev uint64, gid, levels int, deadline time.Time) bool { f := os.NewFile(uintptr(fd), "") defer f.Close() names, err := f.Readdirnames(SampleLimits.PerFolder) if err != nil && !errors.Is(err, io.EOF) { return false } for _, name := range names { if time.Now().After(deadline) { return false } var st unix.Stat_t if err := unix.Fstatat(fd, name, &st, unix.AT_SYMLINK_NOFOLLOW); err != nil { return false } if uint64(st.Dev) != dev { continue } mode := uint32(st.Mode) switch mode & unix.S_IFMT { case unix.S_IFREG: if st.Nlink == 1 && (int(st.Gid) != gid || mode&0o060 != 0o060) { return false } case unix.S_IFDIR: if int(st.Gid) != gid || mode&0o2070 != 0o2070 { return false } if levels > 1 { next, err := unix.Openat(fd, name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) if err != nil || !sampleFolder(next, dev, gid, levels-1, deadline) { return false } } } } return true }