feat(teardown): P3 - exec detach + clone/exec admission - #587
Merged
Conversation
`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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Revert story (one story, one PR)
Quoted verbatim from
docs/planning/teardown-unification/PLAN.md(Phase 3, "Revert — one story, one PR"):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 thisbranch's diff.
git reset --hard HEADrestores the tree afterward (verified clean). The plaingit revert --no-commit main..HEADform errors on a merge commit within the branch (is a merge but no -m option was given), which is expected — the--no-mergesform is the equivalent check. Re-runagainst 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 (artifactT2-b):This clears the two stale-identity fields (
inherited_cr3,thread_group_id) at every exec commitpoint, preserving them on every exec failure path, and adds a
Creatingdispatch arm plus aparent-
Liveadmission check with non-runnable publication — restoring a three-arm dispatch (theCreatingarm was previously dormant/two-arm per #570) so a row can no longer be dispatchedmid-creation, and closing the wrong-victim-after-exec window where a stale
inherited_cr3/thread_group_idpair 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 --checkexits 0, andgit revert --no-commit --no-merges main..HEADexits 0 with no conflicts. Single-revert-story property intact: round 3 added one commitand 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:
preservation (the failure-path preservation claim is proven for the exec paths exercised, not for
every allocation-failure arm).
stale key from a fresh one under the current oracle.
Two more split by architecture, both blocked on #567:
Creatingrefusal is structurally blocked — x86 doesn't have an equivalent real-dispatch path to drive it
through today, tracked as x86: kernel-thread resume corrupts saved context in the pre-userspace boot window (garbage RIP / NX fetch / write-protection faults) #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
FAILcondition was weakened anywhere; the only gate-script change since r2 addstwo 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 tomain. Tier-2 edits (kernel/src/interrupts/context_switch.rs,kernel/src/arch_impl/aarch64/context_switch.rs) add no hot-path allocation/formatting/blocking: thex86 arms are a field read + a relaxed atomic + a once-only swap-gated raw-serial breadcrumb matching
the neighbouring
NO_CR3arm's existing pattern, and the aarch64 arm logs nothing. No over-freesurface:
boot_restore_process_resourcesrefuses unless the row is exclusively empty and movesownership, 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):
mainrebuilt fresh from a clean worktree at724ad2f4ran 100/100; branch at6223d0d9ran 100/100; zero failures on either side.wakeLossBranchOnly=false. No new unfiledsignature 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
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 wascorrected, 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):
fix(exec): detach the exec'd row from its pre-execCLONE_VMthread group (the phase's coremechanism).
fix(clone): admit clones only into a live parent, and refuse unpublished rows at dispatch (thephase's second mechanism).
fix(sched): emit the exec lock-order markers atomically so nothing can tear them.fix(exec): restore TTBR0 when an aarch64 exec fails (a correctness bug found while proving theexec-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 aheadof 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.