Walk for setuid programs in the engine and carry an interrupted walk on (hq issue 361)
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
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
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.
This commit is contained in:
+17
-42
@@ -14,6 +14,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -378,51 +379,25 @@ func contains(xs []string, s string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// setuidSearch is every setuid- or setgid-root regular file on the root filesystem that no installed package
|
||||
// owns, found with find and asked of the package manager, through the search's runner (Exec.SearchRun: idle
|
||||
// priority on a serving engine). **No bound of its own** (novox/hq issue 361): it runs until it has walked
|
||||
// everything, and only a context its caller ends stops it — a search that is stopped, or whose walk find could
|
||||
// not complete (it exited with an error), is an unanswered question, never "none", unless every error was a
|
||||
// file that vanished while it walked.
|
||||
func (e Exec) setuidSearch(ctx context.Context, mounts []string) ([]string, error) {
|
||||
// setuidSearch is every setuid- or setgid-root regular file on the filesystems of p that no installed package
|
||||
// owns: walked by the engine (setuid_walk.go, resumable from p), then asked of the package manager through the
|
||||
// search's runner (Exec.SearchRun). **No bound of its own** (novox/hq issue 361): it runs until it has walked
|
||||
// everything. A walk that could not complete — a directory it could not read — is an unanswered question, never
|
||||
// "none"; a file that vanished while it walked is not.
|
||||
func (e Exec) setuidSearch(ctx context.Context, p *SetuidProgress, checkpoint func()) ([]string, error) {
|
||||
walk := e.Walk
|
||||
if walk == nil {
|
||||
walk = walker{every: CheckpointEvery}.walk
|
||||
}
|
||||
if err := walk(ctx, p, checkpoint); err != nil {
|
||||
return nil, fmt.Errorf("the walk for setuid programs did not complete: %w", err)
|
||||
}
|
||||
if e.SearchRun != nil {
|
||||
e.Run = e.SearchRun
|
||||
}
|
||||
// Each mount point a starting point of its own, -xdev keeping each to its own filesystem: every local
|
||||
// filesystem mounted without nosuid is searched once (the re-review of 2026-10-08), and a program setgid to
|
||||
// root's group counts whoever owns it.
|
||||
args := append(append([]string{}, mounts...), "-xdev", "(", "-path", "/proc", "-o", "-path", "/sys", "-o",
|
||||
"-path", "/var/lib/docker", "-o", "-path", "/var/lib/containers", ")", "-prune", "-o",
|
||||
"-type", "f", "(", "(", "-user", "root", "-perm", "-4000", ")", "-o", "(", "-group", "root", "-perm",
|
||||
"-2000", ")", ")", "-print")
|
||||
out, err := e.Run(ctx, "find", args...)
|
||||
if ctx.Err() != nil {
|
||||
return nil, fmt.Errorf("the search for setuid programs was stopped before it finished: %w", ctx.Err())
|
||||
}
|
||||
if err != nil && (strings.TrimSpace(out) == "" || !onlyVanished(err)) {
|
||||
return nil, fmt.Errorf("the search for setuid programs did not complete its walk: %w", err)
|
||||
}
|
||||
var paths []string
|
||||
for _, l := range strings.Split(out, "\n") {
|
||||
if l = strings.TrimSpace(l); l != "" {
|
||||
paths = append(paths, l)
|
||||
}
|
||||
}
|
||||
sort.Strings(paths)
|
||||
return e.unpackaged(ctx, paths)
|
||||
}
|
||||
|
||||
// onlyVanished says every error find reported was a file that went while it walked — the one error that leaves
|
||||
// the walk complete for what still exists. find's own words come after its exit, one per line.
|
||||
func onlyVanished(err error) bool {
|
||||
said := false
|
||||
for _, l := range errorLines(err, "find") {
|
||||
said = true
|
||||
if !strings.Contains(l, "No such file or directory") {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return said
|
||||
found := slices.Clone(p.Found)
|
||||
slices.Sort(found)
|
||||
return e.unpackaged(ctx, slices.Compact(found))
|
||||
}
|
||||
|
||||
// errorLines are the lines a command said on its error stream, as the runner words a failure: "<name> exited N:
|
||||
|
||||
Reference in New Issue
Block a user