Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 97 additions & 36 deletions litebox_platform_lvbs/src/arch/x86/mm/paging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use x86_64::{
CleanUp, FlagUpdateError, MapToError, PageTableFrameMapping, TranslateResult,
UnmapError as X64UnmapError,
},
page_table::PageTableEntry,
},
},
};
Expand All @@ -35,12 +36,12 @@ use crate::mm::{
#[cfg(not(test))]
const TLB_SINGLE_PAGE_FLUSH_CEILING: usize = 33;

/// Bit position of the PML4 (level-4) index within a virtual address, for
/// x86-64 4-level paging: 12 page-offset bits + 9 bits each for P1-P3.
const PML4_SHIFT: u32 = 39;

/// Mask for a 9-bit page-table index (512 entries per table).
const PML4_INDEX_MASK: u64 = 0x1FF;
const PAGE_SHIFT: usize = Size4KiB::SIZE.trailing_zeros() as usize;
const PAGE_TABLE_LEVEL_BITS: usize = 9;
const P2_SHIFT: usize = PAGE_SHIFT + PAGE_TABLE_LEVEL_BITS;
const P3_SHIFT: usize = P2_SHIFT + PAGE_TABLE_LEVEL_BITS;
const PML4_SHIFT: usize = P3_SHIFT + PAGE_TABLE_LEVEL_BITS;
const PML4_INDEX_MASK: u64 = (1 << PAGE_TABLE_LEVEL_BITS) - 1;

/// PML4 index of the first VTL1-kernel slot (`PA + KERNEL_OFFSET`).
///
Expand Down Expand Up @@ -291,30 +292,6 @@ impl<M: MemoryProvider, const ALIGN: usize> X64PageTable<'_, M, ALIGN> {
Ok(())
}

/// Clean up task-owned intermediate page table frames (P1-P3) for a task
/// page table that is being destroyed.
///
/// # Safety
///
/// The caller must ensure that:
/// - All user data frames have been released before calling this function (e.g., using `PageManager::release_memory()`)
/// - The page table is no longer active (not loaded in CR3)
pub(crate) unsafe fn cleanup_page_table_frames(&self) {
let mut allocator = PageTableAllocator::<M>::new();
// Task-owned slots span the VA below the kernel region, i.e.,
// `0 ..= KERNEL_PML4_START * PML4_SLOT_SIZE - 1`. The kernel region at
// and above `KERNEL_PML4_START` is base-owned/shared.
let start = Page::<Size4KiB>::from_start_address(VirtAddr::new(0)).unwrap();
let end = Page::<Size4KiB>::containing_address(VirtAddr::new(crate::KERNEL_OFFSET - 1));
// Safety: The page table is being destroyed and will not be reused.
// This function crosses the non-canonical hole.
unsafe {
self.inner
.lock()
.clean_up_addr_range(Page::range_inclusive(start, end), &mut allocator);
}
}

pub(crate) unsafe fn remap_pages(
&self,
old_range: PageRange<ALIGN>,
Expand Down Expand Up @@ -499,8 +476,8 @@ impl<M: MemoryProvider, const ALIGN: usize> X64PageTable<'_, M, ALIGN> {
// COW lazy-enable was unimplemented, so granting WRITABLE via a later
// fault would land in the unimplemented COW path and kill the task.
// Install the writable PTE directly until COW (and shared frames) land.
// FIXME: when COW is implemented, restore the lazy-enable masking that
// was removed here so a R->RW mprotect defers WRITABLE to the fault path.
// FIXME: COW needs lazy write enable and refcounted user frames;
// task-table destruction currently assumes exclusive ownership.
if flags != new_flags {
match unsafe {
inner.update_flags(page, (flags & !Self::MPROTECT_PTE_MASK) | new_flags)
Expand Down Expand Up @@ -822,16 +799,100 @@ impl<M: MemoryProvider, const ALIGN: usize> X64PageTable<'_, M, ALIGN> {
}

impl<M: MemoryProvider, const ALIGN: usize> Drop for X64PageTable<'_, M, ALIGN> {
/// Deallocate the physical frame of the top-level page table
/// Reclaims owned user frames and private page tables.
///
/// No TLB shootdown is needed because it is released after a non-PCID CR3 reload.
#[allow(clippy::similar_names)]
fn drop(&mut self) {
let mut allocator = PageTableAllocator::<M>::new();
let p4_va =
core::ptr::from_mut::<PageTable>(self.inner.lock().level_4_table_mut()).cast::<u8>();
let mut inner = self.inner.lock();
let p4 = inner.level_4_table_mut();

// Skip shared VTL1-kernel PML4 entries.
for (p4_index, p4_entry) in p4.iter_mut().enumerate().take(KERNEL_PML4_START) {
let Ok(p3_frame) = p4_entry.frame() else {
p4_entry.set_unused();
continue;
};
let p3 = unsafe { &mut *frame_to_pointer::<M>(p3_frame) };

for (p3_index, p3_entry) in p3.iter_mut().enumerate() {
if p3_entry.flags().contains(PageTableFlags::HUGE_PAGE) {
crate::debug_serial_println!("BUG: LVBS platform does not support huge pages");
debug_assert!(false, "LVBS platform does not support huge pages");
p3_entry.set_unused();
continue;
}
let Ok(p2_frame) = p3_entry.frame() else {
p3_entry.set_unused();
continue;
};
let p2 = unsafe { &mut *frame_to_pointer::<M>(p2_frame) };

for (p2_index, p2_entry) in p2.iter_mut().enumerate() {
if p2_entry.flags().contains(PageTableFlags::HUGE_PAGE) {
crate::debug_serial_println!(
"BUG: LVBS platform does not support huge pages"
);
debug_assert!(false, "LVBS platform does not support huge pages");
p2_entry.set_unused();
continue;
}
let Ok(p1_frame) = p2_entry.frame() else {
p2_entry.set_unused();
continue;
};
let p1 = unsafe { &mut *frame_to_pointer::<M>(p1_frame) };

for (p1_index, p1_entry) in p1.iter_mut().enumerate() {
// Indices >= 256 cannot fall in the user range.
let page_address = (p4_index << PML4_SHIFT)
| (p3_index << P3_SHIFT)
| (p2_index << P2_SHIFT)
| (p1_index << PAGE_SHIFT);
if (crate::USER_ADDR_MIN..crate::USER_ADDR_MAX).contains(&page_address) {
match p1_entry.frame() {
Ok(frame) => {
// Safety: task user leaf frames are exclusively owned.
unsafe { allocator.deallocate_frame(frame) };
}
Err(_) if !p1_entry.is_unused() => {
crate::debug_serial_println!(
"BUG: leaking malformed task leaf during destruction"
);
debug_assert!(false, "malformed task leaf during destruction");
}
Err(_) => {}
}
}
p1_entry.set_unused();
}
}
}
}

let p4_va = core::ptr::from_mut::<PageTable>(p4).cast::<u8>();
let p4_pa = M::va_to_pa(VirtAddr::new(p4_va as u64));
let start = Page::<Size4KiB>::containing_address(VirtAddr::new(0));
let end = Page::<Size4KiB>::containing_address(VirtAddr::new(crate::KERNEL_OFFSET - 1));
debug_assert_eq!(usize::from(end.p4_index()), KERNEL_PML4_START - 1);

// Safety: all private leaves were cleared above.
unsafe {
allocator.deallocate_frame(PhysFrame::containing_address(p4_pa));
inner.clean_up_addr_range(Page::range_inclusive(start, end), &mut allocator);
}
debug_assert!(
inner
.level_4_table()
.iter()
.take(KERNEL_PML4_START)
.all(PageTableEntry::is_unused),
"task page table dropped with live private mappings"
);
drop(inner);

// Safety: owned P4 frame.
unsafe { allocator.deallocate_frame(PhysFrame::containing_address(p4_pa)) };
}
}

Expand Down
12 changes: 7 additions & 5 deletions litebox_platform_lvbs/src/host/per_cpu_variables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,17 +255,19 @@ impl PerCpuVariables {
.map(|(_, page_table)| Arc::clone(page_table))
}

/// Replaces and returns the active task page table.
///
/// # Safety
///
/// CR3 must no longer reference the previous table. A new ID must match
/// CR3. Interrupts must be disabled, and this must not run in exception context.
pub(crate) unsafe fn set_active_page_table(
pub(crate) unsafe fn replace_active_page_table(
&self,
page_table: Option<(usize, Arc<crate::mm::PageTable<PAGE_SIZE>>)>,
) {
// Safety: Only this core accesses the field, interrupts are disabled,
// and the update cannot fault.
unsafe { *self.active_page_table.get() = page_table }
) -> Option<(usize, Arc<crate::mm::PageTable<PAGE_SIZE>>)> {
// Safety: Core-local, IRQs are disabled, and the update cannot fault.
// Return the old owner so the caller can drop it after restoring IRQs.
unsafe { core::mem::replace(&mut *self.active_page_table.get(), page_table) }
}
}

Expand Down
122 changes: 44 additions & 78 deletions litebox_platform_lvbs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ impl core::ops::Deref for PageTableHandle<'_> {
/// Future work could implement KPTI-style isolation to reduce the kernel attack surface
/// exposed to user TAs, mitigating potential side-channel attacks.
pub struct PageTableManager {
/// The base page table, containing only VTL1 kernel mappings (no user-space).
/// The base (kernel) page table; lives for the kernel's lifetime and is never dropped.
base_page_table: mm::PageTable<PAGE_SIZE>,
/// Cached physical frame of the base page table (for fast CR3 comparison).
base_page_table_frame: PhysFrame<Size4KiB>,
Expand Down Expand Up @@ -249,6 +249,18 @@ impl PageTableManager {
);
}

/// Returns an owning handle to a registered task page table.
///
/// The table remains alive while this handle exists.
pub fn task_page_table(&self, task_pt_id: usize) -> Result<PageTableHandle<'_>, Errno> {
if task_pt_id == BASE_PAGE_TABLE_ID {
return Err(Errno::EINVAL);
}
let task_pts = self.task_page_tables.read();
let page_table = Arc::clone(task_pts.get(&task_pt_id).ok_or(Errno::ENOENT)?);
Ok(PageTableHandle::task(page_table))
}

/// Returns the ID of the current page table based on the CR3 register.
///
/// Returns `BASE_PAGE_TABLE_ID` (0) if the base page table is active,
Expand Down Expand Up @@ -287,15 +299,16 @@ impl PageTableManager {
/// after the switch (including the code being executed and stack)
/// - No references to user-space memory are held across the switch
pub unsafe fn load_base(&self) {
x86_64::instructions::interrupts::without_interrupts(|| {
// Ensure decreasing/dropping `Arc` for the previous page table (`set_active_page_table()`)
// only after switching CR3 (`mm::PageTable::load()`).
let previous = x86_64::instructions::interrupts::without_interrupts(|| {
// Replace the per-CPU owner only after CR3 stops referencing it.
self.base_page_table.load();
with_per_cpu_variables(|pcv| {
// Safety: CR3 now references the base page table and interrupts are disabled.
unsafe { pcv.set_active_page_table(None) }
});
unsafe { pcv.replace_active_page_table(None) }
})
});
// Last-owner reclamation must run with IRQs enabled.
drop(previous);
}

/// Loads the specified task page table by updating CR3.
Expand Down Expand Up @@ -323,15 +336,16 @@ impl PageTableManager {
Arc::clone(task_pts.get(&task_pt_id).ok_or(Errno::ENOENT)?)
};

x86_64::instructions::interrupts::without_interrupts(|| {
// Ensure decreasing/dropping `Arc` for the previous page table (`set_active_page_table()`)
// only after switching CR3 (`mm::PageTable::load()`).
let previous = x86_64::instructions::interrupts::without_interrupts(|| {
// Replace the per-CPU owner only after CR3 stops referencing it.
pt.load();
with_per_cpu_variables(|pcv| {
// Safety: CR3 now references `pt` and interrupts are disabled.
unsafe { pcv.set_active_page_table(Some((task_pt_id, pt))) }
});
unsafe { pcv.replace_active_page_table(Some((task_pt_id, pt))) }
})
});
// Last-owner reclamation must run with IRQs enabled.
drop(previous);
Ok(())
}

Expand Down Expand Up @@ -363,63 +377,25 @@ impl PageTableManager {
Ok(task_pt_id)
}

/// Deletes a task page table by its ID.
///
/// This function:
/// 1. Clean up page table structure frames (P1-P3)
/// 2. Drop the page table (deallocating the top-level P4 frame)
///
/// # Arguments
///
/// * `task_pt_id` - The ID of the task page table to delete
/// Unregisters a task table; retained handles keep it alive.
///
/// # Safety
///
/// The caller must ensure that:
/// - All user data frames have been released before calling this function
/// - No references or pointers to memory mapped by this page table are held after deletion
///
/// # Returns
/// The caller must ensure final destruction is safe: user leaf frames are
/// exclusively owned and no access outlives all remaining handles.
///
/// - `Ok(())` if the page table was successfully deleted
/// - `Err(Errno::EINVAL)` if the page table ID is the base page table
/// - `Err(Errno::ENOENT)` if the page table ID does not exist
/// - `Err(Errno::EBUSY)` if the page table is active or has outstanding handles
pub unsafe fn delete_task_page_table(&self, task_pt_id: usize) -> Result<(), Errno> {
/// Returns `EINVAL` for the base ID and `ENOENT` if it is not registered.
pub unsafe fn unregister_task_page_table(&self, task_pt_id: usize) -> Result<(), Errno> {
if task_pt_id == BASE_PAGE_TABLE_ID {
return Err(Errno::EINVAL);
}

let mut task_pts = self.task_page_tables.write();

// Fast path for the page table active on this core.
let (cr3_frame, _) = x86_64::registers::control::Cr3::read();
let cr3_id: usize = cr3_frame.start_address().as_u64().trunc();
if cr3_id == task_pt_id {
return Err(Errno::EBUSY);
}

if let Some(pt) = task_pts.remove(&task_pt_id) {
// An active CR3 retains a per-CPU Arc.
let pt = match Arc::try_unwrap(pt) {
Ok(pt) => pt,
Err(pt) => {
task_pts.insert(task_pt_id, pt);
return Err(Errno::EBUSY);
}
};
drop(task_pts);

// Safety: successful unwrap proves the table is neither active nor
// borrowed. Kernel slots are base-owned and must not be freed.
unsafe {
pt.cleanup_page_table_frames();
}
// The PageTable's Drop impl will deallocate the top-level (P4) frame
Ok(())
} else {
Err(Errno::ENOENT)
}
let pt = {
let mut task_pts = self.task_page_tables.write();
task_pts.remove(&task_pt_id).ok_or(Errno::ENOENT)?
};
drop(pt);
Ok(())
}
}

Expand Down Expand Up @@ -725,27 +701,17 @@ impl<Host: HostInterface> LinuxKernel<Host> {
self.page_table_manager.create_task_page_table()
}

/// Deletes a task page table by its ID.
///
/// This function:
/// 1. Cleans up page table structure frames (P1-P3)
/// 2. Drops the page table (deallocating the top-level P4 frame)
/// Unregisters a task table; retained handles keep it alive.
///
/// # Safety
///
/// The caller must ensure that:
/// - All user data frames have been released before calling this function
/// - No references or pointers to memory mapped by this page table are held after deletion
///
/// # Returns
///
/// - `Ok(())` if successful
/// - `Err(Errno::EINVAL)` if the page table is the base page table
/// - `Err(Errno::ENOENT)` if the page table doesn't exist
/// - `Err(Errno::EBUSY)` if the page table is active or has outstanding handles
pub unsafe fn delete_task_page_table(&self, task_pt_id: usize) -> Result<(), Errno> {
// Safety: caller guarantees no dangling references
unsafe { self.page_table_manager.delete_task_page_table(task_pt_id) }
/// See [`PageTableManager::unregister_task_page_table`].
pub unsafe fn unregister_task_page_table(&self, task_pt_id: usize) -> Result<(), Errno> {
// Safety: the caller upholds the manager's destruction requirements.
unsafe {
self.page_table_manager
.unregister_task_page_table(task_pt_id)
}
}

/// Switch to the specified page table.
Expand Down
Loading
Loading