From 0791b01f38ca6eb7927cb2c08964d10a58a98fd0 Mon Sep 17 00:00:00 2001 From: Matthew Leach Date: Wed, 13 May 2026 16:19:52 +0100 Subject: [PATCH 1/3] libkernel: paging: add `translate` Add a `translate` function which is much more powerful that `get_pte`. It permits block mappings to be returned for a given VA. --- .../src/arch/arm64/memory/pg_descriptors.rs | 4 + .../src/arch/x86_64/memory/pg_descriptors.rs | 8 ++ libkernel/src/arch/x86_64/memory/pg_walk.rs | 76 +++++++++++++++++-- libkernel/src/memory/paging/mod.rs | 3 + libkernel/src/memory/paging/walk.rs | 43 ++++++++++- 5 files changed, 126 insertions(+), 8 deletions(-) diff --git a/libkernel/src/arch/arm64/memory/pg_descriptors.rs b/libkernel/src/arch/arm64/memory/pg_descriptors.rs index 0ef95da..55270e4 100644 --- a/libkernel/src/arch/arm64/memory/pg_descriptors.rs +++ b/libkernel/src/arch/arm64/memory/pg_descriptors.rs @@ -219,6 +219,10 @@ macro_rules! define_descriptor { let addr = reg.read(BlockPageFields::OUTPUT_ADDR); Some(PA::from_value((addr << Self::MAP_SHIFT) as usize)) } + + fn permissions(self) -> Option { + self.permissions() + } } } )? diff --git a/libkernel/src/arch/x86_64/memory/pg_descriptors.rs b/libkernel/src/arch/x86_64/memory/pg_descriptors.rs index 2035819..659df8b 100644 --- a/libkernel/src/arch/x86_64/memory/pg_descriptors.rs +++ b/libkernel/src/arch/x86_64/memory/pg_descriptors.rs @@ -195,6 +195,14 @@ macro_rules! impl_pa_mapper { Some(self.address()) } + + fn permissions(self) -> Option { + if (self.0 & $marker) != $marker { + return None; + } + + Some(self.permissions()) + } } } )+ diff --git a/libkernel/src/arch/x86_64/memory/pg_walk.rs b/libkernel/src/arch/x86_64/memory/pg_walk.rs index 1d63a3d..f28598f 100644 --- a/libkernel/src/arch/x86_64/memory/pg_walk.rs +++ b/libkernel/src/arch/x86_64/memory/pg_walk.rs @@ -4,18 +4,20 @@ use crate::{ error::{MapError, Result}, memory::{ PAGE_SIZE, - address::{TPA, VA}, + address::{PA, TPA, VA}, paging::{ - NullTlbInvalidator, PageTableEntry, PageTableMapper, PgTable, PgTableArray, - walk::{RecursiveWalker, WalkContext}, + NullTlbInvalidator, PaMapper, PageTableEntry, PageTableMapper, PgTable, PgTableArray, + TableMapper, + permissions::PtePermissions, + walk::{RecursiveWalker, Translator, WalkContext}, }, - region::VirtMemoryRegion, + region::{PhysMemoryRegion, VirtMemoryRegion}, }, }; use super::{ pg_descriptors::PTE, - pg_tables::{PML4Table, PTable}, + pg_tables::{PDPTable, PML4Table, PTable}, }; impl RecursiveWalker for PTable { @@ -112,6 +114,70 @@ pub fn get_pte( Ok(descriptor) } +impl Translator for PML4Table { + fn translate( + table_pa: TPA>, + va: VA, + ctx: &mut WalkContext, + ) -> Result> { + let desc = unsafe { + ctx.mapper + .with_page_table(table_pa, |pgtable| Self::from_ptr(pgtable).get_desc(va))? + }; + match desc.next_table_address() { + Some(next_pa) => PDPTable::translate(next_pa, va, ctx), + None if desc.is_valid() => Err(MapError::InvalidDescriptor.into()), + None => Ok(None), + } + } +} + +impl Translator for PTable { + fn translate( + table_pa: TPA>, + va: VA, + ctx: &mut WalkContext, + ) -> Result> { + let desc = unsafe { + ctx.mapper + .with_page_table(table_pa, |pgtable| Self::from_ptr(pgtable).get_desc(va))? + }; + + match desc.mapped_address() { + Some(pa) => Ok(Some(( + pa, + 1 << Self::Descriptor::MAP_SHIFT, + desc.permissions(), + ))), + None if desc.is_valid() => Err(MapError::InvalidDescriptor.into()), + None => Ok(None), + } + } +} + +/// Translates the VA into a physical region plus an offset and permissions. +pub fn translate( + pml4_table: TPA>, + va: VA, + mapper: &mut PM, +) -> Result> { + let mut walk_ctx = WalkContext { + mapper, + // Safe to not invalidate the TLB, as we are not modifying any PTEs. + invalidator: &NullTlbInvalidator {}, + }; + + if let Some((pa, blk_sz, perms)) = PML4Table::translate(pml4_table, va, &mut walk_ctx)? { + debug_assert!(blk_sz.is_power_of_two()); + + let offset = va.value() & (blk_sz - 1); + + Ok(Some((PhysMemoryRegion::new(pa, blk_sz), offset, perms))) + } else { + Ok(None) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/libkernel/src/memory/paging/mod.rs b/libkernel/src/memory/paging/mod.rs index feab2bd..0e4093e 100644 --- a/libkernel/src/memory/paging/mod.rs +++ b/libkernel/src/memory/paging/mod.rs @@ -70,6 +70,9 @@ pub trait PaMapper: PageTableEntry { /// Return the mapped physical address. fn mapped_address(self) -> Option; + + /// Return the permissions set on the PTE. + fn permissions(self) -> Option; } /// Trait representing a single level of the page table hierarchy. diff --git a/libkernel/src/memory/paging/walk.rs b/libkernel/src/memory/paging/walk.rs index 39c1f1a..34b8853 100644 --- a/libkernel/src/memory/paging/walk.rs +++ b/libkernel/src/memory/paging/walk.rs @@ -3,14 +3,14 @@ use crate::{ error::MapError, memory::{ - address::{TPA, VA}, + address::{PA, TPA, VA}, region::VirtMemoryRegion, }, }; use super::{ - PageTableEntry, PageTableMapper, PgTable, PgTableArray, TLBInvalidator, TableMapper, - TableMapperTable, + PaMapper, PageTableEntry, PageTableMapper, PgTable, PgTableArray, TLBInvalidator, TableMapper, + TableMapperTable, permissions::PtePermissions, }; /// A collection of context required to modify page tables. @@ -96,3 +96,40 @@ where Ok(()) } } + +pub(crate) trait Translator: PgTable + Sized { + fn translate( + table_pa: TPA>, + va: VA, + ctx: &mut WalkContext, + ) -> crate::error::Result>; +} + +impl Translator for T +where + T: TableMapperTable, + T::Descriptor: PaMapper, + ::NextLevel: Translator, +{ + fn translate( + table_pa: TPA>, + va: VA, + ctx: &mut WalkContext, + ) -> crate::error::Result> { + let desc = unsafe { + ctx.mapper + .with_page_table(table_pa, |pgtable| T::from_ptr(pgtable).get_desc(va))? + }; + + if let Some(next_pa) = desc.next_table_address() { + ::NextLevel::translate(next_pa, va, ctx) + } else if let Some(block_pa) = desc.mapped_address() { + let block_size = 1usize << T::Descriptor::MAP_SHIFT; + Ok(Some((block_pa, block_size, desc.permissions().unwrap()))) + } else if desc.is_valid() { + Err(MapError::InvalidDescriptor)? + } else { + Ok(None) + } + } +} From 64c8b02904e6194782a69b52a973fd7359b91101 Mon Sep 17 00:00:00 2001 From: Matthew Leach Date: Wed, 13 May 2026 16:20:36 +0100 Subject: [PATCH 2/3] libkernel: smalloc: don't ever allocate a null address If a memory list begins at NULL, never give out that address. It trips up UB checks, instead reserve the null page when that condition has been detected. --- libkernel/src/memory/allocators/smalloc.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/libkernel/src/memory/allocators/smalloc.rs b/libkernel/src/memory/allocators/smalloc.rs index a8b7c12..feb2900 100644 --- a/libkernel/src/memory/allocators/smalloc.rs +++ b/libkernel/src/memory/allocators/smalloc.rs @@ -41,6 +41,8 @@ pub struct RegionList { regions: *mut PhysMemoryRegion, } +const NULL_PAGE_RGN: PhysMemoryRegion = PhysMemoryRegion::new(PA::null(), PAGE_SIZE); + impl RegionList { /// Returns `true` if the list contains no regions. pub fn is_empty(&self) -> bool { @@ -248,6 +250,14 @@ impl> Smalloc { .find_allocation_location(size, align) .ok_or(KernelError::NoMemory)?; + if address.is_null() { + // Reserve the zero page and try again. We never want to return + // an allocation of NULL since it trips up UB checks. + self.res.insert_region(NULL_PAGE_RGN); + + return self.alloc(size, align); + } + // Allocation fits and doesn't overlap any reservation self.res.insert_region(PhysMemoryRegion::new(address, size)); From fee153d23cb48101ad984bdb822676b5f9c63796 Mon Sep 17 00:00:00 2001 From: Matthew Leach Date: Wed, 13 May 2026 17:10:29 +0100 Subject: [PATCH 3/3] libkernel: region: add new utility functions Add the following to all memory region variants: - `is_empty()`: Return true if the region has zero size. - `cap_size()`: Reduces the size if the given size < current. - `shrink_start()`: Move start address forward, while shrinking the region. --- libkernel/src/memory/region.rs | 103 +++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/libkernel/src/memory/region.rs b/libkernel/src/memory/region.rs index 0d51215..b2ddc86 100644 --- a/libkernel/src/memory/region.rs +++ b/libkernel/src/memory/region.rs @@ -59,6 +59,11 @@ impl MemoryRegion { } } + /// Returns `true` if this memory region is empty. + pub fn is_empty(self) -> bool { + self.size == 0 + } + /// Create a memory region from a start and end address. /// /// The size is calculated as `end - start`. No alignment is enforced. @@ -71,6 +76,27 @@ impl MemoryRegion { } } + /// Cap the size of the region. If `max_size < self.size`, the region is + /// shrunk. + pub fn cap_size(self, max_size: usize) -> Self { + if max_size < self.size { + Self::new(self.start_address(), max_size) + } else { + self + } + } + + /// Shrink this region by moving the start address 'forward' by `x` bytes. + /// + /// If `x` moves the start address beyond the end of the region, it + /// saturates on the boundary and becomes empty. + pub fn shrink_start(self, x: usize) -> Self { + Self { + address: self.start_address().add_bytes(x), + size: self.size.saturating_sub(x), + } + } + /// Return a new region with the same size but a different start address. pub fn with_start_address(mut self, new_start: Address) -> Self { self.address = new_start; @@ -676,6 +702,83 @@ mod tests { assert_eq!(main.punch_hole(hole), (Some(main), None)); } + #[test] + fn is_empty_zero_size() { + let r = region(0x1000, 0); + assert!(r.is_empty()); + } + + #[test] + fn is_empty_nonzero_size() { + let r = region(0x1000, 0x10); + assert!(!r.is_empty()); + } + + #[test] + fn is_empty_empty_constructor() { + assert!(PhysMemoryRegion::empty().is_empty()); + } + + #[test] + fn cap_size_below_current() { + let r = region(0x1000, 0x100); + let capped = r.cap_size(0x50); + assert_eq!(capped.start_address().value(), 0x1000); + assert_eq!(capped.size(), 0x50); + } + + #[test] + fn cap_size_equal_to_current() { + let r = region(0x1000, 0x100); + let capped = r.cap_size(0x100); + assert_eq!(capped, r); + } + + #[test] + fn cap_size_above_current() { + let r = region(0x1000, 0x100); + let capped = r.cap_size(0x200); + assert_eq!(capped, r); + } + + #[test] + fn cap_size_to_zero() { + let r = region(0x1000, 0x100); + let capped = r.cap_size(0); + assert_eq!(capped.start_address().value(), 0x1000); + assert!(capped.is_empty()); + } + + #[test] + fn shrink_start_within_bounds() { + let r = region(0x1000, 0x100); + let shrunk = r.shrink_start(0x40); + assert_eq!(shrunk.start_address().value(), 0x1040); + assert_eq!(shrunk.size(), 0xC0); + } + + #[test] + fn shrink_start_exact_size() { + let r = region(0x1000, 0x100); + let shrunk = r.shrink_start(0x100); + assert_eq!(shrunk.start_address().value(), 0x1100); + assert!(shrunk.is_empty()); + } + + #[test] + fn shrink_start_beyond_end_saturates() { + let r = region(0x1000, 0x100); + let shrunk = r.shrink_start(0x200); + assert_eq!(shrunk.start_address().value(), 0x1200); + assert!(shrunk.is_empty()); + } + + #[test] + fn shrink_start_zero() { + let r = region(0x1000, 0x100); + assert_eq!(r.shrink_start(0), r); + } + #[test] fn test_punch_hole_disjoint() { // Hole is completely far away (after)