fix(teardown): make the leaked-process reap prove what it reaped - #3364
matthewstrud wants to merge 5 commits into
Conversation
The reap enumerated leaked processes with lsof. On a host without lsof it silently did nothing while reporting success, so torn-down workers left whole headless browser trees and dev servers running. A guard that silently no-ops is worse than no guard, because it is trusted. Enumeration moves to a reusable library, bin/fm-process-tree-lib.sh, with /proc as the sole coverage authority and no second enumerator to be wrong in a different way. It walks the WHOLE tree from each cwd-matching root rather than counting orphans by PPID==1, which undercounts badly: an orphan's own children reparent to it and read as healthy, so a handful of apparent orphans can be the visible tip of a much larger tree. Only exact PIDs bound to their birth identity are signaled, and identity is rechecked immediately before each individual signal, so a recycled PID is never hit. There is no pattern matching anywhere: sibling worktree processes carry identical command lines. Identity and run state are read from one /proc record so they cannot describe different processes. Coverage and ownership are separated deliberately. An unbindable identity is uncovered ground and refuses. An unreadable cwd is weaker evidence: if ancestry reached the process it is covered and signaled; if not, its pid is disclosed and teardown proceeds. Refusing on every unreadable cwd would refuse forever on any ordinary Linux host, where the session manager and ssh session processes always hide their cwd, yielding a teardown that never cleans anything. The residual is stated in the header rather than hidden. Tests cover whole-tree reaping, reparenting after the parent exits, recycled PIDs, exec changes, foreign-uid intermediaries, and refusal paths. One case runs against the real /proc of the running machine: the synthetic /proc used elsewhere contains no nondumpable processes, so a suite green against it once hid a change that refused on every real host.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 37e1db242b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| proc_root=${FM_PROC_ROOT_OVERRIDE:-/proc} | ||
| [ -d "$proc_root" ] && [ -r "$proc_root" ] && [ -x "$proc_root" ] || { | ||
| fm_process_error "/proc process-cwd enumeration is unavailable" | ||
| return 1 |
There was a problem hiding this comment.
Preserve teardown support on macOS
Captain, on every supported macOS installation, /proc does not exist, so this new mandatory check always returns "/proc process-cwd enumeration is unavailable"; reap_task_worktree_processes consequently refuses before returning the worktree or removing task state. Since the README advertises macOS support and includes macOS-only backends, teardown needs a Darwin-compatible enumerator or another ownership boundary rather than unconditionally requiring Linux /proc.
Useful? React with 👍 / 👎.
Confidence Score: 5/5The PR appears safe to merge, with no concrete changed-code defect remaining after review. The new teardown path binds discovered processes to Reviews (1): Last reviewed commit: "no-mistakes(document): Document process-..." | Re-trigger Greptile |
|
Closing: this was raised against this repository by a tooling default rather than by intent. The work belongs on the author's fork, and any future contribution here will be opened deliberately. Apologies for the noise. |
What this fixes
bin/fm-teardown.shenumerated leaked processes withlsof. On a host withoutlsofthe reap silently did nothing while reporting success. Around eight torn-down workers each left a headless browser tree alive, plus dev servers still serving hours later; the machine reached load 116 on 8 cores with 0 of 15 GB free and had to be recovered by hand, twice.The defect is not the missing tool. It is that a guard which cannot run reports success. A guard that silently no-ops is worse than no guard, because it is trusted.
What changed
Enumeration moves into a reusable library,
bin/fm-process-tree-lib.sh:/procis the sole authority. Nolsofpath and no second enumerator — one that cannot prove coverage is the same silent-degradation bug in another costume.psis gone from enumeration entirely; ppid, run state, uid and birth identity all come from the same/procrecord, so they can never describe different processes.PPID==1undercounts badly: an orphan's children reparent to it and read as healthy. During the incident, six apparent orphans were the visible tip of nineteen processes.pkill, nokillall, no ps-grep-xargs: sibling worktree processes carry identical command lines, and one careless pattern previously killed four unrelated workers.Coverage and ownership are separated deliberately
Conflating them makes the guard useless in one direction or the other:
This distinction is not academic. An earlier revision refused on any unreadable cwd, which meant it refused on every ordinary Linux host, forever —
systemd --user,(sd-pam),sshd-sessionandsftp-serverall hide their cwd. A teardown that never cleans anything is strictly worse than the bug it guards against. Claim what you can observe, disclose what you cannot, refuse only on evidence.Known limitations, stated rather than hidden
hidepid=1/hidepid=2, foreign/procentries are restricted or invisible, so traversal through a foreign intermediary can undercount our own descendants, and userspace cannot detect that invisibility.The durable fix for both is a cgroup or session ownership boundary, which belongs to the follow-up resource guard, not here.
Scope
This is piece 1 of 3, split deliberately. Not included, and coming as separate PRs: the freeze-and-grace signalling sequence, and the
/tmpscratch attribution.How this was validated — please read before merging
The validation pipeline ran and passed intent, rebase, review (no findings remaining) and test, and produced its documentation step. It was then killed by the OOM killer before it could finish, as it was on all six attempts: its peak is 3.6–4.0 GB against 3–4 GB available on this machine, including one attempt with the machine entirely to itself.
So the following were completed by hand, not by the pipeline:
shellcheckon the changed files (library clean under full extended analysis;fm-teardown.shand the test file clean with extended analysis disabled, because full analysis on that file exceeds this machine's memory — it does so onmain's copy too, so CI will cover it)This is not "pipeline-validated" end to end. Review and tests passed inside the pipeline; lint, push and PR were manual. Given that this change exists because a guard reported success it had not earned, overstating its validation would be the same defect in our own colours.
Local results:
fm-teardown68 passed / 0 failed,fm-gate-refuse7/0,fm-backlog-atomicity78/0.One pre-existing failure on
mainis unrelated and untouched here:fm-watch-triage.test.sh— "the fixture captured no process-event result". This branch modifies neitherbin/fm-watch.shnor that test; it reproduces onmainand appears to be what the unmerged #2877 addresses.Testing notes
Covered: whole-tree reaping; the outage regression (parent exits first, reparenting verified, child asserted reaped); recycled PID not adopted; exec change preserving identity; foreign-uid cwd root skipped; foreign-uid intermediary traversed but never signaled; unreadable cwd disclosed rather than refused; refusal when a live identity cannot be read; refusal on an empty
/procsnapshot; mid-scan churn converging instead of refusing.One case deliberately runs against the real
/procand real process table of the running machine. Every other case builds a synthetic/proccontaining no nondumpable processes — and a suite fully green against that synthetic world once hid a revision that refused on every real host. That test was verified non-vacuous by restoring the bug and confirming it fails.Tests clean up unconditionally on every failure path; a test for a process-leak fix must not leak processes.