feat(kernel): kernel-owned memory model

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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