Repository navigation
test(mm): probe shared-fault races and fail-stop same-CPU lock reentry - #86
Open
YUZHEthefool wants to merge 7 commits into
Open
YUZHEthefool wants to merge 7 commits into
YUZHEthefool wants to merge 7 commits into
Conversation
handle_shared_page_fault try-locks PROCESS_TABLE, the faulting task's PCB and its MmState, and returns Busy when any of them is taken. If the context the fault interrupted on the same CPU holds one of them, the retry through IRETQ can never succeed, and the guarded usercopy path retries with IF=0, so the CPU livelocks silently. Only PT_LOCK had an owner record to tell that case apart. Add OwnerCpuMutex, a spin mutex that records its holding CPU like PT_LOCK_OWNER_CPU, and use it for those three locks. A failed try-lock whose owner is the current CPU now returns SharedFaultResult::Reentry, and the #PF arm panics with "same-CPU <lock> reentry during shared fault". Contention from another CPU stays Busy. The owner record is the CPU's SYSCALL_PERCPU slot address, read from IA32_GS_BASE (or IA32_KERNEL_GS_BASE while a Ring-3 entry still has the user base live), not cpu_local::current_cpu_id(). That function reads the LAPIC ID over MMIO, and doing so on every acquisition of these hot locks slowed 16-vCPU TCG guests enough that a hang already present on main struck within a few rounds of the new final-owner leg; with the MSR token that leg completes there. Plan item: ST-K2-P2
The shared_fault_reentry_probe image boots a musl guest that maps one shared-anonymous page and asks the kernel, through a probe-only prctl, to fault it in while holding PROCESS_TABLE, its PCB or its MmState. The panic handler of this image leaves QEMU through isa-debug-exit, so the fail-stop ends the guest with status 35 while a kernel that retries the fault as contention spins until the timeout. scripts/gates/qemu/shared_fault_reentry_probe.py runs one fresh guest per lock and requires status 35, the HOLD marker once, then exactly one panic from the page-fault handler naming that lock, and no other terminal marker. Its verdict has mutation tests. Plan item: ST-K2-P2
…ault When a CPU in munmap holds PT_LOCK and waits for shootdown ACKs, a sibling of the same address space that takes a shared fault from a guarded usercopy gets Busy and retries with the saved IF=0. It cannot take the shootdown IPI, so the holder's wait finishes only because the #PF Busy arm drains the local mailbox. Nothing exercised that path. Add a syscall_test-only hook that pauses munmap Phase 2, with PT_LOCK held, right before its range shootdown until a Busy has been seen on an armed cold shared page while another CPU owns PT_LOCK. Both #PF Busy arms now call mm::tlb_shootdown::drain_from_fault(), the same drain, which in syscall_test builds counts the drains that delivered an ACK (new mm feature syscall_test, forwarded from kernel and kernel_core). The Ring-3 leg runs the holder on CPU 0 and a CLONE_VM sibling on CPU 1 that copies uname into the cold page. Each round needs munmap to return 0, the copy to finish, the hook to complete, Busy >= 1 and drain ACKs >= 1. NILIX_ST_K2_REMOTE_REPS sets the rounds at fixture build time. The ring3-mm oracle requires the leg and exactly one "ST-K2-REMOTE-HOLDER-ACK PASS reps=<n>" marker on SMP. Plan item: ST-K2-P2
…nmap The last address space to drop a shared-anonymous region frees its frames and refunds its charge. Nothing checked that when the final owner is a fork child exiting after its parent unmapped first, or a CLONE_VM thread exiting while the parent unmaps, with another shared region churning on the other CPU. The Ring-3 leg runs both variants, the other owner pinned to CPU 1 on SMP, NILIX_ST_K2_FINAL_OWNER_ITERS times each (default 200) after one warm-up. After every round memory.current and the buddy's free pages must come back to the baseline with no tolerance. The page tables allocate_kernel_stack builds for a new PID slot are never freed, so free pages are compared together with a new syscall_test count of those tables (prctl op 6); one leaked data or table frame still fails. The ring3-mm oracle requires one "ST-K2-FINAL-OWNER PASS" marker per variant at the configured iteration count. Plan item: ST-K2-P2
MmState::settle_failed_populate (LEAD-ROLLBACK-PRUNE-LEDGER-1) debits the ledgered page tables a failed population's rollback prunes, including one a shared fault built. It had no guest evidence: the only injected failure is the boot-armed 16387-page probe, which fires once. Add a re-armable syscall_test failure of the next private population of an exact page count in the arming address space (prctl op 7), at its last data frame like the R192-RES-2 probe, optionally paused first with no lock held. The guest leg maps a shared page at 160 GiB, faults it in, and fails a 515-page population beside it three times each for mmap and for mremap growth: ENOMEM exactly, memory.current unchanged by the failed call, and back to the baseline after the shared munmap. On SMP a third case pauses the population while a CLONE_VM sibling unmaps the shared page; the rollback then empties and prunes the shared-fault table itself, and memory.current returns to the baseline only if settle_failed_populate debits it. brk is not covered: its 2 MiB block cannot also hold a shared page. Plan item: ST-K2-P2
Rebuilt on the dev host with the pinned nightly from the userspace source of the previous commit, with no NILIX_* build-time overrides: one remote-holder round and 200 final-owner iterations per variant. Plan item: ST-K2-P2
Command, exit status and identity (revision, environment, log and serial hashes, notes) for every gate run on the dev host. L0/L1 ran at the branch head. The guest gates ran at 8f9d736, whose bare-metal kernels are byte-identical to the head's (kernel-identity.sha256). The fa4b588 runs are the 16-vCPU diagnostics behind the MSR owner token. Logs stay on the dev host; README.md indexes the files. Plan item: ST-K2-P2
6 tasks
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.
Problem and outcome
The shared-anonymous fault paths had no guest evidence under real concurrency, which kept P0-A strict qualification open (#61). This PR adds the four probes from the #61 execution plan, each with a guest oracle that fails when the property it checks is broken. It also fixes the one defect probe (a) is meant to catch.
handle_shared_page_faulttry-locksPROCESS_TABLE, the faulting task's PCB and itsMmState, and returnsBusywhen any of them is taken. OnlyPT_LOCKrecorded its owner. If the context the fault interrupted held one of the other three on the same CPU, the fault retried through IRETQ forever: a silent livelock, with IF=0 on the usercopy path. The three locks now record their holding CPU, and that case fail-stops withsame-CPU <lock> reentry during shared fault.PT_LOCKbefore its range shootdown receives its acknowledgement from aCLONE_VMsibling spinning on an IF=0 Busy fault. The ACK can only arrive through the #PF Busy arm's mailbox drain.CLONE_VMthread, exiting while the parent unmaps the region and churns another one.memory.currentand the buddy's free pages return exactly to baseline after every round, 200 rounds per variant.memory.current. On SMP, a third case makes the rollback itself prune the shared-fault table, so onlysettle_failed_populatecan release its charge.Related issue or audit finding
Closes #61 once the maintainer confirms at merge review that P0-A strict qualification closes (see "Decisions needed").
Plan item: ST-K2-P2
Design record: Zero-kernel/Nilix-docs-private#2 (
design/st-k2-p2-race-probes-design.md). It covers each race window, how the hook forces it, the exact markers, and the 16-vCPU finding below (§6.1).Design and scope
One commit per probe, plus the fixture refresh:
fix(mm): stop on a same-CPU lock reentry in the shared faultkernel_core::owner_lock::OwnerCpuMutex<T>is aspin::Mutexplus an owner record, set after acquiring and cleared in the guard'sDropbefore release, mirroringPT_LOCK_OWNER_CPU. It is now the type ofProcessArc,Process.mmandPROCESS_TABLE. A failed fault-path try-lock owned by the current CPU returnsSharedFaultResult::Reentry(lock), and the #PF arm panics next to the existing PT assertion. Contention from another CPU staysBusy. Two hosted tests.test(mm): probe same-CPU lock reentry in a dedicated guest imageshared_fault_reentry_probe(theopen_fault_probepattern). A musl guest maps one untouched shared page and asks, through a probe-only prctl, to fault it in while the kernel holds the chosen lock. In this image the panic handler exits QEMU throughisa-debug-exit(status 35), so a fail-stop can be told apart from a hang.scripts/gates/qemu/shared_fault_reentry_probe.pyruns one guest per lock. Its verdict has mutation tests.test(mm): prove a remote page-table holder gets its ACK from a Busy faultsyscall_testhook pauses munmap Phase 2,PT_LOCKheld, before the range shootdown. It waits until a Busy on the armed cold page has been seen while another CPU ownsPT_LOCK. Both #PF Busy arms calltlb_shootdown::drain_from_fault(), the same drain as before; insyscall_testbuilds it counts the drains that delivered an ACK. Newmmfeaturesyscall_test.test(mm): race a shared region's final owner against a parent-first unmapsyscall_testcount of those tables. The check stays exact: one leaked frame fails it.test(mm): fail private populations beside a shared-fault page tablesyscall_testpopulation failure at an exact page count, optionally paused first with no lock held. It uses 515 pages, so it never prints anR192-RES-2marker or consumes the boot-armed 16387-page probe.test(userspace): refresh the syscall fixture for the ST-K2-P2 race legsThe owner record is a per-CPU token read from MSRs, not
current_cpu_id(). The token is the CPU'sSYSCALL_PERCPUslot address. It comes fromIA32_GS_BASE, or fromIA32_KERNEL_GS_BASEwhile a Ring-3 entry still has the user base live. A user GS base is always in the user half (validate_user_segment_base), so user code cannot forge a token. Hosted builds usecurrent_cpu_id(), becauserdmsrfaults in a user process. The first version recordedcurrent_cpu_id(), which reads the LAPIC ID over MMIO on every acquisition of these hot locks. Under TCG at 16 vCPUs that made a hang already present onmainstrike within a few rounds of the (c) leg, 3 runs out of 3. With the owner-lock commit reverted, or with the store kept but no MMIO read, (b), (c) and (d) completed instead (details below and in the design record §6.1).Deviations from the handoff text, with reasons in the design record:
PROCESS_TABLEalso records its owner. The fault try-locks it first, so a holder of it livelocks the same way.result=enomemmarkers and break the R192-RES-2 oracle's "exactly one" check.Non-goals: no new memory features.
Kernel safety and invariants
syscall_testandshared_fault_reentry_probebuilds. Production kernels gain the owner record and the fail-stop, nothing user-reachable. The owner token cannot be influenced from user space; see above.unsafe/ MMIO / page-table / usercopy justification: one newunsafeblock readsIA32_GS_BASEandIA32_KERNEL_GS_BASE, which have no side effects and exist on every x86-64 CPU. The probe image's isa-debug-exit write sits in the panic handler's existingunsafeblock.PT_LOCK_OWNER_CPUmakes. The record is set after acquisition and cleared before release, so it never names a CPU that has released the lock. Lock order is unchanged. The (b) pause holdsPT_LOCKwith IF=0, the (d) pause holds no lock with IF=1, and both are TSC-bounded likecow_test.MmStatemutexes grow by oneAtomicUsize; the existingsize_ofassertion still holds.Validation evidence
Dev host (QEMU 6.2, TCG, 40 cores),
CI_GUEST_RETRIES=0. Baseline for comparison:main @ f998237(P0.1, handoff §9). Logs and serials stay on the host under/tmp/nilix-evidence/st-k2-p2/<sha>/..validation/st-k2-p2/records each command, status and log hash.The guest gates ran at
8f9d736. It differs from the head99267d9only in whichcfgguards the MSR read inowner_lock.rs: target instead of feature, a fix for a hosted SIGSEGV inipc-robust-futex. The bare-metal kernels are byte-identical: default759d0e09…,syscall_test24274492…at both commits (.validation/st-k2-p2/kernel-identity.sha256). L0/L1 ran at99267d9.fa4b588and8f9d736are local revisions from before the owner-token fixes were folded into the first commit; they are not on the remote, and the evidence files keep their names.Probe gates:
Markers in every passing run:
ST-K2-FINAL-OWNER PASS variant=process|thread iterations=200once each, andST-K2-PRIVATE-ROLLBACK PASS site=mmap|mremap cycles=3once each. On SMP, alsoST-K2-REMOTE-HOLDER-ACK PASS reps=<n>andsite=mmap-concurrent cycles=3.The 16-vCPU finding, at the first version
fa4b588(the MMIO owner read). Each run skipped only the known-hangingtest_selective_wait_nested_zombiecall:Observed, not caused by this PR: in one (d) negative-control guest at
fa4b588, run while six TCG lanes loaded the host, the existing leg "COW sibling cold read during the paused PTE replacement" failed with host status 0xc8. That leg waits inside a TSC-bounded pause. It passed in every other run, including the same negative control at8f9d736.Compatibility, performance, and rollout
Two
rdmsrat most and one atomic store per acquire and release of the PCB,MmStateand process-table locks. 8-vCPU ring3-mm took 846 s at8f9d736, against 1076 s with the MMIO version. The hosted count is now 640 per profile. README and README_zh still say 632, and #84 changes those lines to 638, so they need a follow-up to 640 after #84 merges. They are left alone here to keep the two PRs off the same files.Decisions needed
main. The guest stops intest_selective_wait_nested_zombie, and when that leg is skipped it stops in a later one. That hang is the separate 16-vCPU item from the P0 baseline (handoff §9) and is not addressed here. The 16-vCPU evidence for the new legs therefore comes from diagnostic runs with that one call removed.Checklist
unsafeblocks have localSAFETYreasoning and a minimized unsafe scope.