Files
mesh-host/rootsearch/rootsearch.go
T
jochen 2dbbda51c6
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-group group fix/361-the-setuid-search-runs-to-completion ready: every member ready, and composed together they pass
mesh/delivery superseded: a newer head of the same pull request
Walk for setuid programs in the engine and carry an interrupted walk on (hq issue 361)
The control-node's walk takes far longer than the engine runs between
updates, so a search that starts over at every restart never completes.
The walk is the engine's own, in lexical order, so its progress is one
cursor per filesystem, kept every minute beside the engine's state and
carried on after a restart while younger than rootsearch.FreshFor. It runs
on a thread of its own at nice 19 and the idle I/O class.
2026-10-10 01:00:47 +02:00

45 lines
2.9 KiB
Go

// Package rootsearch holds what the node-engine's search for setuid programs and the controller that reads its
// verdicts must agree on (novox/hq ADR 0266, issue 361): how often a full search runs, how long a complete one
// judges, how long the controller stays quiet while one runs, and the words a verdict starts with while it has
// no answer. Public, so the controller imports these rather than keeps copies that could drift.
//
// **Why a complete search may judge for a day and more.** The search looks for a setuid- or setgid-root program
// no package owns, a way for the agent account to become root. Only root can make such a file: the agent account
// can neither set the setuid bit on a file root owns nor give a file it owns to root. So a search that walked
// everything stays sound until root acts — and root's acts through the mesh (an apply that changes an account, a
// group, a sudo rule, a package or runs an action) start a new search at once. What is left is root acting by
// hand, which is the operator; FreshFor bounds how long that goes unseen.
//
// **And why no bound on one search.** On a machine with millions of files the walk takes longer than any bound
// worth stating (the control-node's did not finish in fifteen minutes, issue 361), and a search ended early
// judges nothing, so a bound made such a machine never judged. The search runs to its end at idle priority,
// once at a time, keeping how far its walk got every minute so an engine restarted midway carries it on; an
// incomplete search is never "free". A walk resumed more than FreshFor after it started is begun again.
package rootsearch
import "time"
const (
// Every is how often a full search runs at most, counted from the start of the last complete one; an
// apply that changes what root controls starts one sooner.
Every = 24 * time.Hour
// FreshFor is how long a complete search judges, counted from its start: after it, the verdict is "not
// judged yet" until a newer search completes. Every plus Quiet, so a daily search that finishes within
// the controller's quiet leaves no hour unjudged.
FreshFor = Every + Quiet
// Quiet is how long the controller raises nothing while an agent account waits for a search, counted
// from when it first saw it waiting: the longest a full search is expected to take at idle priority on
// the largest machine. Past it, waiting is itself the urgent condition. It never makes a machine free.
Quiet = 12 * time.Hour
)
// The reasons of a root verdict the search could not answer yet.
const (
// ReasonPending starts the reason while no complete search judges: none has finished since the engine's
// state was kept, or the last one is older than FreshFor, and one runs.
ReasonPending = "not judged yet (search running)"
// ReasonIncomplete starts the reason when no complete search judges and the last one failed: it is tried
// again after a back-off.
ReasonIncomplete = "not judged (search incomplete)"
)