Skip to content

Commit 6d9392a

Browse files
riccajasonclaude
andcommitted
riscv: stop teardown freeing shared kernel tables; wire kernel satp root
Two RISC-V process-exit memory-safety bugs surfaced by the 2026-06-03 kernel correctness review and confirmed against the code. Both are independent of the page-table-root self-free common to x86_64 + riscv (see "Out of scope"). What: 1. reclaim_process_page_tables (src/memory/mod.rs, the shared aarch64/ riscv arm) walked the full L0 range 0..512. On RISC-V there is no TTBR split, so create_process_page_table copies the kernel-half descriptors (L0[256..512]) into every user table; those point at the shared *static* BOOT_L2_IDENTITY / BOOT_L2_KERNEL tables in arch::riscv64::entry. Walking the upper half therefore reached those shared tables and frame_alloc.free zero-filled them — destroying the HHDM and kernel map that every CPU and every address space depend on (global corruption, strictly worse than a single-process brick). Fix: walk only the user half (0..256), matching x86_64's paging::reclaim_process_page_tables. Harmless narrowing on AArch64 (user L0 upper half is always zero; kernel lives in TTBR1) and a standing guard against a future kernel-half copy there. 2. kmain_riscv64 (src/microkernel/main.rs) never set the canonical kernel page-table root, so kernel_cr3() stayed 0. The portable scheduler substitutes kernel_cr3() as the switch hint for kernel/idle tasks (cr3 == 0); the riscv satp reload gate skips when that hint is 0, so a kernel/idle task kept the previous user satp loaded after the owning process exited — a use-after-free of a freed root. Fix: wire set_kernel_cr3(boot_root_phys) from BOOT_SATP, mirroring the x86_64 (CR3) / aarch64 (TTBR1) wiring already in kmain. Why: x86_64 and aarch64 already wire kernel_cr3() and already walk the user half only; RISC-V was the lone gap on both. Found while adversarially verifying the deferred-reclaim design across all three arches. Out of scope: the active-root self-free common to x86_64 and RISC-V (a dying task frees + zeroes the page-table root still loaded in its own CR3/satp before it yields). That is the deferred-reclaim mechanism, landing next. After this commit RISC-V process exit still bricks on that common path; what changes is that the catastrophic *global* HHDM corruption (bug 1) and the satp UAF (bug 2) are gone. Verification: - make check-all: x86_64 + aarch64 + riscv64 build cleanly. - check-assumptions / check-deferrals / check-unsafe-coverage: pass. The two baseline files carry only line-number resyncs for the 3 (assumptions) + 8 (deferrals) pre-existing entries my added comments shifted; no new bounds or deferrals introduced. - Not boot-tested under QEMU: riscv boot-smoke is not yet in CI, and riscv process exit still bricks on the common active-root self-free above, so a boot would not yet demonstrate a clean exit. Verified by code inspection + the tri-arch build. Staged files: src/memory/mod.rs src/microkernel/main.rs tools/check-assumptions-baseline.txt tools/check-deferrals-baseline.txt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 514fad9 commit 6d9392a

4 files changed

Lines changed: 43 additions & 12 deletions

File tree

src/memory/mod.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,21 @@ pub mod paging {
419419
/// by `ProcessDescriptor::reclaim_user_vmas()`. This function frees
420420
/// only the intermediate page-table structures.
421421
///
422+
/// **Walks only the user half (L0[0..256]).** User mappings live in
423+
/// the low half on every arch (matching x86_64's
424+
/// `paging::reclaim_process_page_tables`, which walks 0..256). The
425+
/// upper half (256..512) is NOT process-owned: on RISC-V there is no
426+
/// TTBR split, so `create_process_page_table` copies the kernel-half
427+
/// descriptors from the boot root — and those point at the *shared
428+
/// static* `BOOT_L2_IDENTITY` / `BOOT_L2_KERNEL` tables in
429+
/// `arch::riscv64::entry`. Descending there and freeing them would
430+
/// zero the HHDM and kernel map that every CPU and every address
431+
/// space depend on (global corruption, strictly worse than a single
432+
/// process brick). On AArch64 the upper half is always zero (kernel
433+
/// lives in TTBR1), so 0..256 is a no-op narrowing there — and a
434+
/// standing regression guard should a future change ever copy the
435+
/// kernel half into an AArch64 user L0.
436+
///
422437
/// Returns the total number of frames freed (including the L0).
423438
pub fn reclaim_process_page_tables(
424439
l0_phys: u64,
@@ -435,7 +450,10 @@ pub mod paging {
435450
// terminated so no CPU has this table loaded.
436451
let l0_virt = phys_to_virt(l0_phys) as *const u64;
437452

438-
for l0_idx in 0..512usize {
453+
// User half only (see the doc comment). The upper half (256..512)
454+
// is shared kernel state on RISC-V (copied static tables) or empty
455+
// on AArch64 — freeing it would corrupt the global kernel map.
456+
for l0_idx in 0..256usize {
439457
// SAFETY: l0_virt points to a 4 KiB page table with 512
440458
// 8-byte entries. l0_idx < 512.
441459
let l0_entry = unsafe { core::ptr::read(l0_virt.add(l0_idx)) };

src/microkernel/main.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,19 @@ unsafe extern "C" fn kmain_riscv64(hart_id: u64, dtb_phys: u64) -> ! {
681681
init_kernel_heap();
682682
init_frame_allocator();
683683

684+
// Capture the boot satp root as the canonical kernel page table, so
685+
// the (portable) scheduler substitutes it for kernel/idle tasks
686+
// (cr3 == 0) and the satp reload gate in arch::riscv64 actually fires
687+
// on a switch onto one. Without this, kernel_cr3() == 0, the gate's
688+
// `if page_table_root != 0` skips the reload, and a kernel/idle task
689+
// keeps the previous user satp loaded after that user process exits —
690+
// a use-after-free of a freed page-table root. Mirrors the x86_64
691+
// (CR3) / aarch64 (TTBR1) set_kernel_cr3 wiring in kmain. BOOT_SATP
692+
// holds `(MODE << 60) | PPN`; set_kernel_cr3 wants the root phys.
693+
let boot_satp = cambios_core::arch::riscv64::entry::BOOT_SATP
694+
.load(core::sync::atomic::Ordering::Acquire);
695+
cambios_core::set_kernel_cr3((boot_satp & ((1u64 << 44) - 1)) << 12);
696+
684697
// Smoke-test: allocate a Box and verify the pointer + stored value.
685698
// Confirms the heap + allocator + paging are all wired correctly.
686699
{

tools/check-assumptions-baseline.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -318,9 +318,9 @@ src/memory/frame_allocator.rs:67:BITMAP_WORDS
318318
src/memory/heap.rs:17:MIN_BLOCK_SIZE
319319
src/memory/heap.rs:20:HEAP_ALIGN
320320
src/memory/mod.rs:77:ENTRIES_PER_TABLE
321-
src/memory/mod.rs:611:BOOTLOADER_BASE
322-
src/memory/mod.rs:614:KERNEL_LOAD_ADDR
323-
src/memory/mod.rs:617:EXTENDED_MEMORY_BASE
321+
src/memory/mod.rs:629:BOOTLOADER_BASE
322+
src/memory/mod.rs:632:KERNEL_LOAD_ADDR
323+
src/memory/mod.rs:635:EXTENDED_MEMORY_BASE
324324
src/pci/mod.rs:36:MAX_PCI_DEVICES
325325
src/policy/mod.rs:75:DECISION_NONE
326326
src/policy/mod.rs:76:DECISION_ALLOW

tools/check-deferrals-baseline.txt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,14 @@ src/ipc/capability.rs:727:deferred
8787
src/ipc/capability.rs:780:TODO
8888
src/ipc/capability.rs:788:for-now
8989
src/lib.rs:100:TBD
90-
src/memory/mod.rs:633:placeholder
91-
src/memory/mod.rs:642:placeholder
92-
src/microkernel/main.rs:930:for-now
93-
src/microkernel/main.rs:1022:for-now
94-
src/microkernel/main.rs:1611:deferred
95-
src/microkernel/main.rs:2029:deferred
96-
src/microkernel/main.rs:2058:deferred
97-
src/microkernel/main.rs:2361:for-now
90+
src/memory/mod.rs:651:placeholder
91+
src/memory/mod.rs:660:placeholder
92+
src/microkernel/main.rs:943:for-now
93+
src/microkernel/main.rs:1035:for-now
94+
src/microkernel/main.rs:1624:deferred
95+
src/microkernel/main.rs:2042:deferred
96+
src/microkernel/main.rs:2071:deferred
97+
src/microkernel/main.rs:2374:for-now
9898
src/platform/mod.rs:353:for-now
9999
src/process.rs:992:deferred
100100
src/process.rs:993:deferred

0 commit comments

Comments
 (0)