Skip to content

feat(teardown): P3 - exec detach + clone/exec admission - #587

Merged
ryanbreen merged 28 commits into
mainfrom
feat/teardown-p3-exec-detach
Aug 17, 2026
Merged

feat(teardown): P3 - exec detach + clone/exec admission#587
ryanbreen merged 28 commits into
mainfrom
feat/teardown-p3-exec-detach

Conversation

@ryanbreen

Copy link
Copy Markdown
Owner

Revert story (one story, one PR)

Quoted verbatim from docs/planning/teardown-unification/PLAN.md (Phase 3, "Revert — one story, one PR"):

Revert — one story, one PR (rule 5; artifact §5.2 T2-b). Delete the two = None assignments
at the exec commit points and, in the same revert, the parent-Live admission check with its
non-runnable publication and the Creating dispatch arm — restoring #570's two-arm dispatch verbatim.
The tree returns to today's stale-field behaviour and today's publish-into-a-dying-parent window
together, in one git revert of the merge commit; nothing outside this phase consumes either
mechanism, so no other phase's code is left dangling. Verified by a git revert dry run on the merge
commit before merge, and written into the PR body first.

Dry run status: git diff main...HEAD | git apply --reverse --check → exit 0, clean, no conflicts.
git revert --no-commit --no-merges main..HEAD → exit 0, no conflicts; stages the full revert of this
branch's diff. git reset --hard HEAD restores the tree afterward (verified clean). The plain
git revert --no-commit main..HEAD form errors on a merge commit within the branch (is a merge but no -m option was given), which is expected — the --no-merges form is the equivalent check. Re-run
against the actual merge commit before merge, per the PLAN paragraph above.


Phase-3 scope (quoted)

From docs/planning/teardown-unification/PLAN.md, Phase 3 ledger entry (artifact T2-b):

exec detach + clone/exec admission: clear inherited_cr3/thread_group_id at every exec commit
and preserve on every failure; parent-Live validation + non-runnable publication + the Creating
dispatch arm

This clears the two stale-identity fields (inherited_cr3, thread_group_id) at every exec commit
point, preserving them on every exec failure path, and adds a Creating dispatch arm plus a
parent-Live admission check with non-runnable publication — restoring a three-arm dispatch (the
Creating arm was previously dormant/two-arm per #570) so a row can no longer be dispatched
mid-creation, and closing the wrong-victim-after-exec window where a stale inherited_cr3/
thread_group_id pair could let a kill or signal reach the wrong post-exec identity.

Revert dry-run result

Verified clean, two independent ways, against this branch's tip (details above under "Revert story"):
git diff main...HEAD | git apply --reverse --check exits 0, and git revert --no-commit --no-merges main..HEAD exits 0 with no conflicts. Single-revert-story property intact: round 3 added one commit
and its own revert for an x86 vector that turned out unbuildable (see D13 below), so those two cancel
in the branch's net diff and the merge commit still carries exactly the one story quoted above.

AC verdict

9 of 13 acceptance criteria fully met. Two met with caveat:

  • AC-6 — met with caveat: the page-table-alloc and stack-alloc arms are still not driven for field
    preservation (the failure-path preservation claim is proven for the exec paths exercised, not for
    every allocation-failure arm).
  • AC-7 — met with caveat: the futex evidence exercises the mechanism but cannot discriminate a
    stale key from a fresh one under the current oracle.

Two more split by architecture, both blocked on #567:

AC-13 — partially met: the single-revert-story requirement is verified here (dry run, two ways, at
r3 HEAD) and the PR-body half of the obligation is satisfied by this PR body itself — before this PR
existed, that half was an outstanding land-slot obligation only.

Regression check

Clean. Ratchets re-run at r3: 36/50/25/4/57/5, 0 failed — monotone versus r2. No assertion was
deleted. No gate FAIL condition was weakened anywhere; the only gate-script change since r2 adds
two fields to the aarch64 pinned literal (strictly more coverage, not less). All five Tier-1 files
(kernel/src/syscall/handler.rs, kernel/src/syscall/time.rs, kernel/src/syscall/entry.asm,
kernel/src/interrupts/timer.rs, kernel/src/interrupts/timer_entry.asm) are byte-identical to
main. Tier-2 edits (kernel/src/interrupts/context_switch.rs,
kernel/src/arch_impl/aarch64/context_switch.rs) add no hot-path allocation/formatting/blocking: the
x86 arms are a field read + a relaxed atomic + a once-only swap-gated raw-serial breadcrumb matching
the neighbouring NO_CR3 arm's existing pattern, and the aarch64 arm logs nothing. No over-free
surface: boot_restore_process_resources refuses unless the row is exclusively empty and moves
ownership, never duplicates it. Single revert story verified two independent ways (above).

A/B verdict

Not adverse. Designed-conditions matched sample (14 hogs, nothing else resident, hogs reaped
between legs): main rebuilt fresh from a clean worktree at 724ad2f4 ran 100/100; branch at
6223d0d9 ran 100/100; zero failures on either side. wakeLossBranchOnly=false. No new unfiled
signature appeared on the branch in any r3 leg — the only r3 failure anywhere is clean-gate run 014,
field-verified as pre-adjudicated #576; the r2 leg's run-048 wake loss is filed as #586 with
its serial preserved, and the r2 leg's other two failures are #536. The branch's starved failure
profile is attributable entirely to pre-adjudicated/filed signatures at main-comparable rates.

Stated limit (disclosed in the branch's own evidence, not hidden here): 100 runs per side cannot
exclude a roughly 1-in-100 branch-only effect. This is "did not reproduce under designed conditions" —
the campaign-legal disposition when a fresh, matched sample is filed and preserved and both samples are
reported side by side — not a re-run-to-erase.

Round closures

  • r1: B1–B6 closed.
  • r2: N1–N5 closed.
  • r3: D13 disclosed and ratified (below).

Evidence digest

Mutations including the ratchet-gap closures, custody proofs, beast strict 3/3 + batch runs, clean-gate
100/100, starved A/B (main vs. branch, matched samples, reported above), strict tallies, and Parallels
3/3.

Disclosed deviations

D6–D10 and D13 — ratified as coordinator ruling R6: the x86 real-dispatch vector for a
Creating-arm refusal is blocked on #567; the PLAN premise assuming an x86 equivalent was
corrected, and AC-10/AC-12's x86 halves are recorded blocked-on-#567 rather than silently marked met.
Rulings R1–R5 apply as previously ratified in this campaign's record.

Bonus fixes

Four production-defect fixes landed alongside this phase (found during the campaign, fixed forward
rather than deferred):

  1. fix(exec): detach the exec'd row from its pre-exec CLONE_VM thread group (the phase's core
    mechanism).
  2. fix(clone): admit clones only into a live parent, and refuse unpublished rows at dispatch (the
    phase's second mechanism).
  3. fix(sched): emit the exec lock-order markers atomically so nothing can tear them.
  4. fix(exec): restore TTBR0 when an aarch64 exec fails (a correctness bug found while proving the
    exec-failure preservation path for AC-6).

Filed issues

#583, #584, #586 — plus the #576 evidence comment (field-verifying the clean-gate r3
failure as the pre-adjudicated signature, not a new one).

Prerequisite

PR #585 (docs: restore the ./-prefixed aarch64 gate command the P0 ratchet pins) — merged ahead
of this PR, gate-command fix this phase's evidence runs depend on.

Standard attribution

Co-authored by Ryan Breen and Claude Code, per project convention.

ryanbreen and others added 28 commits August 16, 2026 12:26
`all_phase_zero_counters_have_registered_readers_and_honest_runtime_gates`
(tests/teardown_structure.rs:4542) pins the exact aarch64 standard-gate
command string. The tranche-2 document repair (#581) re-typeset the
standard-gate section and dropped the leading `./`, so the ratchet has been
red on main since that merge: 32 passed, 1 failed.

Repair the PLAN text, not the ratchet. Found while establishing the P3
anti-vacuity baseline; disclosed and fixed in the same pass rather than
carried as someone else's problem.

Co-Authored-By: Ryan Breen <rbreen@jrni.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Tranche-2 Phase 3, part 1 (DESIGN AC-6, PLAN "Scope - part 1, exec detach").

All four exec bodies in kernel/src/process/manager.rs - x86 exec_process and
exec_process_with_argv, aarch64 exec_process_with_argv and exec_process - now
reset `inherited_cr3` and `thread_group_id` to None immediately after
`process.page_table = Some(new_page_table.publish())`. That is inside the
committed region PR #582 created ("All fallible operations have succeeded"), so
every exec failure path above it leaves both fields byte-identical to their
pre-exec values. The aarch64 live-sibling guard is untouched: #468 stays open
and this phase does not close it.

Before this commit a row that execd kept advertising membership of a thread
group whose address space it no longer shared - the wrong-victim-after-exec
defect that was one of the four blockers which killed PR #418's group sweep,
and the stale root that P8's RootProof would later read.

Evidence, observed rather than argued, on BOTH arches in one commit:
exec_detach_oracle_test in kernel/src/tracing/providers/teardown.rs builds a
real CLONE_VM member row (page_table None, inherited_cr3 and thread_group_id
pointing at a live group leader) and drives both exec entry points through
three arms per body:

  - failure arm: the arch's corrupt ELF fixture -> Err("Segment data out of
    bounds"), both fields still exactly their pre-exec values;
  - live-sibling arm (aarch64 only, where the guard exists): a second live
    CLONE_VM member makes the exec return the guard's refusal, fields still
    preserved;
  - success arm: the arch's new valid ELF fixture -> Ok, both fields None, the
    row's actual level_4_frame() address read back and compared against the
    recorded group root (fresh root, read not inferred), and the effective
    group id computed the way real consumers compute it equals the row's own
    pid.

It emits [EXEC_DETACH_ORACLE:<arch>:...] with measured counts and returns the
frame allocator to its starting balance. The x86 side runs as a direct gate
from kernel_main; the aarch64 side is a registered PostScheduler boot test.

The structural half extends the existing per-exec-body census in
tests/teardown_structure.rs rather than adding a parallel one: each of the four
bodies must contain exactly one of each reset, positioned after the publish,
and no body may assign Some(...) to either field. A companion census pins the
production Some(...) writers across clone.rs, manager.rs and process.rs at
exactly one apiece, so exec clears the pair without becoming a second
group-join site. Shapes, not name lists - a fifth exec body fails the ratchet.

Also wires clonevm_exec_test to actually launch. It was built but never
started (grep -rn clonevm kernel/src returned nothing), so extending it would
have produced no gate evidence at all. x86 launches it in the RING3_SMOKE
block beside loopback_wake_test; aarch64 picks it up from the shared
boot::test_list::TEST_BINARIES. The program is reworked into three
deterministic phases - clone a live CLONE_VM child, probe the guard, release
the child and exec for real - plus a second stage that proves futex still
works in the post-exec group with a real two-thread rendezvous. The
live-sibling probe is aarch64-only and x86 prints an explicit SKIP marker
naming #468, because driving that path on x86 would exercise the open defect
rather than an invariant.

Co-Authored-By: Ryan Breen <rbreen@jrni.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
…hed rows at dispatch

Tranche-2 Phase 3, part 2 (DESIGN AC-7 admission half, PLAN "Scope - part 2,
clone/exec admission"). One mechanism, three inseparable pieces - the
non-runnable publication is only safe because dispatch refuses Creating rows,
so neither half ships without the other.

Admission. `ProcessManager::admit_clone_into` reads the parent row and asks
`Process::admits_clone()` inside the same process-manager transaction that
later publishes the child, so no snapshot of the parent's lifecycle is carried
across a guard drop. `sys_clone` calls it before copying any parent state out
and returns EAGAIN on refusal - DESIGN AC-7's stated evidence shape: the child
is either published or the caller gets EAGAIN, never a runnable unrequested
member. A missing parent refuses for the same reason a dying one does.

Non-runnable publication. The child row used to be fully Ready before it
existed: `state = Ready` plus `set_main_thread` (which also writes Ready) both
ran ahead of `insert_process`. Now the child thread is constructed Blocked, the
row is attached through `attach_main_thread_unpublished` and stays Creating
through insertion, and only then - still under the same guard - are the row and
its thread made Ready. Nothing runnable ever refers to a row that does not yet
exist.

Third dispatch arm. `refuse_unpublished_dispatch` is a cheap field read on a
row the caller already holds - no lock, no allocation, no formatting, no page
walk - placed before CR3 resolution at both x86 dispatch sites, and its aarch64
counterpart returns the new `TtbrResult::RowUnpublished` before any TTBR0 value
is computed. It is a third arm on #570's already-refactored site, in that
site's shape, with the same one-shot raw-serial breadcrumb. A refused row is
NOT terminated: Creating is transient by construction, so the recovery is
set_need_resched + idle return, the same shape PmLockBusy already uses on
aarch64. Terminating would be an over-free of a row about to become
legitimately runnable.

Both predicates are exhaustive matches over ProcessState with no catch-all, so
a new variant forces a decision here rather than defaulting.

Both exits from Creating are accounted for. `set_main_thread` and `set_ready`
both write Ready, and the oracle drives the refusal closed through EACH of them
in turn. The ratchet pins the Ready-write CENSUS FAMILY - every write of
ProcessState::Ready across kernel/src must live in process.rs, the row's own
lifecycle module - rather than the single set_main_thread site that the plan
text names. That is the PR #551 sensitivity lesson: pin the family shape, never
one site or a closed name list.

Anti-vacuity. Both new counters are driven nonzero by this PR's own workload.
`clone_admission_oracle_test` runs on both arches and exercises every arm of
both predicates: a live parent admits, a Terminated parent refuses, a
never-inserted pid refuses, a Creating row is refused at dispatch, and the same
row admits once published - once via set_ready and once via set_main_thread. It
emits [CLONE_ADMISSION_ORACLE:<arch>:...] with measured values and returns the
frame allocator to baseline.

Also closes out the exec-detach oracle's frame accounting, which the first
aarch64 boot run caught at balance=18 while every mechanism field was already
exact. Diagnosis: not page tables - all roots, tables, pending-old tables and
leaves were reclaimed - but the fixture's own user stacks, which
GuardedStack::drop still does not reclaim ("cleanup not yet implemented"). The
oracle now returns those external leaf frames through a fail-closed
boot-test-only helper that compare-exchanges the exact LEAF_EXTERNAL state and
goes through the normal return_lease choke point, so it can refuse but never
over-free.

Co-Authored-By: Ryan Breen <rbreen@jrni.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
…ot see

A mutation sweep over this phase found three deletions that left EVERY
structural ratchet green - a mechanism whose removal is invisible to the gate
is not ratcheted, so the designated mutation must turn it red.

Surviving mutations, now red in tests/context_restore_structure.rs:

  delete the admit_clone_into block from sys_clone
    -> "sys_clone must call admit_clone_into exactly once, found 0"
  restore the pre-phase publication order (child Thread Ready + set_main_thread
  before the row is inserted)
    -> "sys_clone child Thread must be constructed Blocked"
  delete a live-sibling guard call from an aarch64 exec body
    -> "aarch64 exec_process_with_argv must call
        find_live_clone_vm_sibling_holding_cr3 exactly once, found 0"

The first two pin sys_clone's admission and publication ordering as a derived
sequence - admission before any parent state is copied and with no guard drop
between it and the insert; attach-unpublished < insert < set_ready < thread
made runnable < guard drop < spawn - so any reordering names the link that
broke. The third is a census over the exec bodies rather than a name list or a
line pin: every aarch64 exec body calls the guard exactly once, before it
allocates a new address space, and refuses with an Err; no x86 body calls it.
That guard is retained because #468 is open and this phase does not close it,
and it sits immediately adjacent to the commit region this phase edits.

Suites stay at 48/33/25/4 - existing validators extended, not duplicated.

Co-Authored-By: Ryan Breen <rbreen@jrni.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
The exec-detach oracle measured a raw frame-allocator delta and required 0.
Real boots put that at 18 on aarch64 and 149 on x86 while every mechanism field
was already exact, so the number was never going to be 0 without a fudge.

Diagnosis: not page tables. Every root, table, pending-old table and leaf the
oracle creates is reclaimed. The residual is the fixture's own user stacks -
only rows that actually exec get one, which is why the neighbouring
exec_supersede_cohort_test never hits it - and the serial log names the gap
outright: "GuardedStack dropped (cleanup not yet implemented)". Filed as #583.

An earlier attempt handed those frames back through a boot-test-only allocator
helper. That is an over-free surface, and a counted leak beats an over-free, so
the helper is gone and kernel/src/memory/frame_allocator.rs is byte-identical
to main again.

The oracle now separates three named quantities, each a hard equality, none a
tolerance:

  custody_balance  the page-table ledger it actually owns - tables returned ==
                   recorded, roots retired == created, loss/undecided/
                   mid-retire/no-arch all zero, refusal counters unchanged.
                   Asserted at 0, and it holds at 0.
  leaf_residual    leaf frames registered for the fixture's stacks that #583
                   never returns. Pinned per arch by exact equality so it
                   cannot silently grow.
  stack_residual   the raw allocator delta from the same cause, pinned the same
                   way.

Closing #583 should drive both residual consts to 0, which will fail these
equalities and force the pin to be re-measured - which is the point.

aarch64, measured:

  [EXEC_DETACH_ORACLE:aarch64:bodies=2:fail_preserved=2:sibling_refused=2:
   success_detached=2:fresh_root=2:tgid_self=2:custody_balance=0:
   leaf_residual=16:stack_residual=18]
  [TESTS_COMPLETE:101/101] [BOOT_TESTS:PASS]

Co-Authored-By: Ryan Breen <rbreen@jrni.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
…count

Two doc errors the P3 implementation had to work around, fixed where they live
rather than worked around silently.

The "fresh root is observed, not argued" bullet named
fork_exit_defer_reclaim_pairing_test (:1192) as the exec-cohort per-PID oracle.
That is the fork/exit defer-reclaim pairing oracle and emits no PT_EXEC_COHORT;
the exec-cohort oracle is exec_supersede_cohort_test. It is also x86-only, so
it cannot carry DESIGN AC-6's "both arches in one commit" by itself - which is
why P3 adds the arch-neutral exec_detach_oracle_test alongside it. The bullet
now says all of that.

The standard gate named four pinned x86 custody literals. The script pins
seven: #573/PR #582 added EXEC_FAILED_RELEASE_ORACLE and
EXEC_FAILED_RELEASE_PROD and re-pinned PT_CUSTODY_COUNTERS, and P3 adds
EXEC_DETACH_ORACLE. The script is the truth, so the text now defers to it and
states the re-pin rule explicitly: a phase that perturbs a literal re-pins it
with a per-delta derivation, never to green a red gate it does not understand.

Co-Authored-By: Ryan Breen <rbreen@jrni.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
The gate rebuilt the BXTEST disk unconditionally but guarded the ext2 image
behind `test -f target/ext2.img ||`, so once that file existed it was never
refreshed and every later run booted whatever userspace binaries were installed
the day it was first created.

Not hypothetical. A measured run of this branch had a freshly built test
program execv its own path under /usr/local/test/bin/ and land in a months-old
copy of itself, which exec'd /bin/simple_exit and turned the gate red:

  TEST_TALLY: exited=99 nonzero=1 failed=[/bin/simple_exit:42]

A gate reporting a real failure for a reason unrelated to the code under test
is the worst kind. The script's own comment already states the rule for the
other disk - "repack every run to pick up rebuilt userspace" - and the ext2
image carries the same ELFs, so it follows the same rule.

Ratcheted in tests/teardown_structure.rs: the script must not contain
`test -f target/ext2.img`, and both disk images must show the
remove-then-build shape. Restoring the guard turns
frame_ledger_return_and_initialization_ratchets_are_exact red (32 passed,
1 failed), so the trap cannot come back.

Co-Authored-By: Ryan Breen <rbreen@jrni.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
The second stage cloned a CLONE_VM child that FUTEX_WAITed while the parent
FUTEX_WAKEd it. On a real x86_64 boot the program reached
"CLONEVM_EXEC_TEST: second stage" and never finished: no further marker, no
TEST_TALLY at all, gate exit 1. Breenix FUTEX_WAIT has no timeout and has an
enqueue-to-block window in which a wake is lost, so the child parked forever.
Filed as #584.

That is a real kernel defect but it is not this phase's mechanism, and a phase
gate must not hang on it. The criterion being served is "futex behaviour across
an exec verified explicitly - the group id falls back to pid, and futex.rs is
the main consumer". A cross-thread rendezvous is one way to show that; it is
not the only honest way, and it is the one that hangs.

The second stage now exercises both futex entry points on the post-exec row at
two distinct keys, deterministically and without any path that can park:
FUTEX_WAKE with no waiters must return 0, and FUTEX_WAIT against a value that
does not match must return -EAGAIN before any enqueue. Both derive their key
through current_thread_group_id() on the row as it exists after the exec, which
is exactly the consumer the criterion names.

The comment at the top of the second stage says plainly what this does and does
not prove, and points at #584; restoring the rendezvous is that issue's job.
The unused child machinery is deleted rather than left dead. Phases 1-3 and
every marker string in them are unchanged.

Co-Authored-By: Ryan Breen <rbreen@jrni.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Standard-gate item 2 requires a phase that adds a userspace test program to
re-pin EXPECTED_USERSPACE_EXITS in the same PR. This phase launches
clonevm_exec_test, so the floor moves - from a measurement, not an estimate.

Two full x86_64 boot-test runs, same host, same script, same environment:

  main @ 43336f5   TEST_TALLY: exited=95 nonzero=0 failed=[]   gate PASS
  this branch       TEST_TALLY: exited=99 nonzero=0 failed=[]   gate PASS

Delta +4, enumerated by name from the branch run's serial log:

  /usr/local/test/bin/clonevm_exec_test  the launched program; it execs into
                                         its own second stage, so it is one
                                         process with one death, renamed by
                                         the exec
  thread-103                             its phase-1 CLONE_VM child, which
                                         exits once the parent releases it
  clone_admission_a, clone_admission_b   the two rows the clone-admission
                                         oracle tears down through the
                                         terminate choke point

The exec-detach oracle contributes zero: its rows go out through the
deferred-reclaim path, which does not pass the Process::terminate /
terminate_minimal choke point where the tally is written. 95+1+1+1+1 = 99.

tests/loopback_pump_structure.rs pins the literal in four places, including a
deliberately-wrong negative control one below the floor; the control moves with
it (94 -> 98) so the one-below relationship still proves the assertion.

run-boot-parallel.sh's floor is deliberately NOT re-pinned. Its runner did not
complete a green pass in the x86 environment available here, and re-pinning
from a run that was not green is exactly what this campaign forbids. It is a
>= floor, so an under-pin is safe but stale; it must be measured and re-pinned
from a green run before merge.

Co-Authored-By: Ryan Breen <rbreen@jrni.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
…hed booleans (B6)

run_single_test polled every 1.5s and latched USERSPACE_DETECTED /
EXEC_SMOKE_COMPLETE, then scored the boot from those booleans after killing
QEMU. A marker that landed between the last poll grep and the kill was present
in the serial file while the boolean was still false, so a healthy boot was
reported as "Userspace not detected" / "Exec smoke did not complete".

Split the two concerns the loop had conflated: the poll loop now only decides
when to STOP WAITING, and a new score_serial() decides the verdict from the
serial file QEMU actually left behind. Every rejection the gate made before is
made here - crash markers, missing userspace liveness, missing
[EXEC_SMOKE:TARGET_OK], missing [EXEC_LOCK_ORDER:FIRST_COMMIT] - nothing is
loosened; only a latched-false boolean loses its ability to outrank the file.

BREENIX_STRICT_SCORE_ONLY=<serial> scores a captured log without booting, which
is how the "a serial containing every success marker scores as a success"
property is proven directly.

Co-Authored-By: Ryan Breen <rbreen@jrni.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
The poll loop broke - and killed QEMU immediately after - as soon as
TEST_TALLY: appeared. The kernel prints TEST_TALLY first and
"TEST RUNNER: All tests passed" / "TEST RUNNER: FAILED" last
(kernel/src/syscall/handlers.rs), and scripts/x86-gate-verdict.sh requires the
terminal marker, so a run whose QEMU was killed in that window failed the
verdict with "nonzero=0 but the all-tests-passed marker is absent" - a healthy
boot scored red. Observed on run 2 of a 3-run batch during the P3 round-1 gate.

Wait for the terminal marker itself. Either polarity ends the wait, and the
failing polarity is still rejected by the verdict script, so no FAIL condition
moves; the only change is that QEMU is no longer killed before the marker the
gate reads.

Co-Authored-By: Ryan Breen <rbreen@jrni.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
…3, B6)

This gate has never been green on the x86 gate host. It hard-required
`docker run`, but the host that runs every x86 gate (the breenix-x86 container
on beast) has no Docker daemon - run-x86-boot-tests.sh already invokes QEMU
natively there. Worse, the launch was backgrounded with `&>/dev/null`, so
"docker: command not found" was invisible and the run surfaced 120 seconds
later as an indistinguishable TIMEOUT. Reproduced on unmodified main
(43336f5): 0 passed, 1 failed, empty serial logs.

Docker only ever supplied the qemu binary; the QEMU arguments are byte-identical
either way, and only the image paths differ because the container sees them
through bind mounts. So: prefer native qemu-system-x86_64, fall back to docker,
and refuse to run when neither exists. Launch output now goes to
$OUTPUT_DIR/runner.log, and a runner that exits without producing serial output
is reported as a launch failure with its log instead of a silent timeout. The
missing test_binaries.img / ext2.img preconditions are checked up front for the
same reason.

Also apply the B6 terminal-marker fix here: this script asked
scripts/x86-gate-verdict.sh for a verdict the moment TEST_TALLY: appeared, which
races the "TEST RUNNER: ..." marker the verdict requires. Wait for the terminal
marker instead.

No FAIL condition is loosened: the kthread markers, the tally, and the full
x86-gate-verdict.sh judgement are all still required, and two new failure modes
(no runner, dead runner) are now reported instead of hidden.

Co-Authored-By: Ryan Breen <rbreen@jrni.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
… run (B3)

D9 left this obligation open in round 1 because the profile had never produced a
green run to measure. With the runner fixed it does, so pin it from measurement,
never from a red run.

Measured on the x86 gate host, one boot each, same script, same runner:
  main @ 43336f5 : TEST_TALLY: exited=15 nonzero=0 failed=[]  gate PASS
  this branch     : TEST_TALLY: exited=17 nonzero=0 failed=[]  gate PASS

The pin was 10 - five below main's own measurement, because loopback_wake_test's
five-process cohort reached this profile without the floor moving and a `>=`
floor cannot report an under-pin. Derivation of the branch's two additions, by
name, from the branch serial:

  +1  clonevm_exec_test  - the launched program; it execs into its own second
      stage, so one row, one death, renamed by the exec.
  +1  thread-14          - its phase-1 CLONE_VM child row (sys_clone names child
      rows thread-<pid>).

15 + 1 + 1 = 17. Both boot_tests-only oracles contribute zero to this profile,
which is built testing,external_test_bins.

Co-Authored-By: Ryan Breen <rbreen@jrni.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
…n tear them (B6)

RCA of the P3 round-1 aarch64 strict-boot run that scored 19/20. The preserved
serial (/tmp/breenix_aarch64_strict_failures/20260816T200814Z-boot2.txt) contains

    [heartbeat] tid=176 upti[me_ms=3866E kbd_nonzerXo=0
    EC_LOCK_ORDER:FIRST_COMMIT]

- the gate-pinned [EXEC_LOCK_ORDER:FIRST_COMMIT] marker interleaved character by
character with a concurrent userspace write() on another CPU. Round 1 attributed
that run to the strict script's latched poll booleans; the booleans were a real
defect (fixed separately) but this is what actually turned the boot red.

ExecSchedCommit::apply emitted its four markers with raw_uart_str, which writes
bytes to the UART with no lock at all, so any concurrent serial writer can split
them. Two aarch64 gate scripts pin those literals, which makes a torn line a
false red on a healthy boot.

All four emissions sit after the inner scheduler-guard block has ended and run on
the exec syscall path with interrupts masked, holding no process-manager lock, so
the locked, atomic-per-line serial_println! is safe here - the lock-free writer
was never required, only inherited.

tests/exec_lock_order_structure.rs is tightened in the same commit rather than
loosened: where it used to ban serial_println! outright it now requires exactly
four locked marker writes, bans raw_uart_str anywhere in apply, requires each of
the four literals exactly once, and requires every write to be positioned after
the scheduler guard is released. That is strictly more specific than the rule it
replaces - the original rule existed to keep output out of the locked region, and
that is now checked directly.

Co-Authored-By: Ryan Breen <rbreen@jrni.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
The Creating-row dispatch refusal is a retry-only arm: the refused thread must be
dispatched again once its process row is published. On aarch64 that works - the
RowUnpublished/PmLockBusy arm calls requeue_thread_after_save after updating
cpu_state. On x86 it did not: scheduler::switch_to_idle() only rewrites
cpu_state[cpu].current_thread, so after the refusal the thread was neither current
on any CPU nor present in any per_cpu_queues entry. It was stranded, and the
"retry" never happened. The neighbouring NO_CR3 arm gets away with the same shape
only because it terminates the thread first; a retry-only arm cannot.

requeue_refused_dispatch is the x86 counterpart, mirroring the aarch64 helper's
safety checks with only the ones x86 has: never an idle thread, never a thread
that is current on any CPU (double-queueing a running thread is the register/stack
corruption the aarch64 comment describes), only a Ready thread, and only when it
is absent from every queue. No logging, no allocation, no formatting - it runs on
the dispatch path.

Both x86 refusal arms now call it immediately after switch_to_idle(), so cpu_state
already names idle when the requeue evaluates "is this thread running anywhere",
which is exactly the ordering the aarch64 arm documents.

tests/context_restore_structure.rs pins the new shape at both sites:
switch_to_idle -> requeue_refused_dispatch -> return, with the existing
"no set_terminated in a retry-only arm" rule untouched. Its two synthetic
fixtures were updated to match, so the negative controls still discriminate.

Co-Authored-By: Ryan Breen <rbreen@jrni.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
…h (B4)

Round 1 drove USERSPACE_DISPATCH_CREATING_REFUSED nonzero by calling
refuse_unpublished_dispatch directly on a synthetic row. That proves the
predicate; rule 2 is about arms. No test ever made the real dispatcher refuse a
row, so the recovery/requeue arm had zero runtime coverage.

creating_dispatch_refusal_test injects the fault the arm exists for: a real
process row with a real page table, forced back to ProcessState::Creating while
its own main thread - mutated in place into a kernel-context probe with
blocked_in_syscall = true, so it takes the aarch64 kernel-restore dispatch arm -
is spawned runnable onto CPU 1. It then observes, through the actual dispatcher:

  * the refusal counter rising by at least TWO. Two, not one: a second refusal is
    only reachable if the first one requeued the thread, so the count is the
    runtime evidence that the arm retries rather than strands.
  * the probe NOT having run before publication,
  * and the probe running after set_ready() publishes the row.

[CREATING_DISPATCH_ORACLE:aarch64:injected=1:refused_via_dispatch=1:requeue_retried=1:dispatched_after_publish=1:balance=0]
is pinned by docker/qemu/run-aarch64-full-test.sh, and the ratchet forbids the
oracle from calling refuse_unpublished_dispatch at all, so the evidence cannot
regress back into a predicate call.

Two real defects surfaced getting this green, both fixed at source rather than
worked around:

1. The oracle left the probe row's root installed in CPU 1's TTBR0 and in the
   thread's cached shadow after the probe terminated. The root was then freed and
   handed straight back to the next test's fixture, whose RootProof correctly
   refused to retire a still-referenced root - reclaim_progress_gate went red with
   PT_RETIRE_BUDGET_REQUEUED moving by 0 instead of 1. The probe now quiesces
   TTBR0 on the CPU that installed it, clears the cached shadow, and the oracle
   re-checks the receipt against the RootProof predicates before removing the row,
   failing with its own reason if any leg is still referenced.

2. reclaim_progress_gate_test collapsed sixteen distinct clauses into two failure
   strings, so its serial could not say which global moved. Every clause now has
   its own reason plus a [RECLAIM_PROGRESS_GATE_DIAG:...] line carrying the
   observed value, the expected value, the difference, and the RootProof blocker
   deltas. Nothing was weakened or reordered - that split is what turned "F:
   oversized retirement did not requeue at its exact budget" into a one-line
   diagnosis.

Verified by A/B on the real gate: 101/101 PASS at 27e5a24 without this oracle,
102/102 PASS with it.

Co-Authored-By: Ryan Breen <rbreen@jrni.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
…he exec'd row (B1)

The ratified Phase-3 gate extra reads "successful exec -> ... and a kill aimed at
the old group cannot reach it". Round 1 shipped nothing for it and disclosed
nothing, which under campaign law is the finding regardless of whether the
deviation would have been granted.

There is no group-scoped kill syscall until P9 - sys_kill is pid-scoped - and the
only userspace row that could carry a foreign group id is a CLONE_VM child, whose
exec the live-sibling guard refuses while its leader is alive and whose leader
cannot be retired first without exercising open #468 inside the gate. So the
evidence is made in the kernel, on both arches, by exec_detach_oracle_test, and it
aims the kill the way P9's sweep will rather than inventing a predicate: victims
are selected with thread_group_id.unwrap_or(pid) - the same expression sys_clone
derives a child's group from and futex.rs::current_thread_group_id reads - and the
signal is delivered through the row's own SignalState, exactly as sys_kill's
non-SIGKILL path does.

Three counters carry it, each exactly 2 (one per exec body) on both arches:

  old_group_reached_pre    the anti-vacuity control. Before the exec, a kill aimed
                           at the leader's group reaches BOTH rows (leader and
                           member). Without this the post-exec miss proves nothing.
  old_group_missed_post    the assertion. After the exec, the same kill reaches
                           the leader only; the exec'd row is not in the victim set.
  self_group_reached_post  the positive control. A kill aimed at the exec'd row's
                           OWN group reaches it, so the miss above is detachment
                           and not a broken probe.

Every pending signal the probe sets is taken back, so no row is retired carrying
one, and the probe neither allocates nor frees, so custody_balance, leaf_residual
and stack_residual are unchanged.

The pinned x86 literal and the teardown_structure vector move together with the
line. The PLAN records where each half of the AC's evidence lives and why the
userspace half is unavailable at this phase.

Co-Authored-By: Ryan Breen <rbreen@jrni.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
…lly boot (B2)

Round 1's aarch64 wiring was one line in kernel/src/boot/test_list.rs, and it was
compiled out of every aarch64 gate kernel. TEST_BINARIES has exactly one consumer,
load_test_binaries_from_ext2() in main_aarch64.rs, whose only call site sits under
#[cfg(feature = "testing")]; every aarch64 gate builds --features boot_tests and
kernel/Cargo.toml defines boot_tests = [] with no implied features. The branch's
own gate serial confirmed it: zero CLONEVM_EXEC_TEST lines in a run that contained
both new oracle lines. No gate pinned the program's markers either, so a silent
skip was invisible - and the live-sibling probe, which is #[cfg(target_arch =
"aarch64")], therefore executed on no architecture at all.

In the boot_tests profile the aarch64 boot reaches userspace through /sbin/init,
which is where exec_smoke is already launched, so that is where the launch belongs.
init spawns the program after the exec smoke (never before: the gates accept on
the smoke's marker) and reports its exit.

docker/qemu/run-aarch64-full-test.sh gains a Phase 1c that requires
"CLONEVM_EXEC_TEST: PASS", fails on any "CLONEVM_EXEC_TEST: ERROR" line or crash
marker, and additionally requires "live sibling refused exec" - on this arch a SKIP
would mean the guard probe had been compiled for the wrong target. A program that
stops being launched, stops passing, or silently skips is now a gate failure.

Co-Authored-By: Ryan Breen <rbreen@jrni.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Carries PR #585 (724ad2f), the ./-prefixed aarch64 gate command the P0 ratchet
pins, which this branch had been carrying as a second revert story. After this
merge the branch's own history holds exactly one story: tranche-2 P3.

Co-Authored-By: Ryan Breen <rbreen@jrni.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Found by running clonevm_exec_test on aarch64 for the first time. The program
probes that exec is refused while a live CLONE_VM sibling shares its address
space; the refusal was correct, but the caller then faulted on the instruction
right after its own svc:

    [INSTRUCTION_ABORT] FAR=0x40000e40 ELR=0x40000e40 ESR=0x8200000e IFSC=0xe
                        TTBR0=0x40200000 from_el0=1

ESR decodes to an instruction abort from a lower EL with a level-2 permission
fault, and 0x40000e40 is inside libbreenix::process::execv, immediately after its
syscall instruction. 0x40200000 is the kernel root: its 0x40000000 L2 block is
kernel-only, so EL0 cannot fetch from it.

Root cause: sys_exec_aarch64 installs the kernel TTBR0 before the fallible
process-manager call, and no failure arm rolled it back. Every failed exec
therefore returned to EL0 with the kernel address space installed - the process
had no user mappings at all, and the first instruction it executed faulted. The
live-sibling guard is only the arm that made it visible; the ELF, page-table and
stack failure arms return through the same path, so this defect sat under every
aarch64 exec failure. It is not #468: nothing was freed and no root was shared.

The syscall now captures the architectural TTBR0 immediately before installing
the kernel root, and every failed manager exec restores it with the required
barriers and TLB invalidation, republishes it to saved_process_cr3, and clears
next_cr3 so the syscall epilogue cannot reinstall a stale target.

tests/context_restore_structure.rs pins the capture-before-transition ordering,
exactly one rollback in the failure arm positioned before the return, and the
coherence of both shadows, with a mutation fixture proving the validator goes red
when the rollback is removed.

Co-Authored-By: Ryan Breen <rbreen@jrni.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
…ock (B2)

Second defect found by running clonevm_exec_test on aarch64. With the TTBR0
rollback in place the program reached "live sibling refused exec" and then made no
further progress, while the rest of the system stayed healthy - no fault, no
panic, heartbeats still printing. The step it was stuck on is the release
handshake: the parent writes the release command and waits for the kernel to zero
the child's CLONE_CHILD_CLEARTID address when the child exits.

Root cause: sys_exit_aarch64 performed a raw write_volatile to that user address
while still holding PROCESS_MANAGER. If the write faults - and a CLONE_VM child's
shared page legitimately can - handle_cow_fault_arm64 tries to reacquire
PROCESS_MANAGER and the CPU stalls there silently. x86_64's handlers::sys_exit
does not have this shape, which is why the same program completes there.
ProcessScheduler::handle_thread_exit never handled clear_child_tid at all, so the
aarch64 exit path was the only place it could be done.

The exit path now snapshots (thread_group_id, tid_addr) under the process-manager
lock, DROPS the lock, performs the write through userptr::copy_to_user while the
exiting thread's TTBR0 is still installed, futex-wakes the same
(thread group, address) key the wait side derives, and only then switches to the
kernel root and runs teardown.

tests/teardown_structure.rs censuses this as a per-architecture invariant: exactly
one clear_child_tid exit consumer per arch, the
snapshot -> copy_to_user(tid_addr) -> futex wake -> teardown ordering, and no raw
volatile user write on the exit path. The mutation that restores the old raw-write
shape turns it red (34 passed / 1 failed), and it is green at 35 with the fix.

With this the aarch64 gate is green end to end: 102/102, and
CLONEVM_EXEC_TEST reports start / child live / live sibling refused exec /
child exited / second stage / post-exec futex keys derived / PASS.

Co-Authored-By: Ryan Breen <rbreen@jrni.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
… (B1)

Found by the designated mutation sweep, not by review. Deleting the pre-exec
group-kill probe from exec_detach_oracle_test - the control that establishes the
row IS reachable by a kill aimed at its group while it is still a member - left
teardown_structure at 35 passed, 0 failed. The mutation was invisible, which means
the AC-5 evidence could have decayed into a post-exec assertion with no control
behind it and no gate would have said so.

Census the oracle's kill-probe shape the way this file censuses everything else:
all three counters present, the pre-exec control positioned before the exec it
controls for, and the post-exec assertion and positive control after it.

Re-proven: 36 passed with the control present, 35 passed / 1 failed with it
deleted.

Co-Authored-By: Ryan Breen <rbreen@jrni.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
`creating_dispatch_refusal_test` measured `leaf_residual` and
`user_stack_residual` every boot and printed them only on its DIAG line.
Nothing asserted either value and the aarch64 gate pinned only the
non-residual marker, so a tenfold growth in either would have been
invisible everywhere - the same ratchet blindness D7 already closed for
`exec_detach_oracle_test`.

What the 16s are: the oracle creates exactly one real process row through
`manager.create_process`, whose 64 KiB user stack is 16 x 4 KiB frames.
`GuardedStack::drop` does not reclaim user stack frames (#583), so those
16 mappings are recorded and never returned (leaf_residual = 16) and the
frame allocator ends 16 frames heavier (user_stack_residual = 16). Same
issue, same class, same counted-not-freed disposition as the exec-detach
residuals; closing #583 drives all of them to zero together. Measured
stable at 16/16 across 27+ boots of the aarch64 boot_tests profile - only
`refusal_delta` varies - so both are pinned as exact equalities, not
ranges.

Both layers hold independently: the kernel-side `!=` comparisons fail the
test before the marker is emitted, and the success marker now carries the
two values as measured `{}` arguments (the D7 shape) rather than as
literal text, so a drifted residual also breaks the gate's exact-line
match even if the assertions were deleted. Mutation-proven: deleting a
comparison and re-hardcoding the marker fields each turn the ratchet red.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (N3)

`scheduler::requeue_refused_dispatch` is this round's actual x86 production
fix - before it, the x86 Creating-refusal arms redirected the thread to
idle and returned without requeueing, leaving it neither current nor
queued. It shipped with three callers in the whole tree (the definition
plus the two refusal arms) and no gate on any arch entering those arms:
x86's `creating_refused=1` came from `clone_admission_oracle_test` calling
the predicate directly. AC-10/AC-12 want both arms of every new predicate
exercised by this PR, so the fix was pinned but dormant.

The aarch64 vector does not port as-is. On x86 both refusal arms sit on
the userspace-dispatch path and `switch_to_thread` short-circuits
Kernel-privilege threads to `setup_kernel_thread_return` before either arm
is reached, so an aarch64-style kernel-context probe would never be
refused. This vector keeps the scheduler-visible thread `User` privilege
with `blocked_in_syscall = true` - which reaches the admission arm - and
parks a kernel-mode return frame in `process.main_thread.context`, which
is exactly what that arm restores once the row is published. Both halves
are therefore real: refusal through the dispatcher, and a post-publication
dispatch that runs, without ever entering ring 3.

The refusal observation is `delta >= 2`, same as aarch64: a second refusal
is reachable only if the first one requeued. The dispatch arms themselves
are untouched - the vector drives existing code - and the
`requeue_refused_dispatch` census stays at three hits.

The residual constants are deliberately impossible sentinels: this gate is
RED by construction until calibrated against a measured x86 boot, which
lands in the next commit. Nothing here claims a green x86 gate.

Ratchet (mutation-proven red, all three): the oracle cannot move its
counter through a direct `refuse_unpublished_dispatch` call (M-x1, the
mirror of aarch64's M10), cannot weaken the observation to `>= 1` (M-x2),
and cannot drop a residual equality (M-x3).

Exit floors are unchanged and re-derived, not assumed: the probe row is
removed through deferred reclaim and its thread is marked terminated by
the probe, so it never reaches the exit tally - `EXPECTED_USERSPACE_EXITS`
stays 99 and run-boot-parallel.sh stays 17 (that profile does not build
this oracle at all).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dormancy (N3)"

This reverts commit 5fea9eb.

The vector cannot run on x86, and I proved that by booting it rather than
by reasoning about it. Measured on beast at 5fea9eb: the oracle emitted
nothing - no START marker, no DIAG line - while every other x86 custody
literal was present and exact.

Why, from the boot evidence:

1. The x86 serial contains no `[STAGE:serial|early|sched:ADVANCE]` marker
   at all. Its `[BOOT_TESTS:PASS]` comes from `advance_stage_marker_only`
   on the syscall path (`[STAGE:user:ADVANCE]` immediately precedes it),
   not from `run_all_tests`. The boot-test registry never runs on x86, so
   a registry entry - whatever its `arch`/`stage` - is not a vehicle there.
2. Every x86 custody oracle is instead an explicit `run_x86_*_gate()`
   driver called from `kernel_main_on_kernel_stack`, with interrupts
   disabled and before the scheduler preempts. `yield_current()` on x86
   only sets `need_resched`; the switch happens at the next interrupt
   return. A real dispatch is therefore unreachable from that window.
3. That window is not merely inconvenient, it is a known-fatal one:
   **#567** (open) - "any boot-time code path in
   `kernel_main_on_kernel_stack` that ... spawns a kthread and lets it be
   scheduled ... can resume with a corrupted CPU context and die", and
   "almost certainly the same reason the boot-test registry's EarlyBoot
   stage hangs on x86". `main.rs` and `run-x86-boot-tests.sh` both already
   carry comments deferring the four scheduling tests on x86 until #567
   is fixed.

So driving a real x86 `Creating` dispatch requires the exact mechanism
#567 documents as fatal. Landing an oracle that cannot run - together with
a gate literal built from impossible sentinels - would have added dormant
code and a permanently red gate in the name of closing a dormancy finding.
Keeping it would be strictly worse than the status quo.

N3 closes instead via the alternative the review names explicitly:
"or by recording an explicit disclosed deviation naming the x86 arm as
structurally-pinned-only and why". That deviation (D13) is recorded in the
next commit, with #567 as the evidenced blocker and the vector design
preserved on #567 so it can be built the moment #567 is fixed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (N3)

Closes N3 by the alternative the r2 review names: "or by recording an
explicit disclosed deviation naming the x86 arm as structurally-pinned-only
and why". The "why" is evidenced by a boot, not asserted.

The x86 arm cannot be runtime-proven on today's tree because x86 has no
window in which a real dispatch can be driven from a boot test:

- x86 does not run the boot-test registry at all. Its `[BOOT_TESTS:PASS]`
  comes from `advance_stage_marker_only` on the syscall path, preceded by
  `[STAGE:user:ADVANCE]`; no `serial`/`early`/`sched` stage marker is ever
  emitted. A registered test is not a vehicle on x86 regardless of its
  `arch`/`stage` fields - measured directly at 5fea9eb, where the
  registered x86 oracle produced no marker at all while every other x86
  custody literal was present and exact.
- Every x86 custody oracle is an explicit `run_x86_*_gate()` driver in
  `kernel_main_on_kernel_stack`, interrupts disabled, before the scheduler
  preempts. On x86 `yield_current()` only sets `need_resched`; the switch
  happens at the next interrupt return, so no dispatch can occur there.
- #567 (open) documents that window as fatal to scheduling events, and
  names it as the likely reason the registry's EarlyBoot stage hangs on
  x86. Four scheduling tests are already deferred there for that reason.

So the required mechanism is precisely the one #567 reports as broken.

Nothing was relaxed to accommodate this. The two x86 refusal arms keep
their exact `switch_to_idle -> requeue -> return` ordering pins, the
"no set_terminated in the arm" rule, and the "refusal precedes any CR3
publish" rule - all mutation-proven. The aarch64 real-dispatch proof is
untouched. The asymmetry is stated in the PLAN clause itself so it cannot
be read as symmetric, and the vector design is recorded on #567 so it gets
built the moment #567 closes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The x86 custody gates in kernel_main_on_kernel_stack do not run with
interrupts disabled - they run inside the IF=1 driver-post-init
self-test window (main.rs:600-652), which the in-tree comment says is
required precisely because the gates need live timer ticks and
scheduler epochs. The true barrier to a real x86 Creating dispatch is
open #567's poisoned-resume hazard in that same window (a resumed
thread can come back with a corrupted CPU context), not an inability
to dispatch under IF=1 as commit 98233a3's message wording implied.

Coordinator ruling R6: D13 ratified as-is otherwise - AC-10's x86 half
and AC-12 stay blocked-on-#567, with the structural pins (M8a/M8b/M9)
and the aarch64 real-dispatch proof standing as evidence, and the
vector design parked on #567 to build when it closes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the roadmap one-liner and PLAN.md ledger row for P3 (exec detach +
clone/exec admission), landing per the 2026-08-16 tranche-2 re-ratification.

Co-Authored-By: Ryan Breen <rbreen@getfastr.com>
@ryanbreen
ryanbreen merged commit d2f9e4b into main Aug 17, 2026
@ryanbreen
ryanbreen deleted the feat/teardown-p3-exec-detach branch August 17, 2026 04:03
ryanbreen added a commit that referenced this pull request Aug 17, 2026
…ed_init (#527, P5a)

Tranche-2 Phase 5a, commit 2 of the phase's two-commit seam. Commit 1 shipped
the reservation, the ticket and the authority with its production validation
as its live consumer; this commit moves the production init sites and their
dependent reads onto designated_init(), ratchets the result, and repairs the
userspace contract the plan wrongly believed did not exist.

Kernel migration - all four production sites and all five dependent reads:
ProcessManager::exit_process_locked and ProcessScheduler::handle_thread_exit
both reparent through the one authority, reparent_children_to_init, which
returns whether it ran so the exiting row's children are cleared on exactly
the path that cleared them before. handle_thread_exit's "init has no children
to reparent" test compares against the designation instead of the literal.
syscall/signal.rs loses const INIT_PID entirely; kill(-1) and its existence
check exclude the designated init when one exists. With no designated init the
defined behaviour is that nothing is excluded by identity and children keep
their existing parent rather than being pointed at a PID-1 row that does not
exist - the oracle drives both arms in the same run. grep for
ProcessId::new(1) in kernel/src now returns only the three allowlisted
test_userspace.rs sites.

Userspace contract (coordinator ruling R7, marshal brief planContradiction
PC-1). The plan's premise that init_shell.rs:1028 was the only userspace PID
contract was false: bsh.rs decided it was the boot shell by testing
getpid() == 2 || 3, which reserving PID 1 renumbers straight through, taking
/etc/init.js and the whole service chain with it, silently. The role is now
conferred by init passing --init-shell and by nothing else; no PID value
participates, so renumbering cannot reach it. init_shell.rs:1028 is untouched
and pinned.

Ratchets (tests/teardown_structure.rs, 36 -> 41 tests). PRODUCTION_INIT_PID_SITES
is now empty. Five new census tests, all shape-anchored per the #549/#551
lesson - never line pins, never a closed list of names:
- the reservation is single-sourced: exactly one next_pid.fetch_add site, the
  eight allocate_ordinary_pid call sites pinned by enclosing item, and the
  base expressed as FIRST_ORDINARY_PID with no numeric literal;
- the init-PID constant FAMILY is confined (ruling R10, PC-2): the old census
  saw only the text ProcessId::new(1) and was blind to const INIT_PID: u64 = 1
  and to direct as_u64()/raw() comparisons against 1;
- the designation authority is closed: one field, two writers (the designation
  transaction and its retirement), and the two ticket types are constructed at
  exactly one site each, are neither Clone nor Copy, and have no public
  constructor or public field;
- the mechanism items carry zero #[cfg];
- the init-shell role is argument-derived, and the oracle's launch wiring is
  non-vacuous: one main.rs direct call, one registry TestDef, and the marker
  pinned in both of the x86 gate's lists.
Each new census carries its own in-test anti-vacuity control - reintroducing
const INIT_PID, an as_u64() == 1 comparison, a foreign ticket construction, a
designation write inside a constructor, or a #[cfg] inside a mechanism item
each turns its census red - so deleting a control is visible.

The x86 gate's oracle literal is pinned at construct_residual=4 and the
aarch64 gates at 2, both read off green measured runs: the residual is the
counted frame residue of the two construction-failure arms, which is
arch-specific because the two page-table constructors record different table
counts.

Doc repairs in the same PR, per coordinator rulings R8/R10/R11/R12: the
held-publication ticket's terminal step is defined against each arch's real
shape rather than a run-queue publish the aarch64 constructor never performs;
the P0 ratchet paragraph now specifies the constant family; the standard
gate's inventory is corrected from seven pinned x86 custody lines to nine and
from four ratchet suites to the seven the tree actually has (202 tests); and
DESIGN section 3's phase cells are reconciled - AC-5 is P5a, and AC-2's
"clone admission" half is split into what P3/#587 landed versus what P5b still
holds on #575.

Co-Authored-By: Ryan Breen <rbreen@getfastr.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ryanbreen added a commit that referenced this pull request Aug 19, 2026
… ledger row, tranche 2 complete

Closes #579, completes #527's creation-path remainder. Ledger row 5
lands as ONE PR per rule 5 (ownership-alone revert conflicts; whole-series
revert clean). Tranche 2 (P3 #587, P4 #601, P5a #590, P5b #595) is now
complete.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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