//go:build linux package apply // The descriptor half of walk.go and acl.go (novox/hq ADR 0325 §5, §8, §9, §10). import ( "errors" "fmt" "io" "os" "sort" "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, } // visitFunc is what a walk does to one folder or file, open on fd, whose stat is st: whether it changed it, and // whether it found the entry narrowed and could not put it back. An error stops the walk. type visitFunc func(fd int, st unix.Stat_t, folder bool) (changed, narrowed, unrepaired bool, err error) // aclWalk is one walk under a kept folder, from the descriptor of the folder itself. type aclWalk struct { dev uint64 deadline time.Time limit int stats walkStats visit visitFunc counts WalkCounts // at is the names of the levels below the folder 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 } // openKept opens a kept folder as a mesh directory is opened (no link another account could make, the folder // itself no link) and starts a walk from it on the folder's own device. func openKept(path string, limits WalkLimits, stats walkStats, visit visitFunc) (*aclWalk, 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 folder is not there; nothing was changed") } if err != nil { return nil, -1, st, errors.New("the folder is not walked: " + pathless(err)) } if mw.At != path { unix.Close(fd) return nil, -1, st, errors.New("the folder is reached through a link, and the engine walks no folder " + "through one; nothing was changed") } if err := stats.fd(fd, &st); err != nil { unix.Close(fd) return nil, -1, st, fmt.Errorf("the folder cannot be read: %v; nothing was changed", err) } return &aclWalk{dev: uint64(st.Dev), deadline: time.Now().Add(limits.Time), limit: limits.Entries, stats: stats, visit: visit}, fd, st, nil } // walkKept 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 walkKept(path string, limits WalkLimits, stats walkStats, resume []string, visit visitFunc) (*aclWalk, error) { w, fd, st, err := openKept(path, limits, stats, visit) if err != nil { return &aclWalk{}, 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 *aclWalk) 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 } // count adds one visit's result. func (w *aclWalk) count(folder, changed, narrowed, unrepaired bool) { switch { case changed && folder: w.counts.Folders++ case changed: w.counts.Files++ default: w.counts.Kept++ } if narrowed { w.counts.Narrowed++ } if unrepaired { w.counts.Unrepaired++ } } // 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 *aclWalk) 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 *aclWalk) 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. A filesystem that allows no ACLs stops the walk; any other // failure on one entry (an immutable file, say) is counted unrepaired, its words kept once, and the walk goes on. func (w *aclWalk) visited(fd int, st unix.Stat_t, folder bool) error { changed, narrowed, unrepaired, err := w.visit(fd, st, folder) if errors.Is(err, ErrNoACL) { return err } if err != nil { unrepaired = true if w.Failed == "" { w.Failed = err.Error() } } w.count(folder, changed, narrowed, unrepaired) return nil } // getACL reads one of an entry's ACLs through its descriptor: the access ACL, or a folder's default. An access // ACL that is not there is the one its mode stands for; a default that is not there is nil. func getACL(fd int, name string, mode uint32) (acl, error) { buf := make([]byte, 4+8*32) for { n, err := unix.Fgetxattr(fd, name, buf) switch { case errors.Is(err, unix.ENODATA): if name == aclAccessXattr { return minimalACL(mode), nil } return nil, nil case errors.Is(err, unix.ERANGE): buf = make([]byte, 2*len(buf)) continue case errors.Is(err, unix.ENOTSUP), errors.Is(err, unix.EOPNOTSUPP): return nil, ErrNoACL case err != nil: return nil, fmt.Errorf("an entry's ACL could not be read: %v", err) } return decodeACL(buf[:n]) } } // setACL writes one of an entry's ACLs through its descriptor, refusing one the kernel would. func setACL(fd int, name string, a acl) error { if err := a.check(); err != nil { return err } err := unix.Fsetxattr(fd, name, a.encode(), 0) switch { case errors.Is(err, unix.ENOTSUP), errors.Is(err, unix.EOPNOTSUPP): return ErrNoACL case err != nil: return fmt.Errorf("an entry's ACL could not be written: %v", err) } return nil } // sameACL is whether a and b hold the same entries, whatever their order. func sameACL(a, b acl) bool { return string(a.encode()) == string(b.encode()) } // needs is, for a folder or a file, each kept uid's bits — except its owner's, which its owner's bits give and the // mesh never changes. func needs(users map[uint32]bool, owner uint32, folder bool) map[uint32]uint16 { out := map[uint32]uint16{} for uid, rw := range users { if uid != owner { out[uid] = needOn(rw, folder) } } return out } // keepVisit makes users' entries on each entry (ADR 0325 §5): the access ACL with each uid's entry and a mask // that covers it, and on a folder the same as its default, so what is made under it inherits them. An entry that // had them is not written. A user that owns the entry is judged by its owner's bits, never given an entry: an // owner whose bits fall short is unrepaired, said, and never changed. func keepVisit(users map[uint32]bool) visitFunc { return func(fd int, st unix.Stat_t, folder bool) (bool, bool, bool, error) { mode := uint32(st.Mode) & 0o7777 need := needs(users, st.Uid, folder) access, err := getACL(fd, aclAccessXattr, mode) if err != nil { return false, false, false, err } narrowed, unrepaired := judgeACL(access, users, st, folder) changed := false if kept := keptACL(access, need); !sameACL(kept, access) { if err := setACL(fd, aclAccessXattr, kept); err != nil { return false, narrowed, true, err } changed = true } if folder { def, err := getACL(fd, aclDefaultXattr, mode) if err != nil { return changed, narrowed, unrepaired, err } base := def if base == nil { // No default yet: one whose owner's, owning group's and others' entries are the folder's own, as // setfacl -d derives it. base = minimalACL(mode) if g, ok := access.perm(aclGroupObj, 0); ok { base[1].Perm = g } } // The default is for what anyone makes under the folder, never its owner alone: every user's entry. all := map[uint32]uint16{} for uid, rw := range users { all[uid] = needOn(rw, true) } if kept := keptACL(base, all); def == nil || !sameACL(kept, def) { if err := setACL(fd, aclDefaultXattr, kept); err != nil { return changed, narrowed, true, err } changed = true } } return changed, narrowed, unrepaired, nil } } // judgeACL is whether an entry with access ACL a gives a user less than its right — by entry and mask together // (ADR 0325 §6) — and whether that is one the mesh cannot put back: the owner's own bits. func judgeACL(a acl, users map[uint32]bool, st unix.Stat_t, folder bool) (narrowed, unrepaired bool) { mode := uint32(st.Mode) & 0o7777 for uid, rw := range users { need := needOn(rw, folder) if effective(a, uid, st.Uid, mode)&need == need { continue } narrowed = true if uid == st.Uid { unrepaired = true } } return narrowed, unrepaired } // dropVisit takes exactly the entries of each uid of drop out of each entry's access ACL and a folder's default // (ADR 0325 §8): no other entry, and not the mask, so nobody else loses a right. func dropVisit(drop map[uint32]bool) visitFunc { return func(fd int, st unix.Stat_t, folder bool) (bool, bool, bool, error) { mode := uint32(st.Mode) & 0o7777 changed := false for _, name := range []string{aclAccessXattr, aclDefaultXattr} { if name == aclDefaultXattr && !folder { continue } a, err := getACL(fd, name, mode) if errors.Is(err, ErrNoACL) { continue } if err != nil { // Counted unrepaired and said by the walk, which then walks it again: never read as "nothing to drop". return changed, false, true, err } if a == nil { continue } if left := withoutUsers(a, drop); len(left) != len(a) { if err := setACL(fd, name, left); err != nil { return changed, false, true, err } changed = true } } return changed, false, false, nil } } // dropThenKeep is drop then keep, on one walk: a user whose use changed, or one gone beside one come. func dropThenKeep(drop map[uint32]bool, users map[uint32]bool) visitFunc { d, k := dropVisit(drop), keepVisit(users) return func(fd int, st unix.Stat_t, folder bool) (bool, bool, bool, error) { c1, _, _, err := d(fd, st, folder) if err != nil { return c1, false, false, err } c2, n, u, err := k(fd, st, folder) return c1 || c2, n, u, err } } // folderEmpty is whether the folder open on fd holds nothing yet: the data 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) } // keepTop keeps users' entries on the kept folder itself (ADR 0325 §5): what every apply does, beside the walks. func keepTop(path string, users map[uint32]bool) (changed bool, err error) { w, fd, st, err := openKept(path, WalkLimits{Entries: 1, Time: time.Minute}, machineStats, keepVisit(users)) if err != nil { return false, err } defer unix.Close(fd) changed, _, _, err = w.visit(fd, st, true) return changed, err } // FolderFacts is what the engine reads of a folder before keeping it: its owner, group and whether it holds data. type FolderFacts struct { UID, GID 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, Empty: empty}, nil } // CreatePath makes a missing accessed folder (ADR 0325 §9): each missing level made from the nearest existing // ancestor down, each with that ancestor's owner, group and mode. No link is followed (the ancestor is reached as // a mesh directory is, and each level is opened with O_NOFOLLOW), no level is made on another filesystem, no file // is made, and nothing that was there is changed: a level that appears meanwhile stops it. The caller has judged // that the ancestor is on the placed filesystem and not root's. How many levels it made. func CreatePath(path string) (int, error) { return createPath(path, machineStats) } func createPath(path string, stats walkStats) (int, error) { m, err := NearestAncestor(path) if err != nil { return 0, fmt.Errorf("the folder cannot be made: %v; nothing was changed", err) } if !m.Info.IsDir() { return 0, errors.New("its nearest existing parent is not a folder (a file or a link), and a folder is " + "made only below a folder; nothing was changed") } mw := &meshWalk{want: m.Ancestor} fd, err := mw.open() if err != nil { return 0, fmt.Errorf("the nearest existing parent is not walked: %v; nothing was changed", err) } if mw.At != m.Ancestor { unix.Close(fd) return 0, errors.New("the nearest existing parent is reached through a link, and nothing is made " + "through one; nothing was changed") } var anc unix.Stat_t if err := stats.fd(fd, &anc); err != nil { unix.Close(fd) return 0, fmt.Errorf("the nearest existing parent cannot be read: %v; nothing was changed", err) } if why := rootOwned(anc.Uid, anc.Gid); why != "" { unix.Close(fd) return 0, fmt.Errorf("its nearest existing parent is %s, and nothing is made under root's data "+ "(novox/hq ADR 0325 §12); nothing was changed", why) } mode := uint32(anc.Mode) & 0o7777 made := 0 for _, name := range m.Levels { // Made for its maker alone (the engine, as root), so it is nobody else's until it is the ancestor's. if err := unix.Mkdirat(fd, name, 0o700); err != nil { unix.Close(fd) if errors.Is(err, unix.EEXIST) { return made, errors.New("a level of the folder appeared while it was made, and nothing that is " + "there is changed; it stopped there") } return made, fmt.Errorf("a level could not be made: %v", err) } next, err := unix.Openat(fd, name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) unix.Close(fd) if err != nil { return made, fmt.Errorf("a level made could not be opened: %v", err) } fd = next made++ var st unix.Stat_t if err := stats.fd(fd, &st); err != nil { unix.Close(fd) return made, fmt.Errorf("a level made could not be read: %v", err) } if uint64(st.Dev) != uint64(anc.Dev) { unix.Close(fd) return made, errors.New("a level made is on another filesystem than its parent, and it stopped there") } if st.Uid != anc.Uid || st.Gid != anc.Gid { if err := unix.Fchown(fd, int(anc.Uid), int(anc.Gid)); err != nil { unix.Close(fd) return made, fmt.Errorf("a level made could not be given its parent's owner: %v", err) } } if err := unix.Fchmod(fd, mode); err != nil { unix.Close(fd) return made, fmt.Errorf("a level made could not be given its parent's mode: %v", err) } } unix.Close(fd) 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 "" }