Skip to content

fix(gc): freeze a fork chain behind its zombies before gc reads it ended - #326

Merged
ThomasK33 merged 4 commits into
mainfrom
fix/gc-freeze-unseen-group
Nov 4, 2026
Merged

ThomasK33 merged 4 commits into
mainfrom
fix/gc-freeze-unseen-group

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

Why

Codex on #302 (round 6, thread 4464932183) found a fork-chain case that gc misses. A PID 1 that never reaps leaves the zombies of a fast fork chain in view, while the chain's live process escapes both /proc scans. Then kill(-pgid, 0) finds the id, but the scans show only zombies. So gc read the group as ended and removed program.json while the chain ran. After that, no destroy could find the chain. Main removes every such session, and #302 did too for this case. This PR closes it.

Follow-up of #302.

What changes for users

  • When gc's scans show only zombies of a session's program group while the group id still names a process, gc sends the group SIGSTOP. Then it checks the group again and sends SIGCONT in a finally. A stopped process stays in view, so the check finds the chain, and gc keeps the session with "the session's program group still runs (destroy ends it)".
  • gc sends no SIGSTOP when /proc does not show its own pid space. That is the same condition that stopProgramGroup uses.
  • gc still kills nothing. USAGE limits 3 and 5 name the two signals.

No JSON shape changes.

What changes for maintainers

  • judgeGroupForGc calls the new judgeFrozenGroup when the probe finds the id and both scans show only zombies. judgeFrozenGroup uses the freeze, judge and SIGCONT-in-finally steps of stopProgramGroup. A frozen group that reads owned keeps the session, and unproven reads ended. gone and unknown are inconclusive, so gc freezes and judges again, up to GC_JUDGEMENTS (10) times. SIGSTOP does not nest: another gc or a destroy can send SIGCONT before this judgement (Codex round 1). A group that has ended reads gone every time, so after ten judgements it reads ended.
  • The model gets the guard GcFreezesUnseenGroup and the environment constant ZombiesInView. With zombies in view, the probe alone proves nothing, and gc sees the chain only through the freeze. mutant-GcFreezesUnseenGroup-unseen-zombies must violate R20 (RemovedSessionStopped). With every guard TRUE, ZombiesInView does not change GcSeesGroup, so current-unseen covers the pass run. That makes 79 runs and thirty-six guards.
  • facts.mjs reads the new guard from judgeGroupForGc and judgeFrozenGroup, and hashes judgeFrozenGroup. ProvesGroupOwnership now allows judgeFrozenGroup as a second caller of signalProcessGroup, which sends only SIGSTOP or SIGCONT. The raw group kill stays inside stopProgramGroup.

Validation

  • Red first, on fix(gc): keep a session whose program group still runs, and model-check the lifecycle in CI #302's head f263f56:
    • Unit tests in programGroup.test.ts: "freezes a group whose scans show only zombies…" (two cases: the frozen group shows a live member, or only zombies) and "gives a frozen group SIGCONT when its judgement throws". All three fail.
    • Integration test in non-reaping-init.test.ts: "gc keeps an exited session while a fork chain runs behind its zombies". It runs the fork chain under non-reaping-subreaper.py and waits for 200 zombies. It failed 2 of 2 runs (gc removed the session). With the fix it passed 3 of 3 runs, together with the other three tests in that file.
  • Red first for the SIGCONT race, on 0ea48c8: "freezes the group again when the frozen group still shows only zombies" and "reads a group that shows only zombies, also frozen, ended after ten judgements".
  • Fact mutations: without the freeze call, without the SIGCONT, without the pid-space check, with unknown read as running, or with a frozen gone read as ended, GcFreezesUnseenGroup reads FALSE. A kill added inside the freeze also turns ProvesGroupOwnership FALSE.
  • check.sh --update-coverage at 1c1bd1c6: 79/79 runs as expected in 1,121 s. current has 29,309,460 distinct states and current-no-token has 29,186,546 (unchanged). The new mutant violates R20.
  • CI on ace0873 (run 41808311495): 21 checks green. The four lifecycle-model shares took 24.5, 10.8, 20.3 and 17.3 min of TLC, and 20 + 20 + 20 + 19 = 79 runs gave the expected results. The SIGCONT fix changes no model input, so ace0873 writes the record from facts.mjs.
  • After the rebase onto main e6b1ebd: check.sh --drift-only, tsc --noEmit, oxlint, oxfmt --check and the unit tests of programGroup, gc and lifecycle (252 tests) pass. The unit suite passes (143 files, 2,019 tests).
  • Two integration timing checks failed locally under load, and neither comes from this change. gc collects exited, failed, and destroyed sessions needs 29.1 s on main against its 30 s limit, and it timed out here at load averages of 26 to 42. gc itself takes 2 to 3 s on both trees for those three sessions. In destroy ends a program whose killed background process stays a zombie, the 2 s list check read 2,471 ms at load 59, with the new test filtered out.

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

… it ended

Codex on #302: under a PID 1 that never reaps, a fork chain leaves its
zombies in view while its live member escapes both scans, so gc read
the group ended and removed program.json while the chain ran.
judgeFrozenGroup now sends SIGSTOP, as stopProgramGroup does, judges the
stopped group, and sends SIGCONT in a finally. The integration test runs
the chain under the subreaper that never reaps.
The model gets the guard GcFreezesUnseenGroup and the environment
constant ZombiesInView. With zombies in view the probe alone proves
nothing, and gc sees the chain only through the freeze.
mutant-GcFreezesUnseenGroup-unseen-zombies must violate R20. With every
guard, ZombiesInView changes nothing, so current-unseen covers the pass
run. ProvesGroupOwnership lets judgeFrozenGroup send SIGSTOP and SIGCONT,
and the raw kill stays in stopProgramGroup. USAGE limits 3 and 5 name the
two signals.
check.sh --update-coverage at 1c1bd1c6 gave 79/79 runs as expected in
1,121 s. current has 29,309,460 distinct states and current-no-token
29,186,546.
@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 4, 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-04T09:34:45.310322Z ace0873 Manual request
🔒 Security Review ✅ Completed 2026-11-04T09:35:27.141846Z ace0873 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 Security Review

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

Reviewed commit: 0ea48c887e

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0ea48c887e

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

Comment thread src/host/programGroup.ts Resolved
Codex on #326: SIGSTOP does not nest, so another gc or a destroy can send
SIGCONT before this gc judges the frozen group, and a fork chain then
read gone and lost program.json. judgeFrozenGroup now reads a frozen
group that still reads gone as inconclusive, so provenProgramGroupRuns
freezes it again, and only GC_JUDGEMENTS such judgements read it ended.
The model has one gc and does not change, so the record comes from
facts.mjs at this commit.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: ace08738b6

ℹ️ 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: ace08738b6

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 4, 2026
ThomasK33 added a commit that referenced this pull request Nov 4, 2026
…ded in one pass

Advisor on #326: a zombie-only ended session under a PID 1 that never
reaps took ten passes of six /proc scans and ten SIGSTOP/SIGCONT pairs:
about 2.5 s with 2,264 processes in /proc. judgeFrozenGroup now freezes
and judges at most twice (FROZEN_JUDGEMENTS). The second freeze covers
a SIGCONT from another gc or a destroy. When that judgement reads gone
too, nothing live was in view to freeze, and the group reads ended: about
300 ms there, and 50 ms with 258 processes.

Also from the advisor: unit tests for a frozen group whose live member is
in another autogroup or session (ended, and resumed) and for a /proc/self
of another pid (no SIGSTOP), and USAGE limit 5 says that gc keeps the
session only when the frozen check proves the group.
Merged via the queue into main with commit 6b7b9dd Nov 4, 2026
22 checks passed
@github-actions github-actions Bot mentioned this pull request Nov 4, 2026
ThomasK33 added a commit that referenced this pull request Nov 4, 2026
Codex round 3 on #327 (P1) and the maintainer: reading a group ended after
two frozen judgements that read gone was a regression from #326, which
retried up to GC_JUDGEMENTS times. A SIGCONT from another gc or a destroy
before both judgements let gc remove the folder of a live fork chain.

judgeFrozenGroup now returns null (inconclusive) after both frozen
judgements read gone, so provenProgramGroupRuns judges the group again. It
keeps the two freezes per judgement, so a gc sends at most 10 x 2
SIGSTOP/SIGCONT pairs. An ended zombie-only group with about 2,300
processes in /proc now takes about 3.5 s at a load of 90, against about
2.5 s for #326's ten single freezes.

New unit test: a live member that escapes both frozen judgements of one pass
keeps the session (fails with the old return). facts.mjs expects the null
return, and a false return turns GcFreezesUnseenGroup FALSE. USAGE limit 5
and SKILL state the cost and the remaining race (all 20 frozen judgements).
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