ADR 0328 supersedes ADR 0325: shared data is a library with one home, kept usable by its group rather than by ACL entries. The engine now makes each library's group and each module's user in its libraries' groups; keeps a library's root and requested sub-paths in its group with group write and setgid on its home, making missing sub-paths as the root is; waits for the operator's word before touching a root that holds data, and on the take act walks it by descriptor (no link, same filesystem, bounded and resumed, files with other names left); holds the containers of a library whose disk or mount is not there; and puts the tool runner's account in the group. The ACL keeping, mask repair and watch are gone; an access is again as before ADR 0325.
451 lines
14 KiB
Go
451 lines
14 KiB
Go
//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.
|
|
const (
|
|
visitKept = iota // it had what it needs
|
|
visitChanged // it was changed
|
|
visitShared // a file with another name elsewhere: left as it is
|
|
)
|
|
|
|
// 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:
|
|
w.counts.Shared++
|
|
case did == visitChanged && folder:
|
|
w.counts.Folders++
|
|
case did == visitChanged:
|
|
w.counts.Files++
|
|
default:
|
|
w.counts.Kept++
|
|
}
|
|
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: a file's own setuid or setgid bit, which the kernel drops when the group changes, is put
|
|
// back as it was. A file with more than one name is left as it is — its other name may be outside the library, and
|
|
// giving it the group would give the group that other file.
|
|
func groupVisit(gid int) visitFunc {
|
|
return func(fd int, st unix.Stat_t, folder bool) (int, error) {
|
|
if !folder && st.Nlink > 1 {
|
|
return visitShared, nil
|
|
}
|
|
mode := uint32(st.Mode) & 0o7777
|
|
want := mode | 0o060
|
|
if folder {
|
|
want = mode | 0o070 | unix.S_ISGID
|
|
}
|
|
if int(st.Gid) == gid && want == mode {
|
|
return visitKept, nil
|
|
}
|
|
if int(st.Gid) != gid {
|
|
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)
|
|
}
|
|
return 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 ""
|
|
}
|