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:
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);
|
||||
}
|
||||
Reference in New Issue
Block a user