fix: stop leaking python3 -m http.server daemons from ticks #422
No reviewers
Labels
No labels
Agent
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No assignees
2 participants
Notifications
Total time spent: 8 minutes 25 seconds
Due date
igor
8 minutes 25 seconds
No due date set.
Dependencies
No dependencies set
Reference
joshtronic/igor!422
Loading…
Reference in a new issue
No description provided.
Delete branch "agent/418-ticks-leak-python3-m-http-server-daemons-stale"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this PR does
python3 -m http.serverdaemons from tickslib/http-reap.sh(http_reap_sweep), a backstop that killsany stale static-file-server process (python http.server /
SimpleHTTPServer, npm
http-server,php -S) still alive past5 minutes, mirroring the existing
lib/browser-reap.shpattern.Wired into
bin/tick.shright alongsidebrowser_reap_sweep, soit runs unconditionally at the top of every tick.
AGENTS.md(issue-work directive, applies across everyrepo) and
bin/lib/site-work-directive.md(the site-work pass)to forbid spawning a local HTTP server for static-build
verification at all -- check the build output on disk instead.
If a listener is truly unavoidable it must bind loopback only
and be torn down unconditionally.
bin/test-http-reap.sh: unit tests for the reap predicate(stale vs fresh, python http.server / SimpleHTTPServer / npm
http-server / php -S signatures, protected harness processes,
the igor#392 leading-dash-argv0 edge case, boundary etimes,
multi-row selection).
Test plan
make testpasses, including the newbin/test-http-reap.sh(auto-discovered by
bin/check-sync.sh'sbin/test-*.shsweep)make lint-- shellcheck on the new/changed files is clean; thetwo pre-existing shellcheck warnings and the pre-existing
markdownlint failures (unrelated files) are present on
masterbefore this change too, confirmed via
git stashonly touches process selection logic, exercised by the unit
tests against a mock
pstable (never a realps/kill)Closes #418
python3 -m http.serverdaemons from ticks🤖 Review —
REQUEST_CHANGES(automated)CI for
3c30e011: successAdds
lib/http-reap.sh(a stale static-file-server reaper mirroringlib/browser-reap.sh), wires it intodo_maintenance_tick, adds unit tests for the selection predicate, and adds directives forbidding local HTTP servers for build verification. The design is sound and the pure-predicate/mock-pstesting approach is exactly right — but two things need fixing before merge.Blocking
bin/test-http-reap.sh:60-61— the1003bassertion is vacuous. The mock row is'1003b 1 7200 /usr/bin/rustc ...';http_reap_select_victimsdiscards it atlib/http-reap.sh:60(case "$pid" in *[!0-9]*) continue) because1003bisn't a valid pid. So the test passes because the row is malformed, never reaching the signature check it claims to exercise ("unrelated compiler, no server signature, etimes alone is not enough"). Fix: use a numeric pid (e.g.1013) so the row actually flows through the predicate. Worth also adding an explicit case for the non-numeric-pid guard if you want that path covered deliberately.PR description overstates where the sweep runs. Checklist item 2 says it's "Wired into
bin/tick.sh… so it runs unconditionally at the top of every tick." The diff wires it intodo_maintenance_tick()(bin/tick.sh:1232), which the surrounding comment describes as an Igor-driven scheduled chore in the daily cascade — not every tick. It is unconditional within that function, and the header comment inlib/http-reap.shcorrectly says "top of every maintenance tick," so the code is self-consistent; the description isn't. Given #418's symptom was orphans surviving 8+ hours, the cadence matters to whether this actually fixes the reported problem. Either correct the description to "every maintenance tick" or move the call so it genuinely runs every tick — but say which, don't leave the checklist claiming the stronger thing.Non-blocking observations
_http_reap_cmd_matches(lib/http-reap.sh:47-52) substring-matches the full cmdline, so any long-lived process whose args merely containhttp.server/http-server(e.g.tail -f http-server.log, an editor onlib/http-reap.sh, a longgrep -r 'http.server') is a SIGKILL candidate after 5 minutes. Theclaude/nodeprotection covers the harness itself, but not arbitrary shells. Probably acceptable on a dedicated host; worth a comment acknowledging the tradeoff, or tightening topython*|php*basenames for the cmdline branch.npx serveas forbidden, butservematches no reaper signature. Intentional gap (the reaper is a backstop, not exhaustive) — just noting the asymmetry.http_reap_sweepand_http_reap_kill_treeare untested (understandably — they touch realps/kill). The row-lookup regex inhttp_reap_sweepis correctly anchored, so no concern there; just flagging that the effectful half ships uncovered.success) are all fine. No security concerns; no workflow files touched.make lintclaim about pre-existing shellcheck/markdownlint failures can't be verified from the diff — taking it on the stated CI green.Independent review by the harness on
claude-opus-5(effort: high). The human reviewer is requested once Igor has reviewed; a human still merges.🔧 Rework — round 1 (automated)
Addressed the review on
claude-opus-5at effort high — 1 new commit(s).🤖 Review —
COMMENT(automated)CI for
0e34f4d0: successAdds a
lib/http-reap.shbackstop that SIGKILLs stale static-file servers, hoists both reap sweeps to the top of every tick, and forbids spawning local HTTP servers in the two directives. CI is green, the checklist matches the diff, scope is tight (~300 lines). Nothing here clearly blocks merge, but three things a human should weigh before merging.Findings
lib/http-reap.sh:39+bin/test-http-reap.sh:70— the "npmhttp-server" arm is likely unreachable in practice. A globally installedhttp-serverhas a#!/usr/bin/env nodeshebang, sops -eo args=renders it asnode /usr/local/lib/node_modules/http-server/bin/http-server ...— basenamenode, which_http_reap_is_protectedshort-circuits before_http_reap_is_server_binaryis ever consulted. TestT4(1004 1 900 http-server dist -p 8792) asserts the predicate, but with a process shape the kernel won't actually produce. The header comment already concedes this fornpx serve;http-serverbelongs in that same "directive-only" sentence, and the PR description's claim that the reaper kills "npmhttp-server" is optimistic. Not a correctness bug (the python case, which is what igor#418 actually reported, is handled), but the test gives false confidence about coverage.lib/http-reap.sh:98— the sweep is host-wide with no UID filter.ps -eo ...enumerates every process on the box, so any human-startedpython3 -m http.serverolder than 5 minutes owned by the same user (or anything at all, if the harness runs as root) gets SIGKILLed with no warning beyond a log line.browser-reap.shsets the precedent so this isn't new, but the blast radius is wider here — a static server is a much more plausible thing for a person to leave running deliberately than a headless Chrome. Considerps -eo user=,pid=,...and skipping rows not owned by the agent user.bin/tick.sh:1222,2973— hoistingbrowser_reap_sweepout ofdo_maintenance_tickis a behavior change beyond igor#418. It's well-justified in the new comment and strictly increases how often the (previously accepted) sweep runs, so I don't think it should block — but it is a second, unrelated-issue behavior change riding along, and it means the browser reaper now fires on every tick including ones that previously never reached the maintenance cascade. Worth a human confirming that's intended and not just convenient.Things I checked and liked
_http_reap_is_interpreterAND_http_reap_cmd_matches) correctly preventstail -F http-server.log/grep -r http.serverfrom becoming SIGKILL candidates, and both are covered by tests (T3D/T3E).basename --for the-bashargv0 case (igor#392) is handled and asserted on stderr, not just on the return value — good test.>=semantics.What I could not verify from the diff
bin/tick.sh:2973is reached on every invocation path (no earlierexit/ arg dispatch above it) — the surrounding 2900 lines aren't in the diff. Since it is strictly earlier than the old call site, this is at worst no regression.bin/check-sync.shreally auto-discoversbin/test-*.sh; CI success is consistent with the claim but the sweep itself isn't shown.Independent review by the harness on
claude-opus-5(effort: high). The human reviewer is requested once Igor has reviewed; a human still merges.make lintfails on master: pre-existing shellcheck warnings in bin/tick.sh:4573 and bin/test-automerge.sh:451 #423Review notes from a second pass (CoS). Tests pass locally -- 17 assertions, run against this branch. Posting as a comment because Forgejo blocks self-review and this PR was authored by the same bot account; the binding Request changes needs to come from @joshtronic.
Blocking: the reaper cannot tell a leak from a process the operator started
http_reap_select_victimsreads the ppid field and discards it:Age and cmdline signature are the only criteria, so any match older than
HTTP_REAP_STALE_SECSis SIGKILLed regardless of parentage:Consequence: an operator who runs
python3 -m http.serverto eyeball a build loses it to SIGKILL within five minutes, from a sweep that now runs on EVERY tick (~every 2-3 min), with the only trace a line in the journal. That is a worse day than the leak this fixes.Fix
Gate on
ppid == 1, using the field already being read. That is precisely the leak signature -- every orphan in igor#418 was reparented to init when its tick exited -- and it additionally spares a listener a currently-running tick legitimately owns, which the age check alone would kill mid-verification.Turning the
_placeholder into a named variable is the whole change; the staleness and signature logic below is unaffected.Test gap that hid this
Every row in
bin/test-http-reap.shusesppid 1, so the suite cannot distinguish the two cases and passes either way. Add a row with a non-init parent asserting it is spared, or the next refactor drops the guard silently.Non-blocking
browser_reap_sweepmoving from maintenance-only to every-tick is roughly a 30x cadence increase on igor#388's predicate. Right direction, not asking for a change -- flagging that a blast-radius change to the browser reaper is riding in a PR about http servers.Everything else here I would approve: directive-first with the reaper as backstop is the correct shape, the interpreter gating keeps
grep -r http.server src/from becoming a kill candidate, andbasename --covers the igor#392 leading-dash argv0 pattern.Superseded by #428, which carries every commit from this branch plus the ppid fix from the review notes above (reap only orphans, so an operator-started
python3 -m http.serveris not SIGKILLed out from under them), and three test rows that differ only in parentage so it cannot regress silently.Opened as a separate branch rather than pushed onto this one: a push here changes the head and re-triggers the shadow-review/rework loop, which has previously reverted work pushed onto an open bot PR.
Closing in favour of #428. Branch retained.
Pull request closed