Repository navigation
test(model): check the freeze of a fork chain behind its zombies - #335
Conversation
…re destroy gives up on it stopProgramGroup sent SIGSTOP only after a judgement of gone. A member of a fork chain that a scan shows can end before its autogroup read, so the judgement reads unknown, and stopProgramGroup returned with no freeze and no kill. A witness that ended before its read again made the loop judge again without a freeze. The wait after it then read the unseen chain ended, and destroy recorded destroyed while the chain ran. CI of #331 hit this in "gc keeps an exited session while a fork chain that the program left runs". Now every judgement that gets no kill and is not unproven sends SIGSTOP and judges the frozen group again: gone as before, unknown, and owned with a witness that ended. A stopped member cannot end, so the next judgement reads its autogroup and the witness, and the group gets the kill. The loop keeps its 5 attempts, and a frozen group that gets no kill still gets SIGCONT in the finally block. New unit tests: a member that ends before its autogroup read, and a witness that ends before its read again, both get SIGSTOP and then SIGKILL (both fail on main). A member whose autogroup /proc refuses now gets 5 SIGSTOP and a SIGCONT, and still no kill. facts.mjs reads the new loop. The model is unchanged: it already assumes that the freeze brings the witness into view.
…group Codex round 1 on #333: a group that job control stopped, whose autogroup or boot id /proc refuses, reads unknown at every judgement. The new freeze sent it SIGSTOP and then SIGCONT, which resumed a program that its user stopped. The unknown verdict now says whether a member without an autogroup still runs. A stopped member cannot end between two reads, so /proc refused its autogroup, and stopProgramGroup sends no signal then. A refused boot id is unknown without a running member too, because no later judgement can prove the group without it. A running member without an autogroup still gets the freeze, once: its stopped member reads the same way, and the group gets SIGCONT. New unit test: a stopped group whose member shows no autogroup gets no signal (fails on the previous head).
…readable-member # Conflicts: # .agents/skills/verify-session-lifecycle/coverage.json
…obe finds its id When the probe kill(-pgid, 0) found the group but no scan showed a member, live or zombie, isProcessGroupAlive read the group ended: it took the processes for ones reaped before the scans. A fork chain that no scan shows reads the same way, so a freeze that missed the chain let the wait after it read the running chain ended, and destroy recorded destroyed. Now a second probe after the scans decides: reaped processes are gone, and a chain still holds the id, so the wait times out with HOST_TIMEOUT instead. Scans that show only zombies still read the group ended, because under a PID 1 that never reaps the zombies of a killed group stay. Model: the guard WaitProbesUnseenGroup and the environment constant FreezeMisses (every freeze of an Unseen group misses, as when a SIGCONT from another process comes before each judgement). ProbeSees now reads the program's processes under Unseen only through that probe, as the code does. GroupJudged keeps gc on its own probe. New runs: current-unseen-misses (pass, 68,556,464 distinct states) and mutant-WaitProbesUnseenGroup-unseen-misses (violates R11). mutant-FreezesUnseenGroup-unseen becomes mutant-UnseenBothGuards-unseen: either guard alone keeps R11. That makes 92 runs and forty guards. Also from the advisor on #333: - stopProgramGroup sends no SIGSTOP before its first freeze to a group that shows a member that job control or a tracer stopped, so the SIGCONT after the judgements cannot resume that member beside a running one. - src/host/README.md names the freeze of a member that ends between two reads, and the comment of stopProgramGroup is rewrapped.
…readable-member # Conflicts: # .agents/skills/verify-session-lifecycle/coverage.json
…the group The merge-group CI of #333 failed "gets no kill and reads alive when the autogroup of its member names no autogroup": the real group got two SIGSTOPs before its SIGCONT. A SIGSTOP takes effect only when the kernel next runs the member. A read of /proc/<pid>/stat right after kill() showed the member not yet stopped in 1,984 of 2,000 tries here, and the stop took about 30 us (max 405 us) on an idle machine. A full /proc scan usually gives it that time, but on a loaded runner the next judgement read the member running and froze the group again. The code is safe either way: a member that still runs gets another SIGSTOP, within the 5 judgements, and the group still gets its SIGCONT. stopProgramGroup now waits STOP_SETTLE_MS (1 ms) after each SIGSTOP, so the next judgement mostly reads the frozen members stopped and fewer of the 5 judgements go to a stop that has not taken effect. On one CPU shared with four busy loops, the test saw a second SIGSTOP in 6 of 24 runs without the wait and in 1 of 24 with it. The test now accepts 1 to 5 SIGSTOPs before the SIGCONT. facts.mjs reads the wait as part of the freeze.
…wait-probes-unseen-group # Conflicts: # .agents/skills/verify-session-lifecycle/SKILL.md # .agents/skills/verify-session-lifecycle/coverage.json # src/host/README.md
… scan The same asynchronous stop as in stopProgramGroup: judgeFrozenGroup judged the group right after its SIGSTOP, so on a loaded machine the scan could read the chain still forking, and the frozen judgement read gone. It now waits STOP_SETTLE_MS (1 ms) inside the try, before the judgement, so the SIGCONT in the finally block still follows every SIGSTOP. An ended group costs 20 such waits, 20 ms, on top of its ten passes.
…es-unseen-group # Conflicts: # .agents/skills/verify-session-lifecycle/SKILL.md # .agents/skills/verify-session-lifecycle/coverage.json # .agents/skills/verify-session-lifecycle/scripts/facts.mjs # docs/USAGE.md # src/host/programGroup.ts # test/unit/host/programGroup.test.ts
With ZombiesInView, scans that show only zombies read the group ended whatever the wait's probe finds, so only FreezesUnseenGroup keeps destroy from recording destroyed while the chain runs. No run checked that since #334 replaced mutant-FreezesUnseenGroup-unseen with a mutant that drops both guards. - check.sh: add current-unseen-zombies (pass) and mutant-FreezesUnseenGroup-unseen-zombies (violates R11, DestroyedProgramStopped), and correct the comment that said ZombiesInView changes nothing. - CI: 9 lifecycle-model shares instead of 8. With 17 runs that expect pass, 8 shares would give one share 3 of them (current, current-unseen and current-win32) with 1 TLC worker each. - SKILL.md: 94 runs, fifteen runs above 20 million states, CHECK_JOBS=94, nine jobs, the two new rows, and the FreezesUnseenGroup section. - USAGE.md limit 2: under a /proc of another pid namespace and a PID 1 that never reaps, destroy of a session without an autogroup record fails with HOST_TIMEOUT after the group ended. - coverage.json: resynced (check.sh fingerprint only).
…een-zombies # Conflicts: # .agents/skills/verify-session-lifecycle/SKILL.md # .agents/skills/verify-session-lifecycle/coverage.json # docs/USAGE.md
|
@codex review |
|
@codex security review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. 🚀 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
🛡️ Codex Security ReviewSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
Why
The clean-context advisor on #334 found two gaps.
mutant-FreezesUnseenGroup-unseenwithmutant-UnseenBothGuards-unseen, because without zombies in view the wait's new probe also keeps R11. WithZombiesInViewthe probe does not help: scans that show only zombies read the group ended whatever the probe finds (ProbeFindsUnseen == WaitProbesUnseenGroup /\ ~ZombiesInView). So there onlyFreezesUnseenGroupkeepsdestroyfrom recordingdestroyedwhile a fork chain runs, and no run checked that.HOST_TIMEOUTthat USAGE did not name.What changes for users
docs/USAGE.md, limit 2 (sessions without an autogroup record): where/procbelongs to another pid namespace, as afterunshare --pid --forkwithout--mount-proc, and that namespace's PID 1 never reaps,destroyof such a session fails withHOST_TIMEOUTafter the program's group has ended. No scan of that/procshows the group's zombies, andkill(-pgid, 0)still finds them. Before fix(destroy): probe a group that no scan shows before the wait reads it ended #334,destroyrecordeddestroyedthere.No code, CLI JSON or exit codes change.
What changes for maintainers
check.sh: two runs afterZOMBIES=TRUE:current-unseen-zombies: every rule, expects pass.mutant-FreezesUnseenGroup-unseen-zombies:FreezesUnseenGroup=FALSE, must violate R11 (DestroyedProgramStopped).ZombiesInView"changes nothing, so current-unseen covers it" now says that only the freeze keeps R11 there.lifecycle-modelshares instead of 8. The new run makes 17 runs that expect pass, and check.sh deals them round-robin. With 8 shares, share 1 would get 3 of them (current,current-unseen,current-win32: 64M, 64M and 53M states) and 13 runs in total, so each pass run would get 1 TLC worker on the 4 vCPUs. Share 1 takes 33 min today with 2 workers per run, and the job timeout is 55 min. With 9 shares, a share gets 9 to 11 runs and at most 2 pass runs, soCHECK_JOBS=12stays. No ruleset requires alifecycle-modelcheck by name.SKILL.md: 94 runs, fifteen runs above 20 million distinct states,CHECK_JOBS=94, nine jobs withCHECK_SHARD=<i>/9, two new rows in the results table, and one sentence in theFreezesUnseenGroupsection.coverage.json: resynced. Only the fingerprint ofcheck.shchanges. All 42 constants stay the same.Validation
mutant-FreezesUnseenGroup-unseen-zombiesviolatesDestroyedProgramStoppedafter 9,776 distinct states (a 12-state trace that ends with the host writingdestroyedwhile the chain runs).current-unseen-zombiespasses with 64,052,288 distinct states.sudo unshare --pid --forkwithout--mount-proc, with a Python PID 1 that waits only for its own child, a sessionsh -c 'sleep 3007 & exec sleep 3011'without an autogroup record, and a plaindestroy:destroyed: trueafter 957 ms. Only the zombie ofsleep 3007was left.HOST_TIMEOUTafter 5.8 s, with the same zombie left.check.sh --drift-only, oxfmt, actionlint and zizmor pass. shellcheck reports the same 4 warnings oncheck.shas before.Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:xhigh