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.
This commit is contained in:
Matthew Leach
2026-04-21 13:59:27 +01:00
parent 68e1b57739
commit 6d41e485b3
2 changed files with 106 additions and 3 deletions

View File

@@ -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);

View File

@@ -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),
);
<T::Descriptor as TableMapper>::NextLevel::walk(
next_desc, sub_region, ctx, modifier,