From 6d41e485b3564df061765ec04bbd8f9ffd6d3b09 Mon Sep 17 00:00:00 2001 From: Matthew Leach Date: Tue, 21 Apr 2026 13:59:27 +0100 Subject: [PATCH] libkernel: pg_walk: use saturating arithmetic to avoid overflow at last page-table entry The generic RecursiveWalker computed the coverage region for each page-table entry as: VirtMemoryRegion::new(entry_va, table_coverage).intersection(region) For the last entry at the PML4 level (PML4[511]) in the kernel high-half, entry_va + table_coverage wraps past usize::MAX, causing an arithmetic overflow panic inside end_address(). Fix this in walk.rs by computing the intersection bounds with saturating_add/min/max directly. Add regression tests for all levels, ensuring last-element walking is correct. --- libkernel/src/arch/x86_64/memory/pg_walk.rs | 95 +++++++++++++++++++++ libkernel/src/memory/paging/walk.rs | 14 ++- 2 files changed, 106 insertions(+), 3 deletions(-) diff --git a/libkernel/src/arch/x86_64/memory/pg_walk.rs b/libkernel/src/arch/x86_64/memory/pg_walk.rs index a591c63..1d63a3d 100644 --- a/libkernel/src/arch/x86_64/memory/pg_walk.rs +++ b/libkernel/src/arch/x86_64/memory/pg_walk.rs @@ -390,6 +390,101 @@ mod tests { )); } + #[test] + fn walk_last_pml4_entry() { + // The last PML4 entry (index 511) covers [0xffff_ff80_0000_0000, END). + // Computing `entry_va + coverage` used to overflow usize for this entry. + let mut harness = TestHarness::new(4); + + let va = VA::from_value(0xffffffff80000000); + let pa = 0x8_0000; + + harness + .map_4k_pages(pa, va.value(), 1, PtePermissions::ro(false)) + .unwrap(); + harness + .map_4k_pages( + pa + PAGE_SIZE, + va.value() + PAGE_SIZE, + 1, + PtePermissions::ro(false), + ) + .unwrap(); + + let mut was_called = false; + walk_and_modify_region( + harness.inner.root_table, + VirtMemoryRegion::new(va.add_pages(1), PAGE_SIZE), + &mut harness.inner.create_walk_ctx(), + &mut |_va, desc: PTE| { + was_called = true; + assert_eq!(desc.mapped_address().unwrap().value(), pa + PAGE_SIZE); + desc + }, + ) + .unwrap(); + + assert!(was_called); + } + + #[test] + fn walk_last_pdpt_entry() { + // The last PDPT entry (index 511 within PML4[511]) covers the last 1 GiB: + // [0xffff_ffff_c000_0000, END). `entry_va + coverage` overflowed usize here. + let mut harness = TestHarness::new(4); + + let va = VA::from_value(0xffffffff_c0000000); + let pa = 0x9_0000; + + harness + .map_4k_pages(pa, va.value(), 1, PtePermissions::ro(false)) + .unwrap(); + + let mut was_called = false; + walk_and_modify_region( + harness.inner.root_table, + VirtMemoryRegion::new(va, PAGE_SIZE), + &mut harness.inner.create_walk_ctx(), + &mut |_va, desc: PTE| { + was_called = true; + assert_eq!(desc.mapped_address().unwrap().value(), pa); + desc + }, + ) + .unwrap(); + + assert!(was_called); + } + + #[test] + fn walk_last_pd_entry() { + // The last PD entry (index 511 within PDPT[511]/PML4[511]) covers the last 2 MiB: + // [0xffff_ffff_ffe0_0000, END). `entry_va + coverage` overflowed usize here. + let mut harness = TestHarness::new(4); + + let va = VA::from_value(0xffffffff_ffe00000); + let pa = 0xa_0000; + + harness + .map_4k_pages(pa, va.value(), 1, PtePermissions::ro(false)) + .unwrap(); + + let mut was_called = false; + walk_and_modify_region( + harness.inner.root_table, + VirtMemoryRegion::new(va, PAGE_SIZE), + &mut harness.inner.create_walk_ctx(), + &mut |_va, desc: PTE| { + was_called = true; + assert_eq!(desc.mapped_address().unwrap().value(), pa); + desc + }, + ) + .unwrap(); + + assert!(was_called); + } + #[test] fn walk_at_canonical_kernel_va() { let mut harness = TestHarness::new(4); diff --git a/libkernel/src/memory/paging/walk.rs b/libkernel/src/memory/paging/walk.rs index 022722a..39c1f1a 100644 --- a/libkernel/src/memory/paging/walk.rs +++ b/libkernel/src/memory/paging/walk.rs @@ -70,9 +70,17 @@ where }; if let Some(next_desc) = desc.next_table_address() { - let sub_region = VirtMemoryRegion::new(entry_va, table_coverage) - .intersection(region) - .expect("Sub region should overlap with parent region"); + // `entry_va + table_coverage` can overflow for the last entry + // at each level (e.g. PML4 entry 511 on x86_64). Compute the + // intersection with saturating arithmetic instead of + // constructing an unrepresentable VirtMemoryRegion. + let entry_end = entry_va.value().saturating_add(table_coverage); + let sub_start = region.start_address().value().max(entry_va.value()); + let sub_end = region.end_address().value().min(entry_end); + let sub_region = VirtMemoryRegion::from_start_end_address( + VA::from_value(sub_start), + VA::from_value(sub_end), + ); ::NextLevel::walk( next_desc, sub_region, ctx, modifier,