Process teardown quiescence: free address space and kernel stack only after the CPU provably leaves them - #417
Merged
Merged
Conversation
Step 0 structurally moves high-value teardown evidence ahead of trace-buffer dumping and claims every postmortem section independently, so a nested abort skips only the section already in progress. It also derives reusable-stack classification from the allocator constants, stamps and reports the owning tid per stack slot, and selects deferred SIGSEGV cleanup from the faulting frame's stamped owner rather than the scheduler's current idle identity. This establishes the diagnostic and victim-selection invariants by construction; it does not prove the teardown crash resolved. The full QEMU and Parallels empirical gates remain pending until Steps 1 and 2 are complete. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Step 1 makes the impossible return states unrepresentable on the covered paths. User context restore now forces EL0t mode. The inline schedule path takes option (a): it stores the kernel resume mask in Thread.inline_schedule_spsr, leaving context.spsr_el1 paired with the saved user ELR. Both Rust idle redirects select kernel TTBR0 through next_cr3 and clear saved_process_cr3. The sync, IRQ, and syscall epilogues reject EL1 returns below KERNEL_VIRT_BASE and record the rejected ELR/SPSR in fixed per-CPU slots using branch-only stores. These are construction-level privilege, context-pairing, and idle-TTBR guarantees. They do not yet prove the teardown crash resolved; the requested QEMU and Parallels empirical gates remain pending until Step 2 is complete. Gold-master adjacency: I read docs/planning/cpu0-user-guard-autopsy/README.md before making this change. The guard widening is adjacent to a gold-master return region and requires PR signoff from the project owner before merge. This change did not touch aarch64_enter_exception_frame, ISB placement, ERET instruction placement, or ERET ordering; only the existing pre-ERET branch conditions and branch-only per-CPU record stores changed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Structurally install the kernel TTBR0 before exit, exec, and fault teardown, and clear the syscall return TTBR shadows before process exit can retire the old root. Replace sys_exit's terminal WFI with the existing inline scheduler pivot; the exit trampoline publishes Terminated only after assembly has moved SP to the per-CPU scheduler stack, then dispatches a successor. Land the specified interim reclamation design: reject any reusable stack slot still named by an online CPU's kernel_stack_top or containing its user_rsp_scratch, and require a post-termination scheduler epoch from every online CPU. Keep the unconditional 64 KiB handout scrub and assert that the selected slot is not live. The full Thread.on_cpu model was not used because clearing it honestly after ERET would require new hooks in the frozen exception-return tails; this interim provides the required live-stack and all-CPU grace gates without changing those tails. These code paths eliminate the teardown-before-pivot and name-only reclamation states by construction. Both release build gates are clean and AArch64 links; the two QEMU SMP=4 boots and Parallels boot gate remain pending, so this commit does not claim the observed crash is empirically fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Require two scheduling-epoch bumps for retired kernel stacks. The bump recorded by a currently in-flight handoff can only reach E+1; reaching E+2 requires a subsequent exception after that handoff's ERET and old-stack register restore have completed. Quiesce the faulting CPU and clear both TTBR0 return shadows before publishing a deferred SIGSEGV exit. Snapshot saved_process_ttbr0 and next_ttbr0 across every online CPU, transfer live page-table resources into an owned pending record, and reclaim them from the fork sweep only after the two-bump grace has elapsed and no shadow retains any retiring root. The same-CPU path remains immediate when local quiescence leaves no peer root live. The allocator-returning operations on exit are cleanup_cow_frames, which deallocates mapped user frames whose CoW references reach zero, and drain_old_page_tables/cleanup_for_exec, which also deallocates old user, table, and root frames. Dropping the active Box<ProcessPageTable> still has no Drop implementation and does not deallocate its L0/L4 or lower-level table frames; that pre-existing leak remains out of scope and is tracked as breenix-mt0. Refcounted lifetime for non-running CLONE_VM owners remains the separate Step 3 tracked as breenix-m25. Validation: both requested x86_64 and AArch64 release builds complete with zero warnings, and the AArch64 kernel links without CONDBR19 failures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ryanbreen
added a commit
that referenced
this pull request
Aug 4, 2026
Teardown closure: quiesce the fault-driven exit path (PR #417 follow-through)
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.
Summary
Fixes an aarch64 SMP teardown race where a terminated thread's kernel stack
and address-space root could be reclaimed and reused while a CPU was still
provably executing on them. The reused 64 KiB stack slot got zero-scrubbed
under a live exception frame, and a still-live TTBR0 root got freed out from
under a peer CPU mid-ERET. Both produce an impossible return frame:
ELR=0x1(garbage return address),
spsr=0x134598(nonsense SPSR), captured live inlogs/parallels-launcher-test/run-20260802-045109/serial-excerpt.txt:1097during a
btermfork/exec teardown — the exact crash class this PR closes.Reconciled mechanism: teardown now frees the kernel stack slot and the
process's page-table root only after the CPU has provably left them, not
merely "probably":
from reuse while any online CPU's
kernel_stack_top/user_rsp_scratchstill names it, and requires two full scheduling-epoch bumps
(
saturating_add(2), not1) past termination. One bump can be recordedby the very handoff still tearing the thread down (the epoch counter is
bumped at the top of
check_need_resched_and_switch_arm64, beforedispatch_thread_lockedrepoints the stack pointers and before theboot.Sepilogue finishes reading the old stack) — two bumps guarantees asecond, later exception entry, which cannot happen while the first
handoff's ERET/register-restore is still in flight.
or a normal
sys_exitpublishes teardown, the faulting/exiting CPU callsquiesce_ttbr0_for_exit(), installing the kernel TTBR0 locally andclearing both
saved_process_ttbr0/next_ttbr0shadows. Reclamation thensnapshots those shadows across every online CPU (
is_ttbr0_root_live)and only frees the root once no peer CPU retains it and the two-epoch
stack grace above has elapsed. The same-CPU path stays immediate when local
quiescence already leaves no peer holding the root.
sys_exit's terminalWFIwas replaced with the existing inline schedulerpivot, so
Terminatedis published only after assembly has moved SP offthe dying stack onto the per-CPU scheduler stack and a successor has been
dispatched.
STEP 0 / 1 / 2
120eb569, diag): postmortem-evidence ordering — moveshigh-value teardown evidence ahead of trace-buffer dumping, claims every
postmortem section independently (a nested abort skips only the section
already in progress), derives reusable-stack classification from allocator
constants, stamps/reports the owning tid per stack slot, and selects
deferred SIGSEGV cleanup from the faulting frame's stamped owner instead of
the scheduler's current idle identity. Record-only; does not change
teardown ordering.
0dc2f440, fix): makes the impossible return statesunrepresentable on the covered paths — user context restore forces EL0t
mode; the inline-schedule path stores the kernel resume mask in
Thread.inline_schedule_spsr(leavingcontext.spsr_el1paired with thesaved user ELR); both Rust idle redirects select kernel TTBR0 via
next_cr3and clearsaved_process_cr3; the sync/IRQ/syscall epiloguesreject EL1 returns below
KERNEL_VIRT_BASEand record the rejectedELR/SPSR in fixed per-CPU slots using branch-only stores (no UART on the
hot path).
b34448a9+6f66f37f, fix): the actual quiescence-before-reclamation machinery described above (kernel-TTBR0 install before exit/
exec/fault teardown, two-epoch stack grace, cross-CPU TTBR0-shadow scan
before root reclamation, deferred-reclaim list drained from
sys_fork).6f66f37fis a codex fix-forward round closing two review-blocking holesfound in
b34448a9: (1) the epoch race described above (saturating_add(1)→
saturating_add(2)), and (2)drain_deferred_fault_sigsegv_exits()draining any CPU's buffer and tearing down the page table with no
cross-CPU quiescence — closed by the
quiesce_ttbr0_for_exit+is_ttbr0_root_livedesign above.Gold-master adjacency signoff
Step 1 widens the pre-ERET return-privilege guard conditions, which sit
immediately adjacent to the gold-master-frozen ERET/dispatch regions called
out in
docs/planning/cpu0-user-guard-autopsy/README.md. Per that autopsy'srequirement, the design was read before editing, and this change does
not touch
aarch64_enter_exception_frame, ISB placement, ERETinstruction placement, or ERET ordering — only the existing pre-ERET branch
conditions and branch-only per-CPU record stores. Independently re-verified
at final HEAD
6f66f37fviagit diffagainst2b98725d:idle_loop_arm64,the EL0-dispatch banner,
aarch64_enter_exception_frame(incl. ISB/ERETordering),
gic.rs's SGI-enable block,timer_interrupt.rs, and everyTier-1 x86_64 file are byte-identical / untouched. This still needs
project-owner signoff on the guard widening before/at merge, per the
autopsy's own requirement — flagging explicitly rather than asserting it
away.
Gates (honest numbers)
at exact HEAD
6f66f37f(aarch64 also links clean, noCONDBR19relocationfailures) immediately before opening this PR.
reports "Userspace not detected" on this branch, but that reproduces
identically on baseline
2b98725dafter anext2rebuild (trackedpre-existing gap:
bd breenix-5up,/bin/bwmspawn EIO stalls theuserspace-completion marker even on a healthy kernel boot). No
UNHANDLED_EC/DATA_ABORT/INSTRUCTION_ABORT/FATAL_POSTMORTEM/panic markers on
any attempt. One ad-hoc QEMU smoke boot at
6f66f37fdid complete fully:4 CPUs online, init/bsshd/heartbeat/xhci_counters spawn,
xhci_countersexits(0) cleanly exercising the modified exit path, 20+s with no fault.
50,000–65,000+, 105–120 heartbeats each, zero fault markers, VMs
force-stopped and verified stopped).
(
--no-build, fresh VM each, screen unlocked + VM-stopped verified beforeevery attempt). 13/14
RESULT: PASSclean (inject_retries=0); 1/14(attempt 7)
ENV: HOST_INPUT_WEDGE— a documented host-side ParallelsUSB-HID dispatcher wedge (probe key never incremented in 10s), not a kernel
fault and not screen-lock; Breenix was not exercised that attempt per the
harness's own classification. Zero kernel-fault markers across all 14
attempts. Streak sequence: attempts 1–6 clean (streak 6), attempt 7 the
environmental break, attempts 8–14 clean (streak 7).
cleanStreakMax=7;the 10-consecutive target was not reached — mathematically foreclosed
once attempt 7 broke the streak with only 7 attempts left. Per the literal
rubric (which names screen-lock specifically as the allowed environmental
exception, and this was a different cause) this reports RED, while
noting the underlying kernel fix showed 0/14 kernel faults — the miss is
test-harness/host environmental noise, not evidence of a regression.
2b98725d, a 90-minutesoak was clean (no UNHANDLED_EC/DATA_ABORT/panic/POSTMORTEM/CPU0 ALARM),
confirming baseline health. In the same session, a follow-on
final-acceptance launcher streak then hit a kernel fault on attempt 1/14
— cascading INSTRUCTION_ABORT/EL1_INLINE_ABORT/FATAL_POSTMORTEM during
btermfork/exec,inject_retries=0(not a harness artifact), producingthe
ELR=0x1/spsr=0x134598impossible frame referenced above. Thatfault is what motivated this branch.
Remaining follow-ups (not fixed here, tracked separately)
bd breenix-5up— pre-existing/bin/bwmspawn EIO on native SMP=4aarch64 boot prevents the userspace-completion marker even on an otherwise
healthy boot; reproduces on baseline
2b98725dtoo, unrelated to this fix.a transient ~4-minute window (~04:43–04:47 EDT) saw the CPU0 timer-tick
rate drop from a steady ~500 Hz to ~78–90 Hz before recovering to ~624 Hz.
No alarm fired and ticks never stalled, but flagging given this project's
CPU0-timer fragility history. Not reproduced or investigated further here.
Box<ProcessPageTable>has noDropimpl and does not deallocate its L0/L4 (or lower) table frames onthe active-process path (pre-existing leak, tracked as
breenix-mt0);refcounted lifetime for non-running
CLONE_VMowners is the separateStep 3 (
breenix-m25).dump_all_eret_frame_anomaly_snapshotsprints the raw slot-encodedLAST_DISPATCHED_TIDasowner_tid_canary(decodes to a garbage tid inpostmortems);
Thread.inline_schedule_spsris currently write-only (noreader yet);
SCHEDULING_EPOCHSis 8×AtomicU64sharing one 64B cacheline, RMW'd by every CPU on every exception return (false-sharing on the
hottest path); the EL0 arm of the old
elr<0x1000guard was dropped; the(spsr&0xF)!=0clause of the[BUG] dispatch_threadguard is now deadcode.
Co-Authored-By: Claude Fable 5 noreply@anthropic.com