P4 (T2-c): kernel-stack single ownership + creation-path lock order (#579, #527 remainder) - #601
Merged
Merged
Conversation
… two-epoch grace (#579) Move kernel-stack ownership from process-table rows into scheduler publication copies, then reclaim those copies only after every CPU crosses the two-epoch fence. Unmap x86 stack pages, release frames, return per-PID slots, and refuse live-slot reuse so the freed-row path cannot free a running stack. Co-Authored-By: Ryan Breen <rbreen@jrni.com>
…publication (#527 remainder) Extract scheduler-owned thread values while the process-manager guard is held, then let the guard fall before calling every creation-path scheduler publication. Instrument the shared publication seam so boot tests can prove the PM-to-scheduler lock nesting is gone and that the detector itself can fire. Co-Authored-By: Ryan Breen <rbreen@jrni.com>
…C-8) Add the AC-8 boot oracle for ownership classification, pool reuse, publication transfer, and post-grace reclamation on both architecture profiles. Register and launch it with the teardown suite, and align existing teardown evidence with kernel-stack frame release so the new ownership behavior is measured honestly. Co-Authored-By: Ryan Breen <rbreen@jrni.com>
…er (#579, #527) Ratchet the structural checks around single stack ownership, two-epoch reclamation, and process-manager guard release at every creation publication site. Pin both x86 and AArch64 QEMU gates to the ownership and lock-order oracle markers so regressions cannot pass through missing or vacuous evidence. Co-Authored-By: Ryan Breen <rbreen@jrni.com>
…findings (#579) Correct the P4 architecture-cfg matrix and narrow the coverage language to the evidence the gates actually provide. Record the ownership and creation-lock-order vacuity findings and their ratchets so the plan and design describe the shipped guarantees without overstating them. Co-Authored-By: Ryan Breen <rbreen@jrni.com>
…ruth (B2) PLAN.md:470-476 still carried the pre-branch 222-test inventory even after the P4 doc commit; teardown_structure, context_restore_structure, exec_lock_order_structure and serial_line_atomicity_structure all grew on this branch. Re-measured all ten suites via `cargo test --test <name> -- --list` (256 total) and updated the counts in place. Companion fixes to PR-BODY.md/p4-impl-notes.md (out-of-repo scratchpad) correct the same 239->251 error to 222->256, and p4-mac.md/PR-BODY.md attribute the branch's first red aarch64 full-test run to #589 with the serial-loss admission (B1). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… 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.
P4 (T2-c): kernel-stack single ownership + creation-path lock order (#579, #527 remainder)
Revert story (quoted verbatim from
docs/planning/teardown-unification/PLAN.md)Ledger row 5 of
docs/planning/teardown-unification/PLAN.md§0 (18 rows, unamended — see the seamadjudication below). Closes #579. Completes #527's creation-path remainder (#527 itself stays
closed and is referenced, not reopened). #546 (user-stack
GuardedStackreclamation) is untouchedand does not substitute for AC-8. This lands tranche 2's fourth and final phase (P3 #587, P5a #590,
P5b #595 already landed) — merging this PR completes tranche 2 of the teardown-unification plan.
1. What was wrong
Five sites in
kernel/src/process/manager.rsdidBox::leak(Box::new(kernel_stack));and storedkernel_stack_allocation: None— a permanent per-process kernel-stack leak on the primary creationpaths of both architectures and on both x86 fork paths. The reason was structural: threads live
in two places (the process-table row and the scheduler's own
Box<Thread>), publication is aThread::clone, andThread::clonecannot clone aKernelStack.Three further defects were found while building the fix, and are fixed here:
Scheduler::reclaim_terminated_threads,RetirementGrace,retirement_grace_elapsedandis_kernel_stack_slot_livewere#[cfg(target_arch = "aarch64")].Transferring ownership to the scheduler copy on x86 without them would have handed the stack to an
object that is never reclaimed.
KernelStack::dropreturned only the bitmap bit — no unmap, nodeallocate_frame. Each reuseof a freed slot mapped 128 new frames over the same VA range and orphaned the previous 128, and
map_kernel_pagesilently overwrote the present PTE that made it possible. A bounded per-processslot leak would have become an unbounded per-reuse frame leak.
debug_assert!and there is no[profile.release]section anywhere in this tree, so
--release— every gate build — compiled it out. It had nevergated anything, and x86 had no such predicate at all.
2. What this does
Ownership.
Thread::publish_to_scheduler()is the single API that publishes a row thread: it clonesand moves
kernel_stack_allocationto the published copy. The scheduler copy is the single owner; therow's copy holds
Nonebecause ownership moved, not because it leaked. All fiveBox::leaksitesare gone, and the two pre-existing hand-written transfers (
syscall/clone.rs,aarch64/syscall_entry.rs) now go through the same API so the census has one shape to anchor on.Grace. The two-epoch retirement grace is now architecture-neutral, with x86 callers in
idle_loopand on the x86 fork path. Reclaimed
Box<Thread>s are dropped after the scheduler guard and theinterrupt-disabled region end, because x86's
Dropnow takes the frame-allocator lock.Frames. x86
KernelStack::dropunmaps the stack's VA range and releases its 128 frames — but onlybehind a liveness refusal: a slot an online CPU still names is counted and leaked, never freed
(
drop_refused_live).map_kernel_pagerefuses, counted, to overwrite a PRESENT PTE inside thekernel-stack range.
Live-slot guard. A real release-mode, counted check on both arches: if the allocator selects a
slot a CPU still names it releases the bit and fails the allocation at the source.
Lock order. The process-manager guard is released before every scheduler publication, at six
sites (the plan named three;
test_exec.rscarries three more of the identical class), and thepublication seam (
spawn/spawn_front/spawn_as_current) now detects a publication made whilethis CPU holds the process-manager lock.
Dead code. Both
#[allow(dead_code)] spawn_userspace_from_elfimplementations are deleted withtheir two user-stack
Box::leaksites (verified dead: the only textual references outside thedefinitions are in a stale build log).
complete_fork's stale#[allow(dead_code)]is replaced withhonest gating — both its callers live inside
#[cfg(feature = "testing")]blocks, so the function isgenuinely absent without that feature.
3. Evidence (AC-8)
One oracle,
[KSTACK_OWNER_ORACLE:<arch>:…], launched on both profiles through all five links(definition → aarch64
TestDef→ x86run_x86_…_gate()→ gate-script pin → launch ratchet), 33 fields,every one asserted.
aarch64 (
[BOOT_TESTS:PASS], 106/106):x86 (beast,
USERSPACE TEST COMPLETE,TEST_TALLY: exited=100 nonzero=0 failed=[]):allocated == freed,both
>= 1000, never a vacuous zero — on unmodifiedmainthe same workload cannot complete: itexhausts the 481-slot (aarch64) / 254-slot (x86) pool. Mutation M1 reproduces exactly that.
frames_mapped_delta == frames_released_delta == 128 × slot_alloc_delta == 128000, andframe_used_deltabounded strictly below one stack's 128frames. aarch64's zeros are asserted as legitimately zero (HHDM-preallocated, no frames mapped).
creation rows plus a fork arm driving both x86 fork helpers / the aarch64 one, plus production
counters over every
publish_to_scheduleron the boot (pub_sched_owned == pub_pooled,pub_row_residual = 0,pub_unowned = 0).slot_returns_exact_onein the new oracle, andkstack_returns=64on the P0/P2 per-PID pairing oracle (kstack_returns == children, asserted).Box::leak(Box::new(acrosskernel/srcas(file, item, count)triples with anempty expected set, paired with a non-empty companion census over the four
Box::leak(v.into_boxed_slice())sites so the machinery is proven able to see rows.4. Mutations (each turned a real gate RED, then was reverted)
Box::leaksitescreation_rows=939:zero_owner=470:balance=555test_disk.rs's publication under the PM guardexec_lock_order_structurecreate_user_processpublicationSCRIPT_EXIT=1, 4 ×[CREATION_LOCK_ORDER:VIOLATION:PM_HELD]is_kernel_stack_slot_liveon aarch64live_refusals_injected=0, "live-slot guard accepted the injected live stack"publish_to_schedulercopy instead of moveone_owner=0:pub_row_residual=1073:balance=1014Box::leak(Box::new(...))in a new itemteardown_structurekernel/src-wideA vacuity finding this PR fixes. M2's runtime half came back GREEN the first time:
run_userspace_from_disknever executes in the aarch64 boot-test profile, so a counter asserted at zerohad no driver — precisely the condition-6 vacuity the standard gate forbids. Fixed at source with a
deliberate injection arm that drives the detector through the same
process_manager_held_on_current_cpu()predicate the production seam uses and emits a distinct[CREATION_LOCK_ORDER:INJECTED:PM_HELD], pinned present, while the production[CREATION_LOCK_ORDER:VIOLATION:PM_HELD]is pinned absent. M2′ then proved the production markerfires from a real creation site and reddens the x86 gate.
5. Pinned residuals moved — with the derivation, not a re-pin
EXEC_DETACH_ORACLEstack_residual, x86: 149 → 21. Mechanism: x86KernelStack::dropnowreleases its frames, and
stack_residualis aframe_allocator_used_frames()delta across the oraclebody. Arithmetic stated before the run:
old − new = 128 × (kernel stacks dropped inside the window);a kernel stack is
512 KiB / 4 KiB = 128frames. Observed:149 − 21 = 128 = 128 × 1, to the frame.The oracle now derives it: it measures
kstack_frames_releasedover its own window and assertsEXPECTED_STACK_RESIDUAL_PRE_KSTACK_RELEASE − stack_residual == kstack_frames_released(and% 128 == 0on x86), so a one-frame discrepancy fails with its own message. Measured:kstack_frames_released=128(x86) and=0(aarch64, where18 − 0 = 18is unchanged).Not moved, and stated explicitly:
EXPECTED_LEAF_RESIDUAL(16, both arches) and thecreating-dispatch oracle's
leaf_residual=16:user_stack_residual=16are user-leaf-mapping quantitiesand did not move; #588's counted creation-failure residue did not move either
(
construct_residual=4x86 /=2aarch64, unchanged). The in-code #583 comment block is updated soit no longer claims the kernel-stack component of
stack_residualis a #583 manifestation.PT_RETIRE_COHORT/PT_RETIRE_ORACLEgainkstack_returns=64— derived, not pinned: the oracleasserts
kstack_returns == childrenover its 64-child cohort.6. Seam adjudication (rule 5) — ONE PR, dependency named
Both scopes were built on one branch and rule 5's criterion was tested against the implementation:
sites to call
main_thread.publish_to_scheduler()under a&mutguard and publish after the guarddrops. Reverting ownership deletes that method and leaves those call sites referencing it — a compile
failure. A
git revertdry run confirms it: oldest-to-newest conflicts inprocess/creation.rs,boot/test_disk.rs,test_exec.rsandaarch64/syscall_entry.rs; newest-to-oldest (which is whatreverting the merge commit does) applies cleanly.
Exactly one legal revert order — the same shape that kept P5a at one PR — so the seam does not fire.
Ledger row 5 stands; §0 is not amended (18 rows). Both seam commits nevertheless build independently
on both architectures, so the commit boundary the plan asked for is real.
seamOutcome: CONFIRMED as ONE PR — the whole-series revert applies cleanly onto a tree
byte-identical to
main; reverting the ownership commit alone conflicts inarch_impl/aarch64/syscall_entry.rs,boot/test_disk.rs,process/creation.rs,test_exec.rs.7. AC verdict
All 8 P4 acceptance criteria MET (AC-P4-1 through AC-P4-8) and campaign AC-8 MET, with rulings
R21–R27 all applied as authorised (R21 cfg table + coverage sentence corrected, both profiles gated;
R22 grace machinery de-
cfg'd with x86 callers; R23 unmap + 128-frame release behind a fail-closedliveness refusal, frame steady-state asserted; R24 creation-path lock order moved to the publication
seam; R25 counted release-mode live-slot guard on both arches; R26 seam adjudication above; R27 zero
Box::leak(Box::new(census, item-anchored).8. Gates
testing,external_test_binsandboot_tests,…aarch64-breenix-kernel.json, plain andboot_testsscripts/check-kernel-no-neon.shagainst the booted ELFrun-aarch64-full-test.sh --rebuild --boot-tests-onlyARM64 BOOT TESTS: PASSED, 106/106run-aarch64-boot-test-strict.shrun-aarch64-boot-test-native.shrun-aarch64-service-sequence-gate.sh(default 25/profile, both profiles)575=0 576=0 DATA_ABORT=0 589=21 596=0 P5B=0 GREEN=29 UNATTRIBUTED=0; every #589 occurrence carries the exact pre-adjudicated signaturerun-x86-boot-tests.shSCRIPT_EXIT=0, all eleven pinned custody linesrun-boot-parallel.shSCRIPT_EXIT=0,x86 userspace gate: PASS — exited=100 expected>=100 nonzero=0 allowlist=0run-aarch64-boot-test-strict.sh 20× 3./run.sh --parallels, fresh epoch VM)cargo test --test <name> -- --liston this branch:teardown_structure45→53,context_restore_structure50→61,exec_lock_order_structure25→34,serial_line_atomicity_structure3→9; the other six files unchanged)The x86 script now pins eleven custody lines (was ten) and, for the first time, greps a lock-order
violation marker.
Red-run ledger (review remedy B1). The first
run-aarch64-full-test.sh --rebuild --boot-tests-onlyon this branch failed at Phase 1c (
clonevm_exec_test never completed, 30s timeout) on an unstarvedhost. Attribution: pre-adjudicated #589 (clonevm sibling-exit liveness flake) by exact signature
match — 30s Phase-1c-only timeout, heartbeats continuing normally afterward, no further
CLONEVM_EXEC_TESTline, no other subsystem failure, no DATA_ABORT/#596-class marker. Recorded as acomment on #589 (an unstarved hit is new information relative to that issue's starved-vs-unloaded
table): #589 (comment). The retry (same
binaries, no rebuild) reused the serial path and overwrote the failing run's full serial before it was
archived — only the tool's own last-20-lines excerpt survives (preserved as
step1-full-test.login theevidence bundle); the retry itself passed cleanly with every P4-relevant marker present. This is a
process gap (the retry should have copied the failing serial aside first) and is disclosed, not hidden.
9. Doc repair carried in this PR
cfgcolumn was an authoring error at both2c7b8798andb344e4f2: zero of thefive sites are "both". The split is 3 x86-only / 2 aarch64-only, so both profiles are
mandatory and neither alone covers the surface. PLAN and DESIGN corrected, anchors re-based to
b344e4f2.kernel/src-wide(file, item, count)census.the stress asserts frame steady-state.
creation-class marker with an injection arm replaces it; the creation-site count is six, not three.
all ten suites via
cargo test --test <name> -- --liston this branch (256 total) and correctedPLAN.md:470-476, which still carried the pre-branch 222-test inventory, to the measured truth.10. Disclosed, not hidden
Both aarch64 kernel builds emit Cargo's cached future-incompatibility summary for the pinned
toolchain's own sysroot
coreunder-Z build-std. It reproduces byte-identically onmain@b344e4f2, is not this change's doing, and was not suppressed:.cargo/config.tomlisbyte-identical to
main(an attempt to silence it with[future-incompat-report] frequency = "never"was reverted). Its fix is a nightly-pin migration, filed separately.
Closing: every leg of the evidence battery is green; only pre-adjudicated signatures observed
(#589 × 21 across the service-sequence gate, #555 × 1 in the starved-gate); zero #596-class DATA_ABORT
anywhere; all QEMU/Parallels processes cleaned up. Tranche 2 is complete.
Co-Authored-By: Ryan Breen ryan@ryanbreen.com
Co-Authored-By: Claude Fable 5 noreply@anthropic.com