- 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
392 lines
12 KiB
Rust
392 lines
12 KiB
Rust
//! 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);
|
|
}
|