Files
mesh-host/internal/apply/walk_linux.go
T
jschoubben 46c16e18fe Name a filesystem by its UUID, its kernel id or its source, and keep nothing that has no name (hq ADR 0325 fifth review)
NFS and FUSE report a zero f_fsid, so another export or another FUSE mount at the
same point was named alike and kept without a warrant; xfs's f_fsid follows the
device number. A filesystem is now named by its UUID (its device matched in
/dev/disk/by-uuid), else a nonzero f_fsid, else mountinfo's source; one that cannot
be named is never kept or taken. Entries left on a filesystem not back within 30 days
are forgotten, said once, and the naming read is shared per mount.
2026-10-12 19:14:42 +02:00

573 lines
18 KiB
Go

//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 ""
}