Repository navigation
fix(destroy): probe a group that no scan shows before the wait reads it ended - #334
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
|
@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. Delightful! 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
#333 makes
stopProgramGroupfreeze a fork chain whose member ends between two reads. A freeze can still miss a chain: when another process sends SIGCONT before each of its 5 judgements, or when a SIGSTOP does not take effect within them on a loaded machine. After a miss, the wait read the running chain as ended, anddestroyrecordeddestroyedwhile the chain ran. The cause was inisProcessGroupAlive: when its probekill(-pgid, 0)found the group but no scan showed a member, live or zombie, it took the processes for ones reaped before the scans and read the group ended (?? false).What changes for users
destroynow fails withHOST_TIMEOUTand leaves the session as it was. It no longer reportsdestroyedwhile the chain runs.destroythat reports success. USAGE item 3 says so.kill -STOP, or Ctrl-Z in a shell) gets no SIGSTOP fromdestroybefore its first freeze. So the SIGCONT after the judgements cannot resume that process beside a running one. Before, a group with one such stopped member and one running member without an autogroup got SIGSTOP and then SIGCONT, which resumed the stopped member (advisor follow-up on fix(destroy): freeze a group whose member ends between two reads before destroy gives up on it #333).No CLI JSON or exit codes change.
What changes for maintainers
isProcessGroupAlive:return groupHasLiveProcess(pgid) ?? probeFindsGroup(pgid);. A second probe after the scans decides. Processes reaped meanwhile are gone (ESRCH), and the group reads ended. A fork chain still holds the id, and the group reads alive.stopProgramGroup:if (!frozen && showsStoppedMember(group.pid)) { return; }before the first SIGSTOP.showsStoppedMemberscans the group for a member in stateTort.judgeFrozenGroup(gc) waitsSTOP_SETTLE_MS(1 ms) after its SIGSTOP, inside thetry, asstopProgramGroupdoes since fix(destroy): freeze a group whose member ends between two reads before destroy gives up on it #333. An ended group costs 20 more ms on top of its ten passes.src/host/README.mdnames the freeze of a member that ends between two reads, the stopped-member rule, and the wait after each SIGSTOP. The long comment line instopProgramGroupis rewrapped.WaitProbesUnseenGroup, and the environment constantFreezeMisses: withUnseen, every freeze misses the chain.ProbeFindsUnseenis the second probe:WaitProbesUnseenGroupwithoutZombiesInView.GroupSeenreads a group with a token alive underUnseenthrough it, andProbeSees(a group without a token) now reads the program's processes underUnseenonly through it, as the code does.GroupJudgedisGroupSeenwithout that probe, and gc reads it with its own probe (GcSeesGroup).ProvenKillgets no kill underUnseenwithout the freeze or withFreezeMisses.current-unseen-misses(pass, 68,556,464 distinct states) andmutant-WaitProbesUnseenGroup-unseen-misses(violates R11).mutant-FreezesUnseenGroup-unseenbecomesmutant-UnseenBothGuards-unseen(both guards off, violates R11): either guard alone keeps R11, likemutant-GcBothChecks. That makes 92 runs and forty guards. Fourteen runs explore more than 20 million distinct states.Unseen, the changed operators equal the old ones (ProbeSees,GroupSeen=GroupJudged,ProvenKill), so every run withoutUnseenexplores the same states as before.facts.mjs:waitProbeFactsreadsWaitProbesUnseenGroupfromisProcessGroupAliveandprobeFindsGroup.freezeFactsreads the stopped-member check, andfrozenJudgesTwicereads gc's wait.Validation
['ok', [], true]in "on linux, when the probe ends ... and the group shows ..., is alive"current-unseen(64,052,288 states) andcurrent-unseen-missespass.mutant-UnseenBothGuards-unseenandmutant-WaitProbesUnseenGroup-unseen-missesviolateDestroyedProgramStopped.mutant-GcProbesUnseenGroup-unseenandmutant-GcFreezesUnseenGroup-unseen-zombiesstill violateRemovedSessionStopped.stopProgramGroupskipped, which forces a miss: fix(destroy): freeze a group whose member ends between two reads before destroy gives up on it #333 alone recordeddestroyedwhile the chain ran in 3 of 3 runs. This PR givesHOST_TIMEOUTin 3 of 3 runs, the session staysexited, and the chain runs as before.destroyed. 14 runs hitunknown, and each got SIGSTOP and then SIGKILL.check.sh --drift-onlyandtest/unit(2,073 tests) pass.Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:xhigh