feat(kernel): kernel-owned memory model

- FrameTable: per-frame owner/use, replaces PagePool + ProcessMemory
- Quota per process, page tables charged to owner
- Regions for shared mem + device MMIO, torn down w/ owner
- Platform: create/destroy AS, map/unmap, zero_frame; drop translate_va
- RequestPages/ReleasePages/MapPage(move) checked vs kernel records
- user window 0x1000..32MiB, private pts by construction
- boot loader + device setup go through kernel api
- kernel built in place, boot allocator bounded
- isolation tests, randomized invariant test, FrameTable kani proofs
- fixes: inflated release, unmapped release, dst va unchecked,
  heap overflow, unbounded pt alloc, unzeroed frames, double free
This commit is contained in:
2026-10-26 19:40:50 +01:00
parent e931485ca7
commit 8d2c40cb1e
43 changed files with 2815 additions and 1172 deletions

View File

@@ -1,6 +1,7 @@
#![no_std]
pub mod interrupt;
mod memory;
#[cfg(kani)]
pub mod proofs;
pub mod syscall;
@@ -16,7 +17,7 @@ use domain::{
endpoint::EndpointTable,
error::KernelError,
irq::IrqRoutingTable,
memory::{RegionTable, page_pool::PagePool, process_memory::ProcessMemory},
memory::{RegionTable, frame::FrameTable, quota::Quota},
message::Message,
process::{MAX_PROCESSES, ProcessTable},
resource::Resource,
@@ -26,6 +27,7 @@ use domain::{
};
use platform::Platform;
use types::{
address::PhysicalAddress,
bounded::{CapSlot, Priority},
generation::Generation,
id::{ProcessId, ThreadId},
@@ -39,8 +41,8 @@ pub struct Kernel<P: Platform> {
threads: ThreadTable,
endpoints: EndpointTable,
scheduler: MultiCoreScheduler,
page_pool: PagePool,
process_memory: [Option<ProcessMemory>; MAX_PROCESSES],
frames: FrameTable,
quotas: [Quota; MAX_PROCESSES],
regions: RegionTable,
irqs: IrqRoutingTable,
pending_messages: [Option<Message>; MAX_THREADS],
@@ -49,7 +51,7 @@ pub struct Kernel<P: Platform> {
}
impl<P: Platform> Kernel<P> {
pub fn new(platform: P, page_pool: PagePool) -> Self {
pub fn new(platform: P, pool_base: PhysicalAddress, pool_pages: usize) -> Self {
let num_cores = platform.num_cores();
Self {
platform,
@@ -57,8 +59,8 @@ impl<P: Platform> Kernel<P> {
threads: ThreadTable::new(),
endpoints: EndpointTable::new(),
scheduler: MultiCoreScheduler::new(num_cores),
page_pool,
process_memory: [const { None }; MAX_PROCESSES],
frames: FrameTable::new(pool_base, pool_pages),
quotas: [Quota::default(); MAX_PROCESSES],
regions: RegionTable::new(),
irqs: IrqRoutingTable::new(),
pending_messages: [const { None }; MAX_THREADS],
@@ -67,18 +69,51 @@ impl<P: Platform> Kernel<P> {
}
}
/// Builds the kernel directly in `slot`, one table at a time, so the
/// whole structure never has to fit on the (small) boot stack.
///
/// # Safety
/// `slot` must be valid for writes and properly aligned. Any previous
/// value is overwritten without being dropped.
pub unsafe fn init_in_place(
slot: *mut Self,
platform: P,
pool_base: PhysicalAddress,
pool_pages: usize,
) {
use core::ptr::addr_of_mut;
let num_cores = platform.num_cores();
// SAFETY: the caller guarantees `slot` is valid and aligned; each
// field is written exactly once.
unsafe {
addr_of_mut!((*slot).platform).write(platform);
addr_of_mut!((*slot).processes).write(ProcessTable::new());
addr_of_mut!((*slot).threads).write(ThreadTable::new());
addr_of_mut!((*slot).endpoints).write(EndpointTable::new());
addr_of_mut!((*slot).scheduler).write(MultiCoreScheduler::new(num_cores));
addr_of_mut!((*slot).frames).write(FrameTable::new(pool_base, pool_pages));
addr_of_mut!((*slot).quotas).write([Quota::default(); MAX_PROCESSES]);
addr_of_mut!((*slot).regions).write(RegionTable::new());
addr_of_mut!((*slot).irqs).write(IrqRoutingTable::new());
addr_of_mut!((*slot).pending_messages).write([const { None }; MAX_THREADS]);
addr_of_mut!((*slot).pending_results).write([const { None }; MAX_THREADS]);
addr_of_mut!((*slot).wait_sets).write([const { None }; MAX_THREADS]);
}
}
pub fn boot(
&mut self,
priority: Priority,
time_slice: TimeSlice,
) -> Result<(ProcessId, ThreadId), KernelError> {
let pid = self.processes.create(CapabilityTable::new(), None)?;
if let Err(e) = self.create_address_space(pid) {
let _ = self.processes.destroy(pid);
return Err(e);
}
let tid = self.threads.spawn(pid, priority, time_slice, None)?;
self.processes.get_mut(pid)?.add_thread(tid)?;
self.process_memory[pid.raw() as usize] =
Some(ProcessMemory::new(pid, ProcessMemory::DEFAULT_PAGE_LIMIT));
let cap = Capability::new(
Resource::ProcessCreate,
PermissionSet::empty().with(Permission::Execute),
@@ -116,10 +151,6 @@ impl<P: Platform> Kernel<P> {
self.threads.get_mut(tid)?.set_running(core)
}
pub fn free_page_count(&self) -> usize {
self.page_pool.free_count()
}
pub fn process_exists(&self, pid: ProcessId) -> bool {
self.processes.get(pid).is_ok()
}
@@ -155,11 +186,10 @@ impl<P: Platform> Kernel<P> {
pub fn create_process_internal(&mut self) -> Option<ProcessId> {
let pid = self.processes.create(CapabilityTable::new(), None).ok()?;
self.process_memory[pid.raw() as usize] =
Some(domain::memory::process_memory::ProcessMemory::new(
pid,
ProcessMemory::DEFAULT_PAGE_LIMIT,
));
if self.create_address_space(pid).is_err() {
let _ = self.processes.destroy(pid);
return None;
}
Some(pid)
}
@@ -245,12 +275,7 @@ impl<P: Platform> Kernel<P> {
self.cleanup_threads(&thread_ids);
// Free allocated pages back to pool
if let Some(pm) = &self.process_memory[pid.raw() as usize] {
for &(pa, count) in pm.allocations() {
self.page_pool.free(pa, count);
}
}
self.teardown_memory(pid);
// Unblock threads in other processes that were blocked waiting
// on endpoints where this process's threads were the other side.
@@ -260,7 +285,6 @@ impl<P: Platform> Kernel<P> {
// when the endpoint is revoked.
let _ = self.processes.destroy(pid);
self.process_memory[pid.raw() as usize] = None;
}
pub(crate) fn cleanup_threads(&mut self, thread_ids: &[Option<ThreadId>]) {

422
crates/kernel/src/memory.rs Normal file
View File

@@ -0,0 +1,422 @@
//! Address-space bookkeeping. The kernel decides every user mapping from
//! its own records (`FrameTable`, `RegionTable`, quotas) and only then tells
//! the platform to apply it; it never asks the hardware what is mapped.
use domain::{
error::KernelError,
memory::{
Region, RegionBacking, RegionMapping,
frame::{FrameTable, FrameUse, PAGE_SIZE},
quota::Quota,
},
};
use platform::{FrameSource, MapError, Platform};
use types::{
address::{PhysicalAddress, VirtualAddress},
bounded::PageCount,
id::{ProcessId, RegionId},
mapping::PagePerms,
};
use crate::Kernel;
/// First VA the kernel considers when picking a spot for `RequestPages`.
/// Below it sit the program image and boot-time device windows.
pub(crate) const USER_HEAP_START: usize = 0x40000;
/// Page-table frames for one address space, charged to its owner.
struct ChargedFrames<'a> {
frames: &'a mut FrameTable,
quota: &'a mut Quota,
owner: ProcessId,
}
impl FrameSource for ChargedFrames<'_> {
fn alloc_table_frame(&mut self) -> Option<PhysicalAddress> {
if !self.quota.charge(1) {
return None;
}
match self.frames.alloc(FrameUse::PageTable { owner: self.owner }) {
Some(pa) => Some(pa),
None => {
self.quota.uncharge(1);
None
}
}
}
}
fn map_error(e: MapError) -> KernelError {
match e {
MapError::OutOfFrames => KernelError::OutOfMemory,
MapError::InvalidAddress => KernelError::InvalidAddress,
MapError::NoAddressSpace => KernelError::OutOfMemory,
}
}
impl<P: Platform> Kernel<P> {
/// Whether `[va, va + pages)` is page aligned and inside the user window.
pub(crate) fn in_user_window(va: VirtualAddress, pages: usize) -> bool {
let start = va.raw();
let Some(len) = pages.checked_mul(PAGE_SIZE) else {
return false;
};
let Some(end) = start.checked_add(len) else {
return false;
};
pages > 0
&& start.is_multiple_of(PAGE_SIZE)
&& start >= P::USER_VA_START
&& end <= P::USER_VA_END
}
fn is_mapped(&self, pid: ProcessId, va: VirtualAddress) -> bool {
self.frames.find_user(pid, va).is_some() || self.regions.covering(pid, va).is_some()
}
fn range_is_free(&self, pid: ProcessId, va: VirtualAddress, pages: usize) -> bool {
(0..pages).all(|i| !self.is_mapped(pid, page_at(va, i)))
}
fn quota_mut(&mut self, pid: ProcessId) -> Result<&mut Quota, KernelError> {
self.quotas
.get_mut(pid.raw() as usize)
.ok_or(KernelError::InvalidProcess(pid))
}
pub fn page_usage(&self, pid: ProcessId) -> usize {
self.quotas.get(pid.raw() as usize).map_or(0, Quota::used)
}
pub fn set_page_limit(&mut self, pid: ProcessId, limit: usize) -> Result<(), KernelError> {
self.processes.get(pid)?;
if self.quota_mut(pid)?.set_limit(limit) {
Ok(())
} else {
Err(KernelError::OutOfMemory)
}
}
pub fn free_page_count(&self) -> usize {
self.frames.free_count()
}
/// Maps `pa` at `va` in `pid`, building page tables on `pid`'s quota.
fn platform_map(
&mut self,
pid: ProcessId,
va: VirtualAddress,
pa: PhysicalAddress,
perms: PagePerms,
) -> Result<(), KernelError> {
let quota = self
.quotas
.get_mut(pid.raw() as usize)
.ok_or(KernelError::InvalidProcess(pid))?;
let mut source = ChargedFrames {
frames: &mut self.frames,
quota,
owner: pid,
};
self.platform
.map_page(pid, va, pa, perms, &mut source)
.map_err(map_error)
}
pub(crate) fn create_address_space(&mut self, pid: ProcessId) -> Result<(), KernelError> {
self.quotas[pid.raw() as usize] = Quota::default();
let mut source = ChargedFrames {
frames: &mut self.frames,
quota: &mut self.quotas[pid.raw() as usize],
owner: pid,
};
let created = self
.platform
.create_address_space(pid, &mut source)
.map_err(map_error);
if created.is_err() {
self.frames.free_owned_by(pid);
self.quotas[pid.raw() as usize] = Quota::default();
}
created
}
/// Allocates, zeroes and maps one fresh page for `pid` at `va`.
fn map_fresh_page(
&mut self,
pid: ProcessId,
va: VirtualAddress,
perms: PagePerms,
) -> Result<PhysicalAddress, KernelError> {
let usage = FrameUse::user(pid, va, perms).ok_or(KernelError::InvalidAddress)?;
if !self.quota_mut(pid)?.charge(1) {
return Err(KernelError::OutOfMemory);
}
let Some(pa) = self.frames.alloc(usage) else {
self.quota_mut(pid)?.uncharge(1);
return Err(KernelError::OutOfMemory);
};
self.platform.zero_frame(pa);
if let Err(e) = self.platform_map(pid, va, pa, perms) {
self.frames.free(pa);
self.quota_mut(pid)?.uncharge(1);
return Err(e);
}
Ok(pa)
}
/// Removes one user page of `pid` and returns its frame to the pool.
fn unmap_and_free(&mut self, pid: ProcessId, va: VirtualAddress) {
if let Some((pa, _)) = self.frames.find_user(pid, va) {
self.platform.unmap_page(pid, va);
self.frames.free(pa);
if let Some(q) = self.quotas.get_mut(pid.raw() as usize) {
q.uncharge(1);
}
}
}
/// Frame backing the user page at `va` in `pid`. Lets boot code fill
/// pages it has just mapped for a process.
pub fn user_page_frame(&self, pid: ProcessId, va: VirtualAddress) -> Option<PhysicalAddress> {
self.frames.find_user(pid, va).map(|(pa, _)| pa)
}
/// Maps `pages` fresh zeroed pages at `va`, all or nothing.
pub fn map_fresh_range(
&mut self,
pid: ProcessId,
va: VirtualAddress,
pages: usize,
perms: PagePerms,
) -> Result<(), KernelError> {
if !Self::in_user_window(va, pages) {
return Err(KernelError::InvalidAddress);
}
if !self.range_is_free(pid, va, pages) {
return Err(KernelError::AddressInUse);
}
for i in 0..pages {
if let Err(e) = self.map_fresh_page(pid, page_at(va, i), perms) {
for j in 0..i {
self.unmap_and_free(pid, page_at(va, j));
}
return Err(e);
}
}
Ok(())
}
/// First-fit search for `pages` free pages in the heap part of the window.
pub(crate) fn find_free_heap_range(
&self,
pid: ProcessId,
pages: usize,
) -> Option<VirtualAddress> {
let mut candidate = USER_HEAP_START.max(P::USER_VA_START);
while Self::in_user_window(VirtualAddress::from_raw(candidate), pages) {
let va = VirtualAddress::from_raw(candidate);
match (0..pages).find(|&i| self.is_mapped(pid, page_at(va, i))) {
None => return Some(va),
Some(busy) => candidate += (busy + 1) * PAGE_SIZE,
}
}
None
}
/// Validates that every page of the range is a user page `pid` owns,
/// then releases them all.
pub(crate) fn release_range(
&mut self,
pid: ProcessId,
va: VirtualAddress,
pages: usize,
) -> Result<(), KernelError> {
if !Self::in_user_window(va, pages) {
return Err(KernelError::InvalidAddress);
}
if !(0..pages).all(|i| self.frames.find_user(pid, page_at(va, i)).is_some()) {
return Err(KernelError::InvalidAddress);
}
for i in 0..pages {
self.unmap_and_free(pid, page_at(va, i));
}
Ok(())
}
/// Moves the page at `src` in `from` to `dst` in `to`. The frame and its
/// quota charge change hands; `from` loses access.
pub(crate) fn move_page(
&mut self,
from: ProcessId,
src: VirtualAddress,
to: ProcessId,
dst: VirtualAddress,
perms: PagePerms,
) -> Result<(), KernelError> {
if !Self::in_user_window(src, 1) || !Self::in_user_window(dst, 1) {
return Err(KernelError::InvalidAddress);
}
let (pa, _) = self
.frames
.find_user(from, src)
.ok_or(KernelError::InvalidAddress)?;
self.processes.get(to)?;
if !self.range_is_free(to, dst, 1) {
return Err(KernelError::AddressInUse);
}
let usage = FrameUse::user(to, dst, perms).ok_or(KernelError::InvalidAddress)?;
if !self.quota_mut(to)?.charge(1) {
return Err(KernelError::OutOfMemory);
}
if let Err(e) = self.platform_map(to, dst, pa, perms) {
self.quota_mut(to)?.uncharge(1);
return Err(e);
}
self.platform.unmap_page(from, src);
self.frames.retag(pa, usage);
self.quota_mut(from)?.uncharge(1);
Ok(())
}
/// Allocates a physically contiguous, zeroed region charged to `owner`.
pub fn create_pool_region(
&mut self,
owner: ProcessId,
pages: PageCount,
) -> Result<RegionId, KernelError> {
self.processes.get(owner)?;
let id = self.regions.next_free_id().ok_or(KernelError::NoFreeSlot)?;
let count = pages.value() as usize;
if !self.quota_mut(owner)?.charge(count) {
return Err(KernelError::OutOfMemory);
}
let Some(base) = self
.frames
.alloc_contiguous(count, |_| FrameUse::Region { region: id })
else {
self.quota_mut(owner)?.uncharge(count);
return Err(KernelError::OutOfMemory);
};
for i in 0..count {
self.platform.zero_frame(base.offset(i));
}
self.regions
.insert(Region::new(owner, RegionBacking::Pool, base, pages))
.ok_or(KernelError::NoFreeSlot)
}
/// Describes device memory at `base` so it can be mapped like a region.
pub fn create_device_region(
&mut self,
owner: ProcessId,
base: PhysicalAddress,
pages: PageCount,
) -> Result<RegionId, KernelError> {
self.processes.get(owner)?;
if !base.raw().is_multiple_of(PAGE_SIZE) {
return Err(KernelError::InvalidAddress);
}
// Device memory must never alias frames the kernel hands out.
let len = pages.value() as usize * PAGE_SIZE;
let pool_start = self.frames.base().raw();
let pool_end = pool_start + self.frames.capacity() * PAGE_SIZE;
if base.raw() < pool_end && base.raw() + len > pool_start {
return Err(KernelError::InvalidAddress);
}
self.regions
.insert(Region::new(owner, RegionBacking::Device, base, pages))
.ok_or(KernelError::NoFreeSlot)
}
pub fn region_base(&self, id: RegionId) -> Option<PhysicalAddress> {
self.regions.get(id).map(Region::base)
}
/// Maps all of region `id` at `va` in `pid`, all or nothing.
pub fn map_region(
&mut self,
id: RegionId,
pid: ProcessId,
va: VirtualAddress,
perms: PagePerms,
) -> Result<(), KernelError> {
self.processes.get(pid)?;
let region = self.regions.get(id).ok_or(KernelError::InvalidRegion)?;
let (base, pages) = (region.base(), region.pages().value() as usize);
if region.mappings().any(|m| m.process == pid) {
return Err(KernelError::AddressInUse);
}
if !Self::in_user_window(va, pages) {
return Err(KernelError::InvalidAddress);
}
if !self.range_is_free(pid, va, pages) {
return Err(KernelError::AddressInUse);
}
for i in 0..pages {
if let Err(e) = self.platform_map(pid, page_at(va, i), base.offset(i), perms) {
for j in 0..i {
self.platform.unmap_page(pid, page_at(va, j));
}
return Err(e);
}
}
let recorded = self.regions.get_mut(id).is_some_and(|r| {
r.add_mapping(RegionMapping {
process: pid,
va,
perms,
})
});
if !recorded {
for i in 0..pages {
self.platform.unmap_page(pid, page_at(va, i));
}
return Err(KernelError::NoFreeSlot);
}
Ok(())
}
/// Frees regions owned by `pid` (unmapping them from every other
/// process) and forgets `pid`'s mappings of other regions.
fn teardown_regions(&mut self, pid: ProcessId) {
let mut owned = [None; domain::memory::MAX_REGIONS];
for (slot, id) in owned.iter_mut().zip(self.regions.ids_owned_by(pid)) {
*slot = Some(id);
}
for id in owned.into_iter().flatten() {
let Some(region) = self.regions.remove(id) else {
continue;
};
let pages = region.pages().value() as usize;
for m in region.mappings().filter(|m| m.process != pid) {
for i in 0..pages {
self.platform.unmap_page(m.process, page_at(m.va, i));
}
}
if region.backing() == RegionBacking::Pool {
for i in 0..pages {
self.frames.free(region.base().offset(i));
}
}
}
for (_, region) in self.regions.iter_mut() {
region.remove_mapping(pid);
}
}
/// Releases everything `pid` holds in memory: its address space, page
/// tables, user pages and owned regions.
pub(crate) fn teardown_memory(&mut self, pid: ProcessId) {
self.teardown_regions(pid);
self.platform.destroy_address_space(pid);
self.frames.free_owned_by(pid);
if let Some(q) = self.quotas.get_mut(pid.raw() as usize) {
*q = Quota::default();
}
}
}
fn page_at(base: VirtualAddress, i: usize) -> VirtualAddress {
VirtualAddress::from_raw(base.raw() + i * PAGE_SIZE)
}

View File

@@ -38,17 +38,10 @@ impl<P: Platform> Kernel<P> {
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
};
if let Err(e) = self.grant_cap(target_pid, resource.clone(), restricted) {
if let Err(e) = self.grant_cap(target_pid, resource, restricted) {
return SyscallOutcome::Complete(SyscallResult::Error(e));
}
if let domain::resource::Resource::Memory(region_id) = &resource
&& let Some(region) = self.regions.get(*region_id)
{
self.platform
.map_shared(target_pid, region.base(), region.size(), restricted);
}
SyscallOutcome::Complete(SyscallResult::Ok)
}

View File

@@ -57,11 +57,10 @@ impl<P: Platform> Kernel<P> {
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
};
self.process_memory[new_pid.raw() as usize] =
Some(domain::memory::process_memory::ProcessMemory::new(
new_pid,
domain::memory::process_memory::ProcessMemory::DEFAULT_PAGE_LIMIT,
));
if let Err(e) = self.create_address_space(new_pid) {
let _ = self.processes.destroy(new_pid);
return SyscallOutcome::Complete(SyscallResult::Error(e));
}
match self.grant_cap(
caller_pid,
@@ -73,8 +72,8 @@ impl<P: Platform> Kernel<P> {
Ok(slot) => SyscallOutcome::Complete(SyscallResult::Slot(slot)),
Err(e) => {
// Rollback: destroy orphaned process
self.teardown_memory(new_pid);
let _ = self.processes.destroy(new_pid);
self.process_memory[new_pid.raw() as usize] = None;
SyscallOutcome::Complete(SyscallResult::Error(e))
}
}

View File

@@ -1,168 +1,78 @@
use platform::Platform;
use types::address::{PhysicalAddress, VirtualAddress};
use types::{
address::VirtualAddress, bounded::CapSlot, bounded::PageCount, id::ProcessId,
mapping::PagePerms,
};
use super::{SyscallOutcome, SyscallResult};
use crate::Kernel;
use domain::{error::KernelError, resource::Resource};
use types::permission::Permission;
const USER_VA_LIMIT: usize = 0x80000000;
fn complete(result: Result<SyscallResult, KernelError>) -> SyscallOutcome {
SyscallOutcome::Complete(result.unwrap_or_else(SyscallResult::Error))
}
impl<P: Platform> Kernel<P> {
pub(crate) fn handle_request_pages(
&mut self,
pid: types::id::ProcessId,
count: types::bounded::PageCount,
pid: ProcessId,
count: PageCount,
) -> SyscallOutcome {
let pages = count.value() as usize;
match &mut self.process_memory[pid.raw() as usize] {
Some(pm) => {
if pm.grant(pages).is_err() {
return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::OutOfMemory,
));
}
}
None => {
return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::InvalidProcess(pid),
));
}
}
let phys_addr = match self.page_pool.allocate(pages) {
Some(a) => a,
None => {
if let Some(pm) = &mut self.process_memory[pid.raw() as usize] {
let _ = pm.release(pages);
}
return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::OutOfMemory,
));
}
};
let user_va = match &mut self.process_memory[pid.raw() as usize] {
Some(pm) => {
let va = pm.alloc_user_va(pages);
pm.record_allocation(phys_addr, pages);
va
}
None => {
self.page_pool.free(phys_addr, pages);
return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::InvalidProcess(pid),
));
}
};
for i in 0..pages {
let page_va = VirtualAddress::from_raw(user_va.raw() + i * 4096);
let page_pa = PhysicalAddress::from_raw(phys_addr.raw() + i * 4096);
self.platform
.map_user_page(pid, page_va, page_pa, true, false);
}
let region = domain::memory::MemoryRegion::new(
types::id::RegionId::from_raw(0),
pid,
phys_addr,
count,
types::generation::Generation::initial(),
);
let _ = self.regions.insert(region);
SyscallOutcome::Complete(SyscallResult::Value(user_va.raw()))
}
pub(crate) fn handle_map_page(
&mut self,
caller_pid: types::id::ProcessId,
process_cap: types::bounded::CapSlot,
src_va: VirtualAddress,
dst_va: VirtualAddress,
executable: bool,
) -> SyscallOutcome {
// Prevent mapping kernel memory into userspace
if src_va.raw() >= USER_VA_LIMIT {
return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::PermissionDenied {
slot: process_cap,
needed: types::permission::Permission::Read,
},
));
}
let target_pid = match self.check_cap(
caller_pid,
process_cap,
types::permission::Permission::Execute,
) {
Ok(cap) => match cap.resource() {
domain::resource::Resource::Process(pid) => *pid,
_ => {
return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::PermissionDenied {
slot: process_cap,
needed: types::permission::Permission::Execute,
},
));
}
},
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
};
let pa = match self.platform.translate_va(caller_pid, src_va) {
Some(pa) => pa,
None => {
return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::OutOfMemory,
));
}
};
self.platform
.map_user_page(target_pid, dst_va, pa, !executable, executable);
SyscallOutcome::Complete(SyscallResult::Ok)
complete(
self.find_free_heap_range(pid, pages)
.ok_or(KernelError::OutOfMemory)
.and_then(|va| {
self.map_fresh_range(pid, va, pages, PagePerms::ReadWrite)
.map(|()| SyscallResult::Value(va.raw()))
}),
)
}
pub(crate) fn handle_release_pages(
&mut self,
pid: types::id::ProcessId,
pid: ProcessId,
addr: VirtualAddress,
count: types::bounded::PageCount,
count: PageCount,
) -> SyscallOutcome {
let pages = count.value() as usize;
let phys = match self.platform.translate_va(pid, addr) {
Some(pa) => pa,
None => {
return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::OutOfMemory,
));
}
complete(
self.release_range(pid, addr, count.value() as usize)
.map(|()| SyscallResult::Ok),
)
}
/// Moves one of the caller's pages into the process named by
/// `process_cap`. Executable pages are mapped read-execute, others
/// read-write.
pub(crate) fn handle_map_page(
&mut self,
caller_pid: ProcessId,
process_cap: CapSlot,
src_va: VirtualAddress,
dst_va: VirtualAddress,
executable: bool,
) -> SyscallOutcome {
let target = match self.check_cap(caller_pid, process_cap, Permission::Execute) {
Ok(cap) => match cap.resource() {
Resource::Process(pid) => *pid,
_ => {
return complete(Err(KernelError::PermissionDenied {
slot: process_cap,
needed: Permission::Execute,
}));
}
},
Err(e) => return complete(Err(e)),
};
let region_id = match self.regions.find_by_base(pid, phys) {
Some((rid, _)) => rid,
None => {
return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::OutOfMemory,
));
}
let perms = if executable {
PagePerms::ReadExecute
} else {
PagePerms::ReadWrite
};
if !self.page_pool.free(phys, pages) {
return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::OutOfMemory,
));
}
let _ = self.regions.remove(region_id);
if let Some(pm) = &mut self.process_memory[pid.raw() as usize] {
let _ = pm.release(pages);
}
SyscallOutcome::Complete(SyscallResult::Ok)
complete(
self.move_page(caller_pid, src_va, target, dst_va, perms)
.map(|()| SyscallResult::Ok),
)
}
}

View File

@@ -1,4 +1,3 @@
use domain::memory::page_pool::PagePool;
use domain::resource::Resource;
use domain::scheduler::Scheduler;
use domain::thread::ThreadState;
@@ -16,8 +15,7 @@ use crate::Kernel;
pub fn test_kernel() -> Kernel<MockPlatform> {
let mock = MockPlatform::new(1);
let pool = PagePool::new(PhysicalAddress::from_raw(0x8000_0000), 1024);
Kernel::new(mock, pool)
Kernel::new(mock, PhysicalAddress::from_raw(0x8400_0000), 1024)
}
pub fn kernel_with_process() -> (Kernel<MockPlatform>, ProcessId) {

View File

@@ -0,0 +1,261 @@
//! Randomized whole-kernel check of the memory model. Three processes issue
//! thousands of random (often hostile) memory operations; after every step
//! the kernel's records, the quotas and the mock "hardware" must agree.
use super::*;
use crate::{SyscallOutcome, SyscallResult};
use domain::memory::{RegionBacking, frame::FrameUse};
use domain::syscall::Syscall;
use platform::Platform;
use types::address::{DataRef, VirtualAddress};
use types::bounded::{CapSlot, PageCount};
use types::id::ThreadId;
use types::mapping::PagePerms;
const PAGE: usize = 4096;
const PROCS: usize = 3;
const STEPS: usize = 4000;
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
// xorshift64
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn below(&mut self, n: usize) -> usize {
(self.next() % n as u64) as usize
}
/// A VA that is usually plausible and sometimes plain hostile.
fn va(&mut self, known: &[usize]) -> usize {
match self.below(6) {
0 | 1 if !known.is_empty() => known[self.below(known.len())],
2 => self.below(0x0300_0000) & !(PAGE - 1),
3 => self.next() as usize,
4 => 0x8020_0000 + self.below(64) * PAGE,
_ => 0x10000 + self.below(64) * PAGE,
}
}
}
struct World {
k: Kernel<MockPlatform>,
pids: [ProcessId; PROCS],
tids: [ThreadId; PROCS],
alive: [bool; PROCS],
/// A's process caps for B and C.
caps: [CapSlot; PROCS],
known: [[usize; 64]; PROCS],
known_len: [usize; PROCS],
}
impl World {
fn new() -> Self {
let (mut k, pid_a) = kernel_with_process();
let tid_a = k.current_thread().unwrap();
let mut pids = [pid_a; PROCS];
let mut tids = [tid_a; PROCS];
let mut caps = [CapSlot::new(0).unwrap(); PROCS];
for i in 1..PROCS {
caps[i] = match k.handle_syscall(Syscall::CreateProcess {
create_cap: CapSlot::new(0).unwrap(),
image: DataRef::empty(),
}) {
SyscallOutcome::Complete(SyscallResult::Slot(s)) => s,
_ => panic!("create process"),
};
pids[i] = ProcessId::from_raw(i as u16);
tids[i] = k.spawn_thread_for_test(pids[i], Priority::DEFAULT, TimeSlice::DEFAULT);
}
Self {
k,
pids,
tids,
alive: [true; PROCS],
caps,
known: [[0; 64]; PROCS],
known_len: [0; PROCS],
}
}
fn remember(&mut self, who: usize, va: usize) {
let n = self.known_len[who];
self.known[who][n % 64] = va;
self.known_len[who] = (n + 1).min(64);
}
fn known(&self, who: usize) -> &[usize] {
&self.known[who][..self.known_len[who]]
}
fn step(&mut self, rng: &mut Rng) {
let who = rng.below(PROCS);
if !self.alive[who] {
return;
}
self.k.switch_to_thread_for_test(self.tids[who]);
let pid = self.pids[who];
let known: [usize; 64] = self.known[who];
let known = &known[..self.known_len[who]];
match rng.below(10) {
0..=2 => {
let n = 1 + rng.below(4) as u16;
if let SyscallOutcome::Complete(SyscallResult::Value(va)) =
self.k.handle_syscall(Syscall::RequestPages {
count: PageCount::new(n).unwrap(),
})
{
for i in 0..n as usize {
self.remember(who, va + i * PAGE);
}
}
}
3..=4 => {
let addr = rng.va(known);
let count = PageCount::new(1 + rng.below(5) as u16).unwrap();
self.k.handle_syscall(Syscall::ReleasePages {
addr: VirtualAddress::from_raw(addr),
count,
});
}
5..=6 if who == 0 => {
let target = 1 + rng.below(PROCS - 1);
let src = rng.va(known);
let dst = rng.va(self.known(target));
let outcome = self.k.handle_syscall(Syscall::MapPage {
process: self.caps[target],
src_va: VirtualAddress::from_raw(src),
dst_va: VirtualAddress::from_raw(dst),
executable: rng.below(2) == 0,
});
if matches!(outcome, SyscallOutcome::Complete(SyscallResult::Ok)) {
self.remember(target, dst);
}
}
7 => {
let pages = PageCount::new(1 + rng.below(3) as u16).unwrap();
if let Ok(id) = self.k.create_pool_region(pid, pages) {
let other = rng.below(PROCS);
for p in [who, other] {
if self.alive[p] {
let va = VirtualAddress::from_raw(rng.va(self.known(p)));
let _ = self
.k
.map_region(id, self.pids[p], va, PagePerms::ReadWrite);
}
}
}
}
8 if who == 0 && rng.below(200) == 0 => {
let victim = 1 + rng.below(PROCS - 1);
if self.alive[victim] {
self.k.handle_syscall(Syscall::Kill {
process: self.caps[victim],
});
self.alive[victim] = false;
}
}
_ => {}
}
}
fn check(&self) {
let k = &self.k;
let window = MockPlatform::USER_VA_START..MockPlatform::USER_VA_END;
// Every frame is either free or charged to exactly one quota.
let charged: usize = self.pids.iter().map(|&p| k.page_usage(p)).sum();
assert_eq!(
charged + k.free_page_count(),
k.frames.capacity(),
"frames leaked or double counted"
);
for (i, &pid) in self.pids.iter().enumerate() {
let frames_owned = k
.frames
.iter()
.filter(|(_, u)| {
matches!(u, FrameUse::User { owner, .. } | FrameUse::PageTable { owner } if *owner == pid)
})
.count();
let region_pages: usize = k
.regions
.iter()
.filter(|(_, r)| r.owner() == pid && r.backing() == RegionBacking::Pool)
.map(|(_, r)| r.pages().value() as usize)
.sum();
assert_eq!(
k.page_usage(pid),
frames_owned + region_pages,
"quota of {pid:?} drifted"
);
if !self.alive[i] {
assert_eq!(k.page_usage(pid), 0);
assert_eq!(
k.platform.mapping_count(pid),
0,
"dead {pid:?} still mapped"
);
}
}
// Hardware -> kernel: every live mapping is one the kernel recorded.
for (pid, va, pa) in k.platform.mappings() {
assert!(
window.contains(&va),
"{pid:?} mapped outside window at {va:#x}"
);
let v = VirtualAddress::from_raw(va);
let as_user = k.frames.find_user(pid, v).map(|(p, _)| p.raw());
let as_region = k.regions.covering(pid, v).and_then(|id| {
let r = k.regions.get(id)?;
let m = r.mappings().find(|m| m.process == pid)?;
Some(r.base().raw() + (va - m.va.raw()))
});
assert!(
as_user == Some(pa) || as_region == Some(pa),
"{pid:?} has unrecorded mapping {va:#x} -> {pa:#x}"
);
}
// Kernel -> hardware: every recorded user page is really mapped.
for (pa, usage) in k.frames.iter() {
if let FrameUse::User { owner, page, .. } = usage {
assert_eq!(
k.platform.mapping(*owner, *page as usize * PAGE),
Some(pa.raw()),
"recorded page not mapped"
);
}
}
for (_, r) in k.regions.iter() {
for m in r.mappings() {
for i in 0..r.pages().value() as usize {
assert_eq!(
k.platform.mapping(m.process, m.va.raw() + i * PAGE),
Some(r.base().raw() + i * PAGE)
);
}
}
}
}
}
#[test]
fn memory_model_holds_under_random_hostile_operations() {
for seed in [1u64, 0xDEAD_BEEF, 0x1234_5678_9ABC, 42, 7777] {
let mut world = World::new();
let mut rng = Rng(seed);
for _ in 0..STEPS {
world.step(&mut rng);
world.check();
}
}
}

View File

@@ -0,0 +1,391 @@
//! Adversarial memory-isolation tests. Each one is an attack a process
//! with no special capabilities could try; the kernel must refuse it.
use super::*;
use crate::{SyscallOutcome, SyscallResult};
use domain::syscall::Syscall;
use types::address::{DataRef, VirtualAddress};
use types::bounded::{CapSlot, PageCount};
use types::id::ThreadId;
use types::mapping::PagePerms;
const PAGE: usize = 4096;
// Mirrors the user window the mock platform exposes.
const USER_VA_END: usize = 0x0200_0000;
fn pages(n: u16) -> PageCount {
PageCount::new(n).unwrap()
}
fn request(k: &mut Kernel<MockPlatform>, n: u16) -> Option<usize> {
match k.handle_syscall(Syscall::RequestPages { count: pages(n) }) {
SyscallOutcome::Complete(SyscallResult::Value(va)) => Some(va),
_ => None,
}
}
fn release(k: &mut Kernel<MockPlatform>, va: usize, n: u16) -> bool {
matches!(
k.handle_syscall(Syscall::ReleasePages {
addr: VirtualAddress::from_raw(va),
count: pages(n),
}),
SyscallOutcome::Complete(SyscallResult::Ok)
)
}
fn map_page(k: &mut Kernel<MockPlatform>, process: CapSlot, src: usize, dst: usize) -> bool {
matches!(
k.handle_syscall(Syscall::MapPage {
process,
src_va: VirtualAddress::from_raw(src),
dst_va: VirtualAddress::from_raw(dst),
executable: false,
}),
SyscallOutcome::Complete(SyscallResult::Ok)
)
}
/// Boots init (A) and creates a second process B with one runnable thread.
/// Returns A's process cap for B.
fn two_processes() -> (
Kernel<MockPlatform>,
ProcessId,
ThreadId,
ProcessId,
ThreadId,
CapSlot,
) {
let (mut k, pid_a) = kernel_with_process();
let tid_a = k.current_thread().unwrap();
let cap_b = match k.handle_syscall(Syscall::CreateProcess {
create_cap: CapSlot::new(0).unwrap(),
image: DataRef::empty(),
}) {
SyscallOutcome::Complete(SyscallResult::Slot(s)) => s,
_ => panic!("create process"),
};
let pid_b = ProcessId::from_raw(1);
let tid_b = k.spawn_thread_for_test(pid_b, Priority::DEFAULT, TimeSlice::DEFAULT);
(k, pid_a, tid_a, pid_b, tid_b, cap_b)
}
#[test]
fn release_with_inflated_count_cannot_free_another_processes_pages() {
let (mut k, pid_a, tid_a, pid_b, tid_b, _) = two_processes();
let va_a = request(&mut k, 1).unwrap();
k.switch_to_thread_for_test(tid_b);
let va_b = request(&mut k, 8).unwrap();
let victim: [usize; 8] =
core::array::from_fn(|i| k.platform_mut().mapping(pid_b, va_b + i * PAGE).unwrap());
k.switch_to_thread_for_test(tid_a);
assert!(
!release(&mut k, va_a, 9),
"released more pages than it owns"
);
let va_new = request(&mut k, 8).unwrap();
for i in 0..8 {
let pa = k.platform_mut().mapping(pid_a, va_new + i * PAGE).unwrap();
assert!(!victim.contains(&pa), "A was handed B's live page {pa:#x}");
}
}
#[test]
fn released_page_is_unmapped_from_caller() {
let (mut k, pid) = kernel_with_process();
let va = request(&mut k, 1).unwrap();
assert!(release(&mut k, va, 1));
assert_eq!(k.platform_mut().mapping(pid, va), None);
}
#[test]
fn page_cannot_be_released_twice() {
let (mut k, _) = kernel_with_process();
let va = request(&mut k, 1).unwrap();
assert!(release(&mut k, va, 1));
let free = k.free_page_count();
assert!(!release(&mut k, va, 1));
assert_eq!(k.free_page_count(), free);
}
#[test]
fn killing_a_process_after_release_does_not_double_free() {
let (mut k, _, tid_a, pid_b, tid_b, cap_b) = two_processes();
k.switch_to_thread_for_test(tid_b);
let va = request(&mut k, 2).unwrap();
assert!(release(&mut k, va, 2));
k.switch_to_thread_for_test(tid_a);
// Only what is still charged to B (its page tables) may come back.
let expected = k.free_page_count() + k.page_usage(pid_b);
k.handle_syscall(Syscall::Kill { process: cap_b });
assert_eq!(k.free_page_count(), expected);
}
#[test]
fn killed_process_pages_return_to_pool() {
let (mut k, _, tid_a, pid_b, tid_b, cap_b) = two_processes();
let baseline = k.free_page_count() + k.page_usage(pid_b);
k.switch_to_thread_for_test(tid_b);
request(&mut k, 3).unwrap();
k.switch_to_thread_for_test(tid_a);
k.handle_syscall(Syscall::Kill { process: cap_b });
assert_eq!(k.free_page_count(), baseline);
assert_eq!(k.page_usage(pid_b), 0);
}
#[test]
fn map_page_rejects_destination_outside_user_window() {
let (mut k, _, _, pid_b, _, cap_b) = two_processes();
let src = request(&mut k, 1).unwrap();
let kernel_text = 0x8020_0000;
assert!(!map_page(&mut k, cap_b, src, kernel_text));
assert_eq!(k.platform_mut().mapping(pid_b, kernel_text), None);
}
#[test]
fn map_page_rejects_null_page_destination() {
let (mut k, _, _, pid_b, _, cap_b) = two_processes();
let src = request(&mut k, 1).unwrap();
assert!(!map_page(&mut k, cap_b, src, 0));
assert_eq!(k.platform_mut().mapping(pid_b, 0), None);
}
#[test]
fn map_page_moves_the_page_out_of_the_caller() {
let (mut k, pid_a, _, pid_b, _, cap_b) = two_processes();
let src = request(&mut k, 1).unwrap();
let pa = k.platform_mut().mapping(pid_a, src).unwrap();
assert!(map_page(&mut k, cap_b, src, 0x10000));
assert_eq!(k.platform_mut().mapping(pid_a, src), None);
assert_eq!(k.platform_mut().mapping(pid_b, 0x10000), Some(pa));
}
#[test]
fn map_page_cannot_overwrite_an_existing_mapping() {
let (mut k, _, _, pid_b, _, cap_b) = two_processes();
let first = request(&mut k, 1).unwrap();
let second = request(&mut k, 1).unwrap();
assert!(map_page(&mut k, cap_b, first, 0x10000));
let pa = k.platform_mut().mapping(pid_b, 0x10000);
assert!(!map_page(&mut k, cap_b, second, 0x10000));
assert_eq!(k.platform_mut().mapping(pid_b, 0x10000), pa);
}
#[test]
fn map_page_source_must_be_a_page_the_caller_owns() {
let (mut k, _, _, _, _, cap_b) = two_processes();
// An MMIO address the caller never received through RequestPages.
assert!(!map_page(&mut k, cap_b, 0x1000_0000, 0x10000));
}
#[test]
fn heap_never_grows_outside_the_user_window() {
let (mut k, _) = kernel_with_process();
for _ in 0..10_000 {
let Some(va) = request(&mut k, 1) else { break };
assert!(
va >= PAGE && va + PAGE <= USER_VA_END,
"heap page at {va:#x}"
);
assert!(release(&mut k, va, 1));
}
}
#[test]
fn requested_pages_are_zeroed_before_mapping() {
let (mut k, pid) = kernel_with_process();
let va = request(&mut k, 2).unwrap();
for i in 0..2 {
let pa = k.platform_mut().mapping(pid, va + i * PAGE).unwrap();
assert!(
k.platform_mut().was_zeroed(pa),
"page {pa:#x} handed out dirty"
);
}
}
#[test]
fn page_tables_count_against_the_process_quota() {
let (mut k, pid) = kernel_with_process();
let used = k.page_usage(pid);
// Room for exactly one data page plus the leaf table it needs.
k.set_page_limit(pid, used + 2).unwrap();
assert!(request(&mut k, 1).is_some());
assert!(request(&mut k, 1).is_none(), "quota should be exhausted");
}
#[test]
fn killing_a_process_returns_its_page_tables_too() {
let (mut k, pid_a) = kernel_with_process();
let before = k.free_page_count();
let cap_b = match k.handle_syscall(Syscall::CreateProcess {
create_cap: CapSlot::new(0).unwrap(),
image: DataRef::empty(),
}) {
SyscallOutcome::Complete(SyscallResult::Slot(s)) => s,
_ => panic!("create process"),
};
let src = request(&mut k, 1).unwrap();
assert!(map_page(&mut k, cap_b, src, 0x10000));
k.handle_syscall(Syscall::Kill { process: cap_b });
// A keeps its own heap leaf table; B's root, leaf and page are all back.
assert_eq!(k.free_page_count(), before - (k.page_usage(pid_a) - 1));
assert!(!k.platform_mut().has_address_space(ProcessId::from_raw(1)));
}
#[test]
fn map_page_moves_the_quota_charge_to_the_target() {
let (mut k, pid_a, _, pid_b, _, cap_b) = two_processes();
let src = request(&mut k, 1).unwrap();
let a_before = k.page_usage(pid_a);
let b_before = k.page_usage(pid_b);
assert!(map_page(&mut k, cap_b, src, 0x10000));
assert_eq!(k.page_usage(pid_a), a_before - 1);
// the page plus the leaf table covering 0x10000
assert_eq!(k.page_usage(pid_b), b_before + 2);
}
#[test]
fn map_page_fails_cleanly_when_target_quota_is_full() {
let (mut k, pid_a, _, pid_b, _, cap_b) = two_processes();
let src = request(&mut k, 1).unwrap();
let used = k.page_usage(pid_b);
k.set_page_limit(pid_b, used).unwrap();
assert!(!map_page(&mut k, cap_b, src, 0x10000));
// the page stays with the caller
assert!(k.platform_mut().mapping(pid_a, src).is_some());
assert_eq!(k.platform_mut().mapping(pid_b, 0x10000), None);
}
#[test]
fn shared_region_is_visible_in_both_address_spaces() {
let (mut k, pid_a, _, pid_b, _, _) = two_processes();
let id = k.create_pool_region(pid_a, pages(2)).unwrap();
k.map_region(
id,
pid_a,
VirtualAddress::from_raw(0x30000),
PagePerms::ReadWrite,
)
.unwrap();
k.map_region(
id,
pid_b,
VirtualAddress::from_raw(0x50000),
PagePerms::ReadOnly,
)
.unwrap();
for i in 0..2 {
let a = k.platform_mut().mapping(pid_a, 0x30000 + i * PAGE);
assert!(a.is_some());
assert_eq!(a, k.platform_mut().mapping(pid_b, 0x50000 + i * PAGE));
}
assert_eq!(
k.platform_mut().perms(pid_b, 0x50000),
Some(PagePerms::ReadOnly)
);
}
#[test]
fn heap_requests_never_land_on_a_region_mapping() {
let (mut k, pid) = kernel_with_process();
let id = k.create_pool_region(pid, pages(4)).unwrap();
// Exactly where the kernel would otherwise put the first heap page.
let heap_start = request(&mut k, 1).unwrap();
assert!(release(&mut k, heap_start, 1));
k.map_region(
id,
pid,
VirtualAddress::from_raw(heap_start),
PagePerms::ReadWrite,
)
.unwrap();
let va = request(&mut k, 1).unwrap();
assert!(
va >= heap_start + 4 * PAGE || va + PAGE <= heap_start,
"heap page {va:#x} inside region"
);
}
#[test]
fn map_page_cannot_target_a_region_mapping() {
let (mut k, _, _, pid_b, _, cap_b) = two_processes();
let id = k.create_pool_region(pid_b, pages(1)).unwrap();
k.map_region(
id,
pid_b,
VirtualAddress::from_raw(0x30000),
PagePerms::ReadWrite,
)
.unwrap();
let region_pa = k.platform_mut().mapping(pid_b, 0x30000);
let src = request(&mut k, 1).unwrap();
assert!(!map_page(&mut k, cap_b, src, 0x30000));
assert_eq!(k.platform_mut().mapping(pid_b, 0x30000), region_pa);
}
#[test]
fn killing_a_region_owner_unmaps_it_everywhere() {
let (mut k, pid_a, tid_a, pid_b, tid_b, cap_b) = two_processes();
let id = k.create_pool_region(pid_b, pages(2)).unwrap();
k.map_region(
id,
pid_b,
VirtualAddress::from_raw(0x30000),
PagePerms::ReadWrite,
)
.unwrap();
k.map_region(
id,
pid_a,
VirtualAddress::from_raw(0x30000),
PagePerms::ReadWrite,
)
.unwrap();
let _ = (tid_a, tid_b);
let before = k.free_page_count();
k.handle_syscall(Syscall::Kill { process: cap_b });
assert_eq!(k.platform_mut().mapping(pid_a, 0x30000), None);
assert_eq!(k.platform_mut().mapping(pid_a, 0x31000), None);
// region frames, B's root and B's leaf table are all freed
assert_eq!(k.free_page_count(), before + 4);
}
#[test]
fn region_cannot_overlap_existing_pages() {
let (mut k, pid) = kernel_with_process();
let va = request(&mut k, 1).unwrap();
let id = k.create_pool_region(pid, pages(1)).unwrap();
assert!(
k.map_region(id, pid, VirtualAddress::from_raw(va), PagePerms::ReadWrite)
.is_err()
);
}
#[test]
fn region_mapping_must_fit_the_user_window() {
let (mut k, pid) = kernel_with_process();
let id = k.create_pool_region(pid, pages(2)).unwrap();
let last = USER_VA_END - PAGE;
assert!(
k.map_region(
id,
pid,
VirtualAddress::from_raw(last),
PagePerms::ReadWrite
)
.is_err()
);
assert_eq!(k.platform_mut().mapping(pid, last), None);
}

View File

@@ -17,19 +17,21 @@ fn request_pages_returns_user_va() {
}
#[test]
fn request_pages_reduces_pool() {
let (mut k, _) = kernel_with_process();
fn request_pages_reduces_pool_by_exactly_what_is_charged() {
let (mut k, pid) = kernel_with_process();
let before = k.free_page_count();
let used_before = k.page_usage(pid);
k.handle_syscall(Syscall::RequestPages {
count: PageCount::new(10).unwrap(),
});
assert_eq!(k.free_page_count(), before - 10);
let charged = k.page_usage(pid) - used_before;
assert!(charged >= 10, "10 data pages plus any page tables");
assert_eq!(k.free_page_count(), before - charged);
}
#[test]
fn release_pages_frees_and_restores_pool() {
let (mut k, _) = kernel_with_process();
let before = k.free_page_count();
fn release_pages_returns_the_data_pages() {
let (mut k, pid) = kernel_with_process();
let va = match k.handle_syscall(Syscall::RequestPages {
count: PageCount::new(4).unwrap(),
}) {
@@ -37,7 +39,8 @@ fn release_pages_frees_and_restores_pool() {
_ => panic!("expected value"),
};
// Use the VA returned by RequestPages (the kernel mapped it)
let free_before = k.free_page_count();
let used_before = k.page_usage(pid);
let outcome = k.handle_syscall(Syscall::ReleasePages {
addr: VirtualAddress::from_raw(va),
count: PageCount::new(4).unwrap(),
@@ -46,5 +49,7 @@ fn release_pages_frees_and_restores_pool() {
outcome,
SyscallOutcome::Complete(SyscallResult::Ok)
));
assert_eq!(k.free_page_count(), before);
// The four data pages come back; page tables stay with the process.
assert_eq!(k.free_page_count(), free_before + 4);
assert_eq!(k.page_usage(pid), used_before - 4);
}

View File

@@ -1,26 +1,42 @@
use domain::memory::MemoryRegion;
use platform::Platform;
use platform::{FrameSource, MapError, Platform};
use types::{
address::{EntryPoint, PhysicalAddress, StackPointer, VirtualAddress},
bounded::PageCount,
id::{CoreId, IrqId, ProcessId},
permission::PermissionSet,
mapping::PagePerms,
time::{Ticks, TimerDuration},
};
const MAX_MAPPINGS: usize = 256;
const MAX_MAPPINGS: usize = 2048;
const MAX_TABLES: usize = 256;
const MAX_SPACES: usize = 64;
const MAX_ZEROED: usize = 2048;
// Each 2 MiB of user VA needs one leaf table, like Sv39.
const TABLE_SPAN: usize = 2 * 1024 * 1024;
#[derive(Clone, Copy)]
struct PageMapping {
process: ProcessId,
va: usize,
pa: usize,
perms: PagePerms,
}
#[derive(Clone, Copy)]
struct LeafTable {
process: ProcessId,
span: usize,
}
/// Records what the "hardware" would hold so tests can check it against
/// the kernel's own bookkeeping.
pub struct MockPlatform {
ticks: Ticks,
core: CoreId,
cores: u8,
spaces: [bool; MAX_SPACES],
mappings: [Option<PageMapping>; MAX_MAPPINGS],
tables: [Option<LeafTable>; MAX_TABLES],
zeroed: [Option<usize>; MAX_ZEROED],
}
impl MockPlatform {
@@ -29,17 +45,159 @@ impl MockPlatform {
ticks: Ticks::from_raw(0),
core: CoreId::from_raw(0),
cores: num_cores,
mappings: [const { None }; MAX_MAPPINGS],
spaces: [false; MAX_SPACES],
mappings: [None; MAX_MAPPINGS],
tables: [None; MAX_TABLES],
zeroed: [None; MAX_ZEROED],
}
}
/// Physical address `va` is mapped to in `process`, as the "hardware" sees it.
pub fn mapping(&self, process: ProcessId, va: usize) -> Option<usize> {
self.mappings
.iter()
.flatten()
.find(|m| m.process == process && m.va == va)
.map(|m| m.pa)
}
/// Every live mapping as (process, va, pa).
pub fn mappings(&self) -> impl Iterator<Item = (ProcessId, usize, usize)> + '_ {
self.mappings
.iter()
.flatten()
.map(|m| (m.process, m.va, m.pa))
}
pub fn perms(&self, process: ProcessId, va: usize) -> Option<PagePerms> {
self.mappings
.iter()
.flatten()
.find(|m| m.process == process && m.va == va)
.map(|m| m.perms)
}
pub fn mapping_count(&self, process: ProcessId) -> usize {
self.mappings
.iter()
.flatten()
.filter(|m| m.process == process)
.count()
}
pub fn has_address_space(&self, process: ProcessId) -> bool {
self.spaces[process.raw() as usize]
}
pub fn was_zeroed(&self, pa: usize) -> bool {
self.zeroed.contains(&Some(pa))
}
pub fn set_ticks(&mut self, ticks: Ticks) {
self.ticks = ticks;
}
}
impl Platform for MockPlatform {
fn configure_protection(&mut self, _process: ProcessId, _regions: &[MemoryRegion]) {}
const USER_VA_START: usize = 0x1000;
const USER_VA_END: usize = 0x0200_0000;
fn create_address_space(
&mut self,
process: ProcessId,
frames: &mut dyn FrameSource,
) -> Result<(), MapError> {
// Root table.
frames.alloc_table_frame().ok_or(MapError::OutOfFrames)?;
self.spaces[process.raw() as usize] = true;
Ok(())
}
fn destroy_address_space(&mut self, process: ProcessId) {
self.spaces[process.raw() as usize] = false;
for slot in &mut self.mappings {
if slot.is_some_and(|m| m.process == process) {
*slot = None;
}
}
for slot in &mut self.tables {
if slot.is_some_and(|t| t.process == process) {
*slot = None;
}
}
}
fn map_page(
&mut self,
process: ProcessId,
va: VirtualAddress,
pa: PhysicalAddress,
perms: PagePerms,
frames: &mut dyn FrameSource,
) -> Result<(), MapError> {
if !self.spaces[process.raw() as usize] {
return Err(MapError::NoAddressSpace);
}
let va = va.raw();
if !va.is_multiple_of(4096) || !(Self::USER_VA_START..Self::USER_VA_END).contains(&va) {
return Err(MapError::InvalidAddress);
}
let span = va / TABLE_SPAN;
let have_table = self
.tables
.iter()
.flatten()
.any(|t| t.process == process && t.span == span);
if !have_table {
frames.alloc_table_frame().ok_or(MapError::OutOfFrames)?;
let slot = self
.tables
.iter_mut()
.find(|t| t.is_none())
.expect("mock table slots");
*slot = Some(LeafTable { process, span });
}
let entry = PageMapping {
process,
va,
pa: pa.raw(),
perms,
};
match self
.mappings
.iter_mut()
.find(|m| m.is_some_and(|m| m.process == process && m.va == va))
{
Some(existing) => *existing = Some(entry),
None => {
let slot = self
.mappings
.iter_mut()
.find(|m| m.is_none())
.expect("mock mapping slots");
*slot = Some(entry);
}
}
Ok(())
}
fn unmap_page(&mut self, process: ProcessId, va: VirtualAddress) {
for slot in &mut self.mappings {
if slot.is_some_and(|m| m.process == process && m.va == va.raw()) {
*slot = None;
}
}
}
fn zero_frame(&mut self, pa: PhysicalAddress) {
if self.zeroed.contains(&Some(pa.raw())) {
return;
}
if let Some(slot) = self.zeroed.iter_mut().find(|z| z.is_none()) {
*slot = Some(pa.raw());
}
}
fn enable_user_mode(&mut self, _entry: EntryPoint, _stack: StackPointer) -> ! {
panic!("enable_user_mode is not supported in tests")
}
@@ -56,48 +214,8 @@ impl Platform for MockPlatform {
fn num_cores(&self) -> u8 {
self.cores
}
fn map_shared(
&mut self,
_process: ProcessId,
_base: PhysicalAddress,
_size: PageCount,
_permissions: PermissionSet,
) {
}
fn halt(&self) -> ! {
panic!("halt")
}
fn debug_putchar(&self, _byte: u8) {}
fn translate_va(&self, process: ProcessId, va: VirtualAddress) -> Option<PhysicalAddress> {
for m in &self.mappings {
if let Some(m) = m
&& m.process.raw() == process.raw()
&& m.va == va.raw()
{
return Some(PhysicalAddress::from_raw(m.pa));
}
}
None
}
fn map_user_page(
&mut self,
process: ProcessId,
va: VirtualAddress,
pa: PhysicalAddress,
_writable: bool,
_executable: bool,
) {
for slot in &mut self.mappings {
if slot.is_none() {
*slot = Some(PageMapping {
process,
va: va.raw(),
pa: pa.raw(),
});
return;
}
}
}
}

View File

@@ -3,7 +3,9 @@ mod mock_platform;
mod capability;
mod exit;
mod invariants;
mod ipc;
mod isolation;
mod lifecycle;
mod memory;
mod simple;
@@ -13,7 +15,6 @@ pub use helpers::{kernel_with_process, test_kernel};
pub use mock_platform::MockPlatform;
pub use crate::Kernel;
pub use domain::memory::page_pool::PagePool;
pub use types::address::PhysicalAddress;
pub use types::bounded::Priority;
pub use types::id::ProcessId;

View File

@@ -14,8 +14,7 @@ fn tick_continues_when_time_remaining() {
fn tick_preempts_when_time_expired_and_other_thread_ready() {
let _k = test_kernel();
let mock = MockPlatform::new(1);
let pool = PagePool::new(PhysicalAddress::from_raw(0x8000_0000), 1024);
let mut k = Kernel::new(mock, pool);
let mut k = Kernel::new(mock, PhysicalAddress::from_raw(0x8400_0000), 1024);
// Boot with quantum=2 so it expires after 2 ticks
let (pid, _tid1) = k.boot(Priority::DEFAULT, TimeSlice::new(2)).unwrap();