Skip to content

test(model): check the freeze of a fork chain behind its zombies - #335

Merged
ThomasK33 merged 12 commits into
mainfrom
test/model-freeze-unseen-zombies
Nov 5, 2026
Merged

ThomasK33 merged 12 commits into
mainfrom
test/model-freeze-unseen-zombies

Conversation

@ThomasK33

Copy link
Copy Markdown
Member

Why

The clean-context advisor on #334 found two gaps.

  1. Model coverage: fix(destroy): probe a group that no scan shows before the wait reads it ended #334 replaced mutant-FreezesUnseenGroup-unseen with mutant-UnseenBothGuards-unseen, because without zombies in view the wait's new probe also keeps R11. With ZombiesInView the probe does not help: scans that show only zombies read the group ended whatever the probe finds (ProbeFindsUnseen == WaitProbesUnseenGroup /\ ~ZombiesInView). So there only FreezesUnseenGroup keeps destroy from recording destroyed while a fork chain runs, and no run checked that.
  2. Docs: fix(destroy): probe a group that no scan shows before the wait reads it ended #334 adds a rare false HOST_TIMEOUT that USAGE did not name.

What changes for users

  • docs/USAGE.md, limit 2 (sessions without an autogroup record): where /proc belongs to another pid namespace, as after unshare --pid --fork without --mount-proc, and that namespace's PID 1 never reaps, destroy of such a session fails with HOST_TIMEOUT after the program's group has ended. No scan of that /proc shows the group's zombies, and kill(-pgid, 0) still finds them. Before fix(destroy): probe a group that no scan shows before the wait reads it ended #334, destroy recorded destroyed there.

No code, CLI JSON or exit codes change.

What changes for maintainers

  • check.sh: two runs after ZOMBIES=TRUE:
    • current-unseen-zombies: every rule, expects pass.
    • mutant-FreezesUnseenGroup-unseen-zombies: FreezesUnseenGroup=FALSE, must violate R11 (DestroyedProgramStopped).
    • The comment that said ZombiesInView "changes nothing, so current-unseen covers it" now says that only the freeze keeps R11 there.
  • CI: 9 lifecycle-model shares 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, so CHECK_JOBS=12 stays. No ruleset requires a lifecycle-model check by name.
  • SKILL.md: 94 runs, fifteen runs above 20 million distinct states, CHECK_JOBS=94, nine jobs with CHECK_SHARD=<i>/9, two new rows in the results table, and one sentence in the FreezesUnseenGroup section.
  • coverage.json: resynced. Only the fingerprint of check.sh changes. All 42 constants stay the same.

Validation


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: xhigh

…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
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-05T03:07:55.081957Z cb946aa Manual request
🔒 Security Review ✅ Completed 2026-11-05T03:08:41.038454Z cb946aa Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: cb946aa236

ℹ️ 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".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: cb946aa236

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@ThomasK33
ThomasK33 added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 12b83dd Nov 5, 2026
26 checks passed
@ThomasK33
ThomasK33 deleted the test/model-freeze-unseen-zombies branch November 5, 2026 04:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant