perf(kernel): linked cap tree, translate-based page lookup, next-fit frames/heap, targeted dequeue; host benches

This commit is contained in:
2026-10-27 01:33:26 +01:00
parent 19aeabd86d
commit 2a8a5b0c27
24 changed files with 923 additions and 200 deletions

View File

@@ -2,9 +2,15 @@
//! Delegate, IPC transfer) records its parent; `Revoke` deletes a subtree
//! and deleting a capability re-parents its children, so the tree can only
//! shrink by explicit deletion, never by a middleman stepping out.
//!
//! Each capability records its parent (`Capability::parent`), and
//! `Kernel::tree` holds the same relation as explicit links, so deletion
//! and revocation cost the subtree, not a scan of every capability. Every
//! insertion and removal of a capability goes through this module, which
//! keeps the two in step.
use domain::{
capability::{CapRef, Capability, MAX_CAPS_PER_PROCESS, derivation::descendants},
capability::{CapRef, Capability, MAX_CAPS_PER_PROCESS},
error::KernelError,
process::MAX_PROCESSES,
};
@@ -17,7 +23,7 @@ use types::{
use crate::Kernel;
/// Every capability slot in the system, flattened.
const ALL_CAPS: usize = MAX_PROCESSES * MAX_CAPS_PER_PROCESS;
pub(crate) const ALL_CAPS: usize = MAX_PROCESSES * MAX_CAPS_PER_PROCESS;
fn flat(pid: ProcessId, slot: CapSlot) -> usize {
pid.raw() as usize * MAX_CAPS_PER_PROCESS + slot.value() as usize
@@ -46,47 +52,55 @@ impl<P: Platform> Kernel<P> {
self.cap_ref(r.process, r.slot) == Some(r)
}
/// Stores `cap` in `to`'s table as a child of `parent`.
/// Stores `cap` in `to`'s table as a child of `parent` (a root if
/// `None`).
pub(crate) fn insert_derived(
&mut self,
to: ProcessId,
cap: Capability,
parent: Option<CapRef>,
) -> Result<CapSlot, KernelError> {
self.processes
let parent = parent.filter(|r| self.resolves(*r));
let slot = self
.processes
.get_mut(to)?
.capabilities_mut()
.insert(cap.with_parent(parent))
}
/// Calls `f` with every live capability's flat index and reference.
fn for_each_cap(&self, mut f: impl FnMut(usize, CapRef, &Capability)) {
for p in 0..MAX_PROCESSES {
let pid = ProcessId::from_raw(p as u16);
let Ok(process) = self.processes.get(pid) else {
continue;
};
for (slot, cap) in process.capabilities().iter() {
if let Some(r) = self.cap_ref(pid, slot) {
f(flat(pid, slot), r, cap);
}
}
.insert(cap.with_parent(parent))?;
let linked = self
.tree
.attach(flat(to, slot), parent.map(|r| flat(r.process, r.slot)));
if !linked {
// A slot the tree still thinks is in use: refuse rather than
// let the two disagree.
let _ = self
.processes
.get_mut(to)
.map(|p| p.capabilities_mut().remove(slot));
return Err(KernelError::NoFreeSlot);
}
Ok(slot)
}
/// Removes every region mapping made through `via`.
fn unmap_mappings_via(&mut self, via: CapRef) {
let mut doomed: [Option<RegionId>; domain::memory::MAX_REGIONS] =
[None; domain::memory::MAX_REGIONS];
for (slot, (id, _)) in doomed.iter_mut().zip(
self.regions
.iter()
.filter(|(_, r)| r.mappings().any(|m| m.via == Some(via))),
) {
*slot = Some(id);
self.unmap_mappings_where(|r| r == via);
}
/// Removes every region mapping made through a capability `doomed`
/// picks, in one pass over the regions.
fn unmap_mappings_where(&mut self, doomed: impl Fn(CapRef) -> bool) {
const MAX: usize = domain::memory::MAX_REGIONS * domain::memory::MAX_REGION_MAPPINGS;
let mut found: [Option<(RegionId, ProcessId)>; MAX] = [None; MAX];
let hits = self.regions.iter().flat_map(|(id, r)| {
r.mappings()
.filter(|m| m.via.is_some_and(&doomed))
.map(move |m| (id, m.process))
});
for (slot, hit) in found.iter_mut().zip(hits) {
*slot = Some(hit);
}
for id in doomed.into_iter().flatten() {
self.unmap_region_from(id, via.process);
for (id, pid) in found.into_iter().flatten() {
self.unmap_region_from(id, pid);
}
}
@@ -99,22 +113,17 @@ impl<P: Platform> Kernel<P> {
let grandparent = self.processes.get(pid)?.capabilities().get(slot)?.parent();
self.unmap_mappings_via(me);
let mut children = [false; ALL_CAPS];
self.for_each_cap(|i, _, cap| {
if cap.parent() == Some(me)
&& let Some(c) = children.get_mut(i)
{
*c = true;
}
});
for (i, _) in children.iter().enumerate().filter(|(_, c)| **c) {
if let Some((cpid, cslot)) = unflat(i)
let mut children = [0u16; ALL_CAPS];
let n = self.tree.children(flat(pid, slot), &mut children);
for c in children.iter().take(n) {
if let Some((cpid, cslot)) = unflat(usize::from(*c))
&& let Ok(p) = self.processes.get_mut(cpid)
&& let Ok(child) = p.capabilities_mut().get_mut(cslot)
{
child.set_parent(grandparent);
}
}
self.tree.detach(flat(pid, slot));
self.processes.get_mut(pid)?.capabilities_mut().remove(slot)
}
@@ -128,31 +137,30 @@ impl<P: Platform> Kernel<P> {
if self.cap_ref(pid, slot).is_none() {
return Err(KernelError::InvalidSlot(slot));
}
// Snapshot the tree as parent indices. A parent that no longer
// resolves cannot exist (deletion re-parents), but is treated as
// a root rather than trusted.
let mut parents: [Option<u16>; ALL_CAPS] = [None; ALL_CAPS];
self.for_each_cap(|i, _, cap| {
if let Some(p) = parents.get_mut(i) {
*p = cap
.parent()
.filter(|r| self.resolves(*r))
.map(|r| flat(r.process, r.slot) as u16);
}
});
let root = flat(pid, slot);
let marked = descendants(&parents, root);
for (i, _) in marked.iter().enumerate().filter(|(i, m)| **m && *i != root) {
if let Some((dpid, dslot)) = unflat(i)
&& let Some(r) = self.cap_ref(dpid, dslot)
{
self.unmap_mappings_via(r);
let mut doomed = [0u16; ALL_CAPS];
let n = self.tree.descendants(root, &mut doomed);
let mut marked = [false; ALL_CAPS];
for d in doomed.iter().take(n) {
if let Some(m) = marked.get_mut(usize::from(*d)) {
*m = true;
}
}
self.unmap_mappings_where(|r| {
marked
.get(flat(r.process, r.slot))
.copied()
.unwrap_or(false)
});
for d in doomed.iter().take(n) {
if let Some((dpid, dslot)) = unflat(usize::from(*d)) {
let _ = self
.processes
.get_mut(dpid)
.map(|p| p.capabilities_mut().remove(dslot));
}
}
self.tree.remove_descendants(root);
Ok(())
}

View File

@@ -68,6 +68,10 @@ pub struct Kernel<P: Platform> {
pending_results: [Option<SyscallResult>; MAX_THREADS],
/// The core each thread runs on (see smp.rs).
homes: [types::id::CoreId; MAX_THREADS],
/// The capability derivation tree as links (see captree.rs).
tree: domain::capability::tree::CapTree<{ captree::ALL_CAPS }>,
/// Where each process's next heap search starts (see memory.rs).
heap_cursors: [usize; MAX_PROCESSES],
}
impl<P: Platform> Kernel<P> {
@@ -87,6 +91,8 @@ impl<P: Platform> Kernel<P> {
ipc: [syscall::IpcState::default(); MAX_THREADS],
pending_results: [const { None }; MAX_THREADS],
homes: [types::id::CoreId::from_raw(0); MAX_THREADS],
tree: domain::capability::tree::CapTree::new(),
heap_cursors: [0; MAX_PROCESSES],
}
}
@@ -120,6 +126,8 @@ impl<P: Platform> Kernel<P> {
addr_of_mut!((*slot).ipc).write([syscall::IpcState::default(); MAX_THREADS]);
addr_of_mut!((*slot).pending_results).write([const { None }; MAX_THREADS]);
addr_of_mut!((*slot).homes).write([types::id::CoreId::from_raw(0); MAX_THREADS]);
addr_of_mut!((*slot).tree).write(domain::capability::tree::CapTree::new());
addr_of_mut!((*slot).heap_cursors).write([0; MAX_PROCESSES]);
}
}
@@ -141,10 +149,7 @@ impl<P: Platform> Kernel<P> {
PermissionSet::empty().with(Permission::Execute),
Generation::initial(),
);
self.processes
.get_mut(pid)?
.capabilities_mut()
.insert(cap)?;
self.insert_derived(pid, cap, None)?;
let core = self.platform.core_id();
self.set_home(tid, core);
@@ -285,8 +290,14 @@ impl<P: Platform> Kernel<P> {
pub(crate) fn cleanup_threads(&mut self, thread_ids: &[Option<ThreadId>]) {
use domain::scheduler::Scheduler;
for tid in thread_ids.iter().flatten() {
self.endpoints.remove_thread_from_all(*tid);
self.notifications.remove_thread_from_all(*tid);
self.leave_queue(*tid);
if let Some(n) = self
.ipc(*tid)
.and_then(|s| s.bound)
.and_then(|n| self.notifications.get_mut(n))
{
n.remove_thread(*tid);
}
let ipc = self.ipc_mut(*tid).map(core::mem::take).unwrap_or_default();
if let Some(caller) = ipc.reply_to {
self.release_caller(caller);
@@ -298,6 +309,29 @@ impl<P: Platform> Kernel<P> {
}
}
/// Takes a blocked thread off the one queue its block reason names, so
/// nothing needs to scan every endpoint and notification.
pub(crate) fn leave_queue(&mut self, tid: ThreadId) {
use domain::thread::BlockReason::*;
let Ok(ThreadState::Blocked(reason)) = self.threads.get(tid).map(|t| *t.state()) else {
return;
};
match reason {
Sending(ep) | Calling(ep) => {
self.endpoints.get_mut(ep).map(|e| e.remove_sender(tid));
}
Receiving(ep) => {
self.endpoints.get_mut(ep).map(|e| e.remove_receiver(tid));
}
WaitingNotification(n) => {
if let Some(n) = self.notifications.get_mut(n) {
n.remove_waiter(tid);
}
}
AwaitingReply => {}
}
}
/// Destroys every endpoint `owner` created, waking anyone blocked on
/// them with an error instead of leaving them blocked forever.
pub(crate) fn destroy_endpoints_owned_by(&mut self, owner: ProcessId) {
@@ -387,6 +421,6 @@ impl<P: Platform> Kernel<P> {
.resource_generation(&resource)
.ok_or(KernelError::NoSuchObject)?;
let cap = Capability::new(resource, permissions, generation);
self.processes.get_mut(to)?.capabilities_mut().insert(cap)
self.insert_derived(to, cap, None)
}
}

View File

@@ -70,8 +70,19 @@ impl<P: Platform> Kernel<P> {
&& end <= P::USER_VA_END
}
/// `pid`'s user page at `va`: the platform's page table says where it
/// points, the frame table confirms the frame really is that page.
fn user_frame(
&self,
pid: ProcessId,
va: VirtualAddress,
) -> Option<(PhysicalAddress, PagePerms)> {
let pa = self.platform.translate(pid, va)?;
self.frames.user_at(pa, pid, va).map(|perms| (pa, perms))
}
fn is_mapped(&self, pid: ProcessId, va: VirtualAddress) -> bool {
self.frames.find_user(pid, va).is_some() || self.regions.covering(pid, va).is_some()
self.user_frame(pid, va).is_some() || self.regions.covering(pid, va).is_some()
}
fn range_is_free(&self, pid: ProcessId, va: VirtualAddress, pages: usize) -> bool {
@@ -173,7 +184,7 @@ impl<P: Platform> Kernel<P> {
/// 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) {
if let Some((pa, _)) = self.user_frame(pid, va) {
self.platform.unmap_page(pid, va);
self.frames.free(pa);
if let Some(q) = self.quotas.get_mut(pid.raw() as usize) {
@@ -185,7 +196,7 @@ impl<P: Platform> Kernel<P> {
/// 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)
self.user_frame(pid, va).map(|(pa, _)| pa)
}
/// Maps `pages` fresh zeroed pages at `va`, all or nothing.
@@ -213,13 +224,35 @@ impl<P: Platform> Kernel<P> {
Ok(())
}
/// First-fit search for `pages` free pages in the heap part of the window.
/// Next-fit search for `pages` free pages in the heap part of the
/// window: from just past `pid`'s previous request, then once more from
/// the bottom. Requests in a row cost O(pages) each instead of
/// rescanning everything allocated before.
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);
let bottom = USER_HEAP_START.max(P::USER_VA_START);
let cursor = self
.heap_cursors
.get(pid.raw() as usize)
.copied()
.unwrap_or(0)
.max(bottom);
self.first_fit_from(pid, cursor, pages)
.or_else(|| self.first_fit_from(pid, bottom, pages))
}
/// Records where `pid`'s next heap search starts.
pub(crate) fn advance_heap_cursor(&mut self, pid: ProcessId, past: VirtualAddress) {
if let Some(c) = self.heap_cursors.get_mut(pid.raw() as usize) {
*c = past.raw();
}
}
fn first_fit_from(&self, pid: ProcessId, from: usize, pages: usize) -> Option<VirtualAddress> {
let mut candidate = from;
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))) {
@@ -241,7 +274,7 @@ impl<P: Platform> Kernel<P> {
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()) {
if !(0..pages).all(|i| self.user_frame(pid, page_at(va, i)).is_some()) {
return Err(KernelError::InvalidAddress);
}
for i in 0..pages {
@@ -264,8 +297,7 @@ impl<P: Platform> Kernel<P> {
return Err(KernelError::InvalidAddress);
}
let (pa, _) = self
.frames
.find_user(from, src)
.user_frame(from, src)
.ok_or(KernelError::InvalidAddress)?;
self.processes.get(to)?;
if !self.range_is_free(to, dst, 1) {
@@ -478,6 +510,9 @@ impl<P: Platform> Kernel<P> {
if let Some(q) = self.quotas.get_mut(pid.raw() as usize) {
*q = Quota::default();
}
if let Some(c) = self.heap_cursors.get_mut(pid.raw() as usize) {
*c = 0;
}
}
}

View File

@@ -35,6 +35,9 @@ impl Platform for KaniPlatform {
Ok(())
}
fn unmap_page(&mut self, _process: ProcessId, _va: VirtualAddress) {}
fn translate(&self, _process: ProcessId, _va: VirtualAddress) -> Option<PhysicalAddress> {
None
}
fn zero_frame(&mut self, _pa: PhysicalAddress) {}
fn enable_user_mode(&mut self, _entry: EntryPoint, _stack: StackPointer) -> ! {
kani::assume(false);

View File

@@ -35,17 +35,17 @@ impl<P: Platform> Kernel<P> {
/// Homes a new thread on the core with the fewest live threads.
pub(crate) fn place(&mut self, tid: ThreadId) {
let cores = self.scheduler.num_cores().max(1);
let load = |core: CoreId| {
self.threads
.iter()
.filter(|(t, _)| *t != tid && self.home(*t) == core)
.count()
};
let cores = usize::from(self.scheduler.num_cores().max(1));
let mut load = [0usize; u8::MAX as usize + 1];
for (t, _) in self.threads.iter().filter(|(t, _)| *t != tid) {
if let Some(n) = load.get_mut(usize::from(self.home(t).raw())) {
*n += 1;
}
}
let best = (0..cores)
.map(CoreId::from_raw)
.min_by_key(|c| load(*c))
.unwrap_or(CoreId::from_raw(0));
.min_by_key(|c| load.get(*c).copied().unwrap_or(usize::MAX))
.and_then(|c| u8::try_from(c).ok())
.map_or(CoreId::from_raw(0), CoreId::from_raw);
self.set_home(tid, best);
}

View File

@@ -449,7 +449,7 @@ impl<P: Platform> Kernel<P> {
self.wake(t, SyscallResult::Received(Delivery::signal(bits)));
}
SignalEffect::WakeBound(t, bits) => {
self.endpoints.remove_thread_from_all(t);
self.leave_queue(t);
self.wake(t, SyscallResult::Received(Delivery::signal(bits)));
}
SignalEffect::Stored => {}

View File

@@ -27,8 +27,14 @@ impl<P: Platform> Kernel<P> {
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()))
self.map_fresh_range(pid, va, pages, PagePerms::ReadWrite)?;
self.advance_heap_cursor(
pid,
VirtualAddress::from_raw(
va.raw() + pages * domain::memory::frame::PAGE_SIZE,
),
);
Ok(SyscallResult::Value(va.raw()))
}),
)
}

View File

@@ -0,0 +1,183 @@
//! Worst-case timings of kernel paths that scan tables. Not run by default:
//! `cargo test -p kernel --lib bench -- --ignored --nocapture --test-threads 1`.
//! Numbers are host nanoseconds at the test profile's opt-level, good for
//! comparing before and after a change, not for absolute claims.
extern crate std;
use super::*;
use crate::{SyscallOutcome, SyscallResult};
use domain::{capability::Capability, message::Message, resource::Resource, syscall::Syscall};
use std::time::Instant;
use types::{
bounded::{CapSlot, PageCount},
generation::Generation,
permission::{Permission, PermissionSet},
};
fn all() -> PermissionSet {
PermissionSet::empty()
.with(Permission::Read)
.with(Permission::Write)
.with(Permission::Execute)
.with(Permission::Delegate)
}
fn report(name: &str, ops: usize, start: Instant) {
let ns = start.elapsed().as_nanos() as f64 / ops as f64;
std::eprintln!("BENCH {name}: {ns:.0} ns/op ({ops} ops)");
}
/// `n` processes besides init, each with a full capability table derived
/// from one cap in init, so deleting or revoking walks the whole space.
fn crowded(
n: usize,
) -> (
Kernel<MockPlatform>,
ProcessId,
CapSlot,
std::vec::Vec<ProcessId>,
) {
let (mut k, init) = kernel_with_process();
let ep = k.create_endpoint(init).unwrap();
let root = k.grant_cap_for_test(init, Resource::Endpoint(ep), all());
let parent = k.cap_ref(init, root);
let mut pids = std::vec::Vec::new();
for _ in 0..n {
let Some(pid) = k.create_process_internal() else {
break;
};
while k
.insert_derived(
pid,
Capability::new(Resource::Endpoint(ep), all(), Generation::initial()),
parent,
)
.is_ok()
{}
pids.push(pid);
}
(k, init, root, pids)
}
#[test]
#[ignore]
fn bench_destroy_crowded_processes() {
let (mut k, _, _, pids) = crowded(60);
let start = Instant::now();
for pid in &pids {
k.destroy_process(*pid);
}
report("destroy process with 64 derived caps", pids.len(), start);
}
#[test]
#[ignore]
fn bench_revoke_whole_space() {
let mut total = 0;
let mut runs = 0;
for _ in 0..5 {
let (mut k, init, root, pids) = crowded(60);
let start = Instant::now();
k.revoke_subtree(init, root).unwrap();
total += start.elapsed().as_nanos();
runs += 1;
assert!(
pids.iter()
.all(|p| k.cap_slots_for_test(*p).iter().all(|s| !s))
);
}
std::eprintln!(
"BENCH revoke ~3800 caps across 60 processes: {:.0} ns/op ({runs} ops)",
total as f64 / runs as f64
);
}
#[test]
#[ignore]
fn bench_drop_cap_in_crowded_space() {
let (mut k, _, _, pids) = crowded(60);
let victim = pids[0];
let start = Instant::now();
let mut ops = 0;
for slot in 0..=CapSlot::MAX {
if k.delete_cap(victim, CapSlot::new(slot).unwrap()).is_ok() {
ops += 1;
}
}
report("delete one cap among ~3800", ops, start);
}
#[test]
#[ignore]
fn bench_request_pages_until_full() {
let (mut k, _) = kernel_with_process();
let start = Instant::now();
let mut ops = 0;
while let SyscallOutcome::Complete(SyscallResult::Value(_)) =
k.handle_syscall(Syscall::RequestPages {
count: PageCount::new(1).unwrap(),
})
{
ops += 1;
}
report(
"RequestPages(1) until the pool or quota runs out",
ops,
start,
);
}
#[test]
#[ignore]
fn bench_ipc_round_trip() {
let (mut k, init) = kernel_with_process();
let client = k.current_thread().unwrap();
let server = k.spawn_thread_for_test(init, Priority::DEFAULT, TimeSlice::DEFAULT);
let ep = k.create_endpoint(init).unwrap();
let slot = k.grant_cap_for_test(init, Resource::Endpoint(ep), all());
k.switch_to_thread_for_test(server);
assert!(matches!(
k.handle_syscall(Syscall::Receive { endpoint: slot }),
SyscallOutcome::BlockCaller(_)
));
let _ = k
.pick_next_thread()
.map(|t| k.set_thread_running(t, types::id::CoreId::from_raw(0)));
assert_eq!(k.current_thread(), Some(client));
let rounds = 20_000;
let start = Instant::now();
for _ in 0..rounds {
let out = k.handle_syscall(Syscall::Call {
endpoint: slot,
message: Message::empty(),
});
assert!(matches!(out, SyscallOutcome::SwitchTo(t) if t == server));
let out = k.handle_syscall(Syscall::ReplyRecv {
endpoint: slot,
message: Message::empty(),
});
assert!(matches!(out, SyscallOutcome::BlockCaller(_)));
let next = k.pick_next_thread().unwrap();
k.set_thread_running(next, types::id::CoreId::from_raw(0))
.unwrap();
assert_eq!(next, client);
}
report("Call + ReplyRecv round trip", rounds, start);
}
#[test]
#[ignore]
fn bench_create_spawn_kill() {
let (mut k, _) = kernel_with_process();
let rounds = 2_000;
let start = Instant::now();
for _ in 0..rounds {
let pid = k.create_process_internal().unwrap();
k.spawn_thread_internal(pid).unwrap();
k.destroy_process(pid);
}
report("create process + spawn thread + destroy", rounds, start);
}

View File

@@ -93,12 +93,17 @@ impl Model {
}
}
/// Sorted, so a seed always means the same run (HashMap order is
/// random per process).
fn held_by(&self, pid: ProcessId) -> Vec<(usize, CapSlot)> {
self.alive
let mut held: Vec<(usize, CapSlot)> = self
.alive
.iter()
.filter(|(_, (p, _))| *p == pid)
.map(|(id, (_, s))| (*id, *s))
.collect()
.collect();
held.sort_by_key(|(id, _)| *id);
held
}
}
@@ -302,7 +307,14 @@ fn newly_used(before: [bool; 64], after: [bool; 64]) -> CapSlot {
#[test]
fn derivation_tree_matches_the_model() {
for seed in [5u64, 0xBADC0DE, 31337, 0x5EED, 2024] {
let extra: u64 = std::env::var("MODEL_SEEDS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
for seed in [5u64, 0xBADC0DE, 31337, 0x5EED, 2024]
.into_iter()
.chain(1000..1000 + extra)
{
let mut world = World::new();
let mut rng = Rng(seed);
let mut revokes = 0;
@@ -314,6 +326,9 @@ fn derivation_tree_matches_the_model() {
}
world.check();
}
if seed >= 1000 {
continue; // extra seeds hunt for mismatches, not coverage
}
assert!(
revokes > 20,
"seed {seed:#x}: too little deletion exercised"

View File

@@ -1,8 +1,10 @@
use super::*;
use crate::{SyscallOutcome, SyscallResult};
use domain::syscall::Syscall;
use platform::Platform;
use types::address::VirtualAddress;
use types::bounded::PageCount;
use types::mapping::PagePerms;
#[test]
fn request_pages_returns_user_va() {
@@ -53,3 +55,78 @@ fn release_pages_returns_the_data_pages() {
assert_eq!(k.free_page_count(), free_before + 4);
assert_eq!(k.page_usage(pid), used_before - 4);
}
fn request(k: &mut Kernel<MockPlatform>, pages: u16) -> Option<usize> {
match k.handle_syscall(Syscall::RequestPages {
count: PageCount::new(pages).unwrap(),
}) {
SyscallOutcome::Complete(SyscallResult::Value(va)) => Some(va),
_ => None,
}
}
#[test]
fn a_lying_page_table_cannot_hand_out_another_process_frame() {
// The kernel finds a page through the platform's page table but only
// believes the answer if its own frame table agrees.
let (mut k, a) = kernel_with_process();
let b = k.create_process_internal().unwrap();
k.map_fresh_range(
b,
VirtualAddress::from_raw(0x50000),
1,
PagePerms::ReadWrite,
)
.unwrap();
let b_frame = k
.user_page_frame(b, VirtualAddress::from_raw(0x50000))
.unwrap();
let pages_before = k.page_usage(b);
// A's page at 0x40000 is claimed to be B's frame.
k.platform_mut().lie_about(a, 0x40000, b_frame.raw());
assert_eq!(
k.user_page_frame(a, VirtualAddress::from_raw(0x40000)),
None
);
let out = k.handle_syscall(Syscall::ReleasePages {
addr: VirtualAddress::from_raw(0x40000),
count: PageCount::new(1).unwrap(),
});
assert!(matches!(
out,
SyscallOutcome::Complete(SyscallResult::Error(_))
));
assert_eq!(
k.user_page_frame(b, VirtualAddress::from_raw(0x50000)),
Some(b_frame)
);
assert_eq!(k.page_usage(b), pages_before);
// Nor is B's frame A's under a lie about the very page B uses.
k.platform_mut().lie_about(a, 0x50000, b_frame.raw());
assert_eq!(
k.user_page_frame(a, VirtualAddress::from_raw(0x50000)),
None
);
// And a lie about the page number of B's own frame is caught too.
k.platform_mut().lie_about(b, 0x60000, b_frame.raw());
assert_eq!(
k.user_page_frame(b, VirtualAddress::from_raw(0x60000)),
None
);
}
#[test]
fn the_heap_search_wraps_back_to_the_bottom() {
let (mut k, pid) = kernel_with_process();
let first = request(&mut k, 1).unwrap();
k.advance_heap_cursor(
pid,
VirtualAddress::from_raw(MockPlatform::USER_VA_END - 4096),
);
let top = request(&mut k, 1).expect("the last page of the window");
assert_eq!(top, MockPlatform::USER_VA_END - 4096);
let again = request(&mut k, 2).expect("wraps to the bottom");
assert!(again < top && again != first);
}

View File

@@ -1,4 +1,7 @@
extern crate std;
use platform::{FrameSource, MapError, Platform};
use std::collections::{BTreeMap, BTreeSet};
use types::{
address::{EntryPoint, PhysicalAddress, StackPointer, VirtualAddress},
id::{CoreId, IrqId, ProcessId},
@@ -6,27 +9,10 @@ use types::{
time::{Ticks, TimerDuration},
};
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 {
@@ -34,9 +20,15 @@ pub struct MockPlatform {
core: CoreId,
cores: u8,
spaces: [bool; MAX_SPACES],
mappings: [Option<PageMapping>; MAX_MAPPINGS],
tables: [Option<LeafTable>; MAX_TABLES],
zeroed: [Option<usize>; MAX_ZEROED],
/// (process, va) -> (pa, perms). Ordered, so iteration is the same
/// on every run.
mappings: BTreeMap<(u16, usize), (usize, PagePerms)>,
/// (process, 2 MiB span) with a leaf table.
tables: BTreeSet<(u16, usize)>,
zeroed: BTreeSet<usize>,
/// Answers `translate` gives instead of the truth, to test that the
/// kernel never trusts them.
lies: BTreeMap<(u16, usize), usize>,
/// Cores an IPI was sent to and that have not taken it yet.
ipis: [bool; 8],
}
@@ -48,9 +40,10 @@ impl MockPlatform {
core: CoreId::from_raw(0),
cores: num_cores,
spaces: [false; MAX_SPACES],
mappings: [None; MAX_MAPPINGS],
tables: [None; MAX_TABLES],
zeroed: [None; MAX_ZEROED],
mappings: BTreeMap::new(),
tables: BTreeSet::new(),
zeroed: BTreeSet::new(),
lies: BTreeMap::new(),
ipis: [false; 8],
}
}
@@ -74,34 +67,25 @@ impl MockPlatform {
/// 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)
self.mappings.get(&(process.raw(), va)).map(|(pa, _)| *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))
.map(|((p, va), (pa, _))| (ProcessId::from_raw(*p), *va, *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)
.get(&(process.raw(), va))
.map(|(_, perms)| *perms)
}
pub fn mapping_count(&self, process: ProcessId) -> usize {
self.mappings
.iter()
.flatten()
.filter(|m| m.process == process)
.range((process.raw(), 0)..=(process.raw(), usize::MAX))
.count()
}
@@ -110,7 +94,12 @@ impl MockPlatform {
}
pub fn was_zeroed(&self, pa: usize) -> bool {
self.zeroed.contains(&Some(pa))
self.zeroed.contains(&pa)
}
/// From now on `translate(process, va)` answers `pa`.
pub fn lie_about(&mut self, process: ProcessId, va: usize, pa: usize) {
self.lies.insert((process.raw(), va), pa);
}
pub fn set_ticks(&mut self, ticks: Ticks) {
@@ -135,16 +124,9 @@ impl Platform for MockPlatform {
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;
}
}
let p = process.raw();
self.mappings.retain(|(owner, _), _| *owner != p);
self.tables.retain(|(owner, _)| *owner != p);
}
fn map_page(
@@ -163,59 +145,28 @@ impl Platform for MockPlatform {
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 {
if !self.tables.contains(&(process.raw(), span)) {
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);
}
self.tables.insert((process.raw(), span));
}
self.mappings.insert((process.raw(), va), (pa.raw(), perms));
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;
}
}
self.mappings.remove(&(process.raw(), va.raw()));
}
fn translate(&self, process: ProcessId, va: VirtualAddress) -> Option<PhysicalAddress> {
self.lies
.get(&(process.raw(), va.raw()))
.copied()
.or_else(|| self.mapping(process, va.raw()))
.map(PhysicalAddress::from_raw)
}
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());
}
self.zeroed.insert(pa.raw());
}
fn enable_user_mode(&mut self, _entry: EntryPoint, _stack: StackPointer) -> ! {

View File

@@ -2,6 +2,7 @@ mod helpers;
mod mock_platform;
mod abi;
mod bench;
mod board;
mod capability;
mod captree_model;

View File

@@ -96,6 +96,31 @@ impl Kernel<MockPlatform> {
}
}
}
// The tree's links say exactly what the capabilities' parents say.
for i in 0..crate::captree::ALL_CAPS {
let pid = ProcessId::from_raw((i / domain::capability::MAX_CAPS_PER_PROCESS) as u16);
let slot = CapSlot::new((i % domain::capability::MAX_CAPS_PER_PROCESS) as u8).unwrap();
let cap = self
.processes
.get(pid)
.ok()
.and_then(|p| p.capabilities().get(slot).ok());
if cap.is_some() != self.tree.is_live(i) {
return Err(format!(
"{pid:?}/{slot:?}: tree and table disagree on liveness"
));
}
let recorded = cap.and_then(|c| c.parent()).map(|r| {
r.process.raw() as usize * domain::capability::MAX_CAPS_PER_PROCESS
+ r.slot.value() as usize
});
if cap.is_some() && recorded != self.tree.parent(i) {
return Err(format!(
"{pid:?}/{slot:?}: tree parent {:?}, cap parent {recorded:?}",
self.tree.parent(i)
));
}
}
for (_, region) in self.regions.iter() {
let has = |want| region.mappings().any(|m| m.perms == want);
if has(PagePerms::ReadWrite) && has(PagePerms::ReadExecute) {