A module's containers ran as whatever their image chose and shared folders were made writable by putting accounts in the folder's group (ADR 0312), which ADR 0325 supersedes. The engine now makes each declared module identity as a system account with the controller's numbers, runs a container declaring `identity` as it, and keeps each placed shared folder usable through one POSIX ACL entry per user, set through the kernel's xattrs on descriptors it opened itself. A folder holding data is kept only after the operator's warrant (the take act); a missing folder is made where its data lives and waits where its disk is not; root's data is never kept. A watch and a bounded sweep put back what an app narrows. The group grant is removed, and what it put an account in is taken back. Reuses the descriptor walks, the create act, the missing-path wait and the warrant- backed act message from the paused issue 567 branch.
537 lines
17 KiB
Go
537 lines
17 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, fmt.Errorf("the folder is not walked: %v", 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) || a == nil {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return changed, false, false, err
|
|
}
|
|
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
|
|
}
|