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:
@@ -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
422
crates/kernel/src/memory.rs
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
261
crates/kernel/src/tests/invariants.rs
Normal file
261
crates/kernel/src/tests/invariants.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
391
crates/kernel/src/tests/isolation.rs
Normal file
391
crates/kernel/src/tests/isolation.rs
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user