mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/358-judge-directories-read-only delivered: every member is delivered
Once a mesh directory's mode and owner go only through a walk that follows no link another account could make, a directory behind such a link is refused and its module gated: on the control-node that could stop the controller. The operator is to be told which directories that walk would refuse on every node before it lands. So the engine judges the declaration it keeps with the same walk, making and changing nothing: on a signed ask from the controller (mesh.node.<node>.ask.judge-directories, held to the hand-over's checks) and as `nox-mesh-host judge-directories` at the machine.
126 lines
5.7 KiB
Go
126 lines
5.7 KiB
Go
package apply
|
|
|
|
// A mesh directory outside a home is given its mode and owner only through a descriptor (novox/hq issue 358).
|
|
//
|
|
// Below a home every component is opened without following a link (ADR 0266, home_links.go), and a directory the
|
|
// operator handed to the mesh is changed through one descriptor opened from / with no link followed (issue 356).
|
|
// Every other directory the mesh owns — a module's state under /var/lib, a drop-in directory under /etc — was
|
|
// given its mode and owner by path, with chmod and chown, which follow a link anywhere in the path: whoever could
|
|
// turn a parent into a link between two applies directed root's chmod and chown at the link's target.
|
|
//
|
|
// The danger is a link another account can make or replace, not a link as such: /var/run is a link to /run on
|
|
// every node, and a disk is sometimes reached through one. So (the decision recorded on issue 358, 2026-10-11):
|
|
//
|
|
// - **the directory is opened component by component from /**, each with O_DIRECTORY|O_NOFOLLOW, and its mode
|
|
// and owner are changed with fchmod and fchown on that descriptor; a directory the walk makes is made in its
|
|
// parent's descriptor. A link put in place after any check is refused by the kernel at the moment of use;
|
|
// - **a link in a parent is followed only when root alone could have put it there**: the link is root's, and
|
|
// every directory from / down to the one holding it is root's and writable by no other account (no write for
|
|
// others, no write for a group other than root's). Its target is then walked the same way, from /;
|
|
// - **any other link refuses that directory**, naming the link and the account that could replace it; nothing
|
|
// is changed there, and the rest of the declaration applies;
|
|
// - **the directory itself is never a link** (issue 339, applyDirectory).
|
|
//
|
|
// A directory below a home keeps the walk of ADR 0266, which follows no link below the home at all.
|
|
|
|
import (
|
|
"fmt"
|
|
osuser "os/user"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
)
|
|
|
|
// LinkInPathError is a mesh directory refused because a link in its path is one another account could replace.
|
|
type LinkInPathError struct {
|
|
// Path is the directory declared, Link the link in its path, Who the account that could replace it.
|
|
Path, Link, Who string
|
|
}
|
|
|
|
func (e *LinkInPathError) Error() string { return e.Path + " " + e.why() }
|
|
|
|
// why is the refusal without the path it refuses, as a PlacementRefusedError says it.
|
|
func (e *LinkInPathError) why() string {
|
|
return fmt.Sprintf("is reached through the link %s, which %s could replace: the node-engine runs as root and "+
|
|
"follows a link to a directory it gives a mode and owner only when root alone could have made it (novox/hq "+
|
|
"issue 358). Make %s a directory, or make it and every directory above it root's and writable by no other "+
|
|
"account", e.Link, e.Who, e.Link)
|
|
}
|
|
|
|
// couldReplace says who besides root could replace an entry of a directory with this owner, group and mode: ""
|
|
// when nobody can. A variable so a test, which runs below a directory every account may write (/tmp), can name
|
|
// which of its own directories count as root's alone.
|
|
var couldReplace = func(path string, uid, gid, mode uint32) string {
|
|
if uid != 0 {
|
|
return "the account " + accountNamed(uid) + " (it owns " + path + ")"
|
|
}
|
|
if mode&0o002 != 0 {
|
|
return "every account (" + path + " is writable by all)"
|
|
}
|
|
if mode&0o020 != 0 && gid != 0 {
|
|
return "the group " + groupNamed(gid) + " (it may write " + path + ")"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// linkReplacer says who besides root owns a link: "" when root does. A variable for the same reason.
|
|
var linkReplacer = func(link string, uid uint32) string {
|
|
if uid != 0 {
|
|
return "the account " + accountNamed(uid) + " (it owns the link)"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func accountNamed(uid uint32) string {
|
|
if u, err := osuser.LookupId(strconv.FormatUint(uint64(uid), 10)); err == nil {
|
|
return u.Username
|
|
}
|
|
return "with uid " + strconv.FormatUint(uint64(uid), 10)
|
|
}
|
|
|
|
func groupNamed(gid uint32) string {
|
|
if g, err := osuser.LookupGroupId(strconv.FormatUint(uint64(gid), 10)); err == nil {
|
|
return g.Name
|
|
}
|
|
return "with gid " + strconv.FormatUint(uint64(gid), 10)
|
|
}
|
|
|
|
// JudgeDirectories is the walk's judgement over every directory a declaration names, read-only (novox/hq issue
|
|
// 358): nothing is made, chmodded or chowned. One line per directory, in the declaration's order, after one line
|
|
// that counts them: placed, would be refused and why, refused already by the placement guard (issue 339), or
|
|
// below a home, where ADR 0266's walk judges it as before.
|
|
func JudgeDirectories(d *declaration.Declaration) string {
|
|
var lines []string
|
|
var placed, refused, guarded, homed int
|
|
for _, r := range d.Resources {
|
|
dir, ok := r.(*declaration.Directory)
|
|
if !ok {
|
|
continue
|
|
}
|
|
path := filepath.Clean(dir.Path)
|
|
if err := refusePath(path, placingDirectory, moduleOfID(dir.ID), dir.Owner); err != nil {
|
|
guarded++
|
|
lines = append(lines, path+": refused already by the placement guard: "+err.Error())
|
|
continue
|
|
}
|
|
if home := homeAbove(path); home != "" {
|
|
homed++
|
|
lines = append(lines, path+": below the home "+home+", walked without following any link as before (ADR 0266)")
|
|
continue
|
|
}
|
|
if err := judgeMeshDir(path); err != nil {
|
|
refused++
|
|
lines = append(lines, path+": would be refused: "+err.Error())
|
|
continue
|
|
}
|
|
placed++
|
|
lines = append(lines, path+": placed")
|
|
}
|
|
head := fmt.Sprintf("%d directories declared: %d placed, %d would be refused, %d refused already by the "+
|
|
"placement guard, %d below a home (novox/hq issue 358; judged read-only, nothing changed)",
|
|
placed+refused+guarded+homed, placed, refused, guarded, homed)
|
|
return strings.Join(append([]string{head}, lines...), "\n")
|
|
}
|