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,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