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
Cargo.lock
generated
1
Cargo.lock
generated
@@ -245,7 +245,6 @@ dependencies = [
|
||||
name = "platform"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"domain",
|
||||
"types",
|
||||
]
|
||||
|
||||
|
||||
@@ -123,3 +123,10 @@ panic = "abort"
|
||||
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
# The randomized kernel invariant tests are too slow unoptimized.
|
||||
[profile.test.package.kernel]
|
||||
opt-level = 2
|
||||
|
||||
[profile.test.package.domain]
|
||||
opt-level = 2
|
||||
|
||||
1
Makefile
1
Makefile
@@ -15,6 +15,7 @@ kernel-fmt:
|
||||
|
||||
kernel-clippy:
|
||||
cargo clippy $(KERNEL_CRATES) --all-targets -- -D warnings
|
||||
cargo clippy -p adapter-riscv --target $(RISCV_TARGET) --no-deps -- -D warnings
|
||||
|
||||
kernel-test:
|
||||
cargo test $(KERNEL_CRATES)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::hw::uart::Uart;
|
||||
use crate::hw::virtio_blk::VirtioBlk;
|
||||
use crate::{KERNEL, board, kernel_cell::KernelCell, mmu, platform, trap};
|
||||
use crate::{KERNEL, board, mmu, platform, trap};
|
||||
|
||||
unsafe extern "C" {
|
||||
static _bss_start: u8;
|
||||
@@ -22,12 +22,21 @@ pub fn init(uart: &Uart) -> &'static mut mmu::PageTable {
|
||||
crate::hw::plic::Plic::init();
|
||||
uart.write_str("k-os: traps and timer initialized\n");
|
||||
|
||||
if board::USER_POOL_PAGES > domain::memory::frame::MAX_FRAMES {
|
||||
crate::halt(uart, "user pool larger than the frame table");
|
||||
}
|
||||
let riscv_platform = platform::RiscVPlatform::new(kernel_pt as *const mmu::PageTable as usize);
|
||||
let pool = domain::memory::page_pool::PagePool::new(
|
||||
// SAFETY: `init_in_place` writes every field of the kernel.
|
||||
unsafe {
|
||||
KERNEL.init_with(|slot| {
|
||||
kernel::Kernel::init_in_place(
|
||||
slot,
|
||||
riscv_platform,
|
||||
types::address::PhysicalAddress::from_raw(board::USER_POOL_BASE),
|
||||
board::USER_POOL_PAGES,
|
||||
);
|
||||
KERNEL.init(kernel::Kernel::new(riscv_platform, pool));
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
kernel_pt
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
use crate::hw::uart::Uart;
|
||||
use crate::hw::virtio_blk::VirtioBlk;
|
||||
use crate::{boot_fs, mmu};
|
||||
use types::address::VirtualAddress;
|
||||
use types::id::{ProcessId, ThreadId};
|
||||
use types::mapping::PagePerms;
|
||||
|
||||
const PAGE_SIZE: usize = mmu::PAGE_SIZE;
|
||||
const USER_VA_BASE: usize = 0x10000;
|
||||
/// The stack sits just below the image and stops short of the null page.
|
||||
const USER_STACK_PAGES: usize = USER_VA_BASE / PAGE_SIZE - 1;
|
||||
const MAX_FILE_SIZE: usize = 64 * 1024;
|
||||
|
||||
pub struct LoadedProcess {
|
||||
@@ -15,11 +20,23 @@ fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
|
||||
unsafe { &mut *crate::kernel_ptr() }
|
||||
}
|
||||
|
||||
pub fn load_init(
|
||||
uart: &Uart,
|
||||
blk: &mut VirtioBlk,
|
||||
kernel_pt: &mmu::PageTable,
|
||||
) -> (LoadedProcess, ThreadId) {
|
||||
/// Scratch space for reading ELF files, allocated once from the boot heap.
|
||||
fn elf_buffer() -> &'static mut [u8] {
|
||||
static mut BUF: usize = 0;
|
||||
// SAFETY: boot is single threaded; the buffer is kernel-only memory
|
||||
// from the boot allocator and is never handed to a process.
|
||||
unsafe {
|
||||
if BUF == 0 {
|
||||
BUF = mmu::allocator::alloc_zeroed_page();
|
||||
for _ in 1..MAX_FILE_SIZE / PAGE_SIZE {
|
||||
mmu::allocator::alloc_zeroed_page();
|
||||
}
|
||||
}
|
||||
core::slice::from_raw_parts_mut(BUF as *mut u8, MAX_FILE_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_init(uart: &Uart, blk: &mut VirtioBlk) -> (LoadedProcess, ThreadId) {
|
||||
let (pid, tid) = kern()
|
||||
.boot(
|
||||
types::bounded::Priority::DEFAULT,
|
||||
@@ -27,10 +44,7 @@ pub fn load_init(
|
||||
)
|
||||
.unwrap_or_else(|_| crate::halt(uart, "boot failed"));
|
||||
|
||||
let entry = load_elf(blk, kernel_pt, pid, b"init.elf");
|
||||
if entry == 0 {
|
||||
crate::halt(uart, "init.elf not found");
|
||||
}
|
||||
let entry = load_elf(uart, blk, pid, b"init.elf");
|
||||
setup_thread(tid, entry);
|
||||
uart.write_str("k-os: init loaded\n");
|
||||
|
||||
@@ -40,17 +54,13 @@ pub fn load_init(
|
||||
pub fn load_server(
|
||||
uart: &Uart,
|
||||
blk: &mut VirtioBlk,
|
||||
kernel_pt: &mmu::PageTable,
|
||||
filename: &[u8],
|
||||
label: &str,
|
||||
) -> LoadedProcess {
|
||||
let pid = kern()
|
||||
.create_process_internal()
|
||||
.unwrap_or_else(|| crate::halt(uart, "process create failed"));
|
||||
let entry = load_elf(blk, kernel_pt, pid, filename);
|
||||
if entry == 0 {
|
||||
crate::halt(uart, "elf not found");
|
||||
}
|
||||
let entry = load_elf(uart, blk, pid, filename);
|
||||
let tid = kern()
|
||||
.spawn_thread_internal(pid)
|
||||
.unwrap_or_else(|| crate::halt(uart, "thread spawn failed"));
|
||||
@@ -98,100 +108,89 @@ fn setup_thread(tid: ThreadId, entry: usize) {
|
||||
}
|
||||
}
|
||||
|
||||
fn load_elf(
|
||||
blk: &mut VirtioBlk,
|
||||
kernel_pt: &mmu::PageTable,
|
||||
pid: ProcessId,
|
||||
filename: &[u8],
|
||||
) -> usize {
|
||||
let buf_base = mmu::allocator::alloc_zeroed_page();
|
||||
let file_pages = MAX_FILE_SIZE / 4096;
|
||||
for _ in 1..file_pages {
|
||||
mmu::allocator::alloc_zeroed_page();
|
||||
}
|
||||
let buf = unsafe { core::slice::from_raw_parts_mut(buf_base as *mut u8, MAX_FILE_SIZE) };
|
||||
|
||||
let file_size = boot_fs::read_file_from_disk(blk, filename, buf).unwrap_or(0);
|
||||
if file_size == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let elf_info = match elf::parse(&buf[..file_size]) {
|
||||
Ok(info) => info,
|
||||
Err(_) => return 0,
|
||||
/// Copies `data` into `pid`'s already mapped pages starting at `va`.
|
||||
fn copy_to_user(pid: ProcessId, va: usize, data: &[u8]) -> bool {
|
||||
let mut done = 0;
|
||||
while done < data.len() {
|
||||
let addr = va + done;
|
||||
let page = addr & !(PAGE_SIZE - 1);
|
||||
let offset = addr - page;
|
||||
let Some(pa) = kern().user_page_frame(pid, VirtualAddress::from_raw(page)) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let user_pt = mmu::create_user_page_table(
|
||||
kernel_pt,
|
||||
types::address::PhysicalAddress::from_raw(0),
|
||||
0,
|
||||
types::address::VirtualAddress::from_raw(USER_VA_BASE),
|
||||
);
|
||||
|
||||
for segment in elf_info.segments() {
|
||||
let page_start = segment.vaddr & !0xFFF;
|
||||
let offset_in_page = segment.vaddr & 0xFFF;
|
||||
let total_size = offset_in_page + segment.mem_size;
|
||||
let seg_pages = (total_size + 4095) / 4096;
|
||||
|
||||
for p in 0..seg_pages {
|
||||
let page = mmu::allocator::alloc_zeroed_page();
|
||||
let page_va = page_start + p * 4096;
|
||||
let seg_file_start = if p == 0 { 0 } else { p * 4096 - offset_in_page };
|
||||
let dest_off = if p == 0 { offset_in_page } else { 0 };
|
||||
let copy_len = if seg_file_start < segment.file_size {
|
||||
(segment.file_size - seg_file_start).min(4096 - dest_off)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if copy_len > 0 {
|
||||
let src = segment.file_offset + seg_file_start;
|
||||
if src + copy_len <= file_size {
|
||||
let len = (PAGE_SIZE - offset).min(data.len() - done);
|
||||
// SAFETY: `pa` is a pool frame the kernel just mapped for `pid`;
|
||||
// pool frames are identity mapped kernel RAM.
|
||||
unsafe {
|
||||
core::ptr::copy_nonoverlapping(
|
||||
buf.as_ptr().add(src),
|
||||
(page as *mut u8).add(dest_off),
|
||||
copy_len,
|
||||
data.as_ptr().add(done),
|
||||
(pa.raw() as *mut u8).add(offset),
|
||||
len,
|
||||
);
|
||||
}
|
||||
done += len;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
let flags = if segment.flags.execute {
|
||||
mmu::Sv39Flags::user_rx()
|
||||
} else if segment.flags.write {
|
||||
mmu::Sv39Flags::user_rw()
|
||||
} else {
|
||||
mmu::Sv39Flags::user_rx()
|
||||
};
|
||||
mmu::map(
|
||||
user_pt,
|
||||
mmu::Mapping {
|
||||
va: types::address::VirtualAddress::from_raw(page_va),
|
||||
pa: types::address::PhysicalAddress::from_raw(page),
|
||||
pages: 1,
|
||||
flags,
|
||||
},
|
||||
);
|
||||
}
|
||||
fn segment_perms(flags: elf::SegmentFlags) -> Option<PagePerms> {
|
||||
match (flags.write, flags.execute) {
|
||||
(true, true) => None,
|
||||
(false, true) => Some(PagePerms::ReadExecute),
|
||||
(true, false) => Some(PagePerms::ReadWrite),
|
||||
(false, false) => Some(PagePerms::ReadOnly),
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..16usize {
|
||||
let sp = mmu::allocator::alloc_zeroed_page();
|
||||
mmu::map(
|
||||
user_pt,
|
||||
mmu::Mapping {
|
||||
va: types::address::VirtualAddress::from_raw(USER_VA_BASE - (16 - i) * 4096),
|
||||
pa: types::address::PhysicalAddress::from_raw(sp),
|
||||
pages: 1,
|
||||
flags: mmu::Sv39Flags::user_rw(),
|
||||
},
|
||||
);
|
||||
/// Loads `filename` into `pid` through the kernel's own mapping API, so
|
||||
/// every page is tracked, charged and checked like any other. Halts on any
|
||||
/// malformed or overlapping image: boot servers are trusted to be sane.
|
||||
fn load_elf(uart: &Uart, blk: &mut VirtioBlk, pid: ProcessId, filename: &[u8]) -> usize {
|
||||
let buf = elf_buffer();
|
||||
let file_size = boot_fs::read_file_from_disk(blk, filename, buf).unwrap_or(0);
|
||||
if file_size == 0 {
|
||||
crate::halt(uart, "ELF not found");
|
||||
}
|
||||
let file = &buf[..file_size];
|
||||
let elf_info = elf::parse(file).unwrap_or_else(|_| crate::halt(uart, "bad ELF"));
|
||||
|
||||
for segment in elf_info.segments() {
|
||||
let perms =
|
||||
segment_perms(segment.flags).unwrap_or_else(|| crate::halt(uart, "W+X segment"));
|
||||
let page_start = segment.vaddr & !(PAGE_SIZE - 1);
|
||||
let offset_in_page = segment.vaddr - page_start;
|
||||
let pages = offset_in_page
|
||||
.checked_add(segment.mem_size)
|
||||
.map(|total| total.div_ceil(PAGE_SIZE))
|
||||
.unwrap_or_else(|| crate::halt(uart, "segment too large"));
|
||||
if pages == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
kern()
|
||||
.platform_mut()
|
||||
.set_page_table(pid, user_pt as *const mmu::PageTable as usize);
|
||||
.map_fresh_range(pid, VirtualAddress::from_raw(page_start), pages, perms)
|
||||
.unwrap_or_else(|_| crate::halt(uart, "cannot map segment"));
|
||||
|
||||
let copy_len = segment.file_size.min(segment.mem_size);
|
||||
let data = segment
|
||||
.file_offset
|
||||
.checked_add(copy_len)
|
||||
.and_then(|end| file.get(segment.file_offset..end))
|
||||
.unwrap_or_else(|| crate::halt(uart, "segment outside file"));
|
||||
if !copy_to_user(pid, segment.vaddr, data) {
|
||||
crate::halt(uart, "segment copy failed");
|
||||
}
|
||||
}
|
||||
|
||||
let stack_bottom = USER_VA_BASE - USER_STACK_PAGES * PAGE_SIZE;
|
||||
kern()
|
||||
.map_fresh_range(
|
||||
pid,
|
||||
VirtualAddress::from_raw(stack_bottom),
|
||||
USER_STACK_PAGES,
|
||||
PagePerms::ReadWrite,
|
||||
)
|
||||
.unwrap_or_else(|_| crate::halt(uart, "cannot map stack"));
|
||||
|
||||
elf_info.entry_point
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use crate::hw::virtio_blk::VirtioBlk;
|
||||
|
||||
const SECTOR_SIZE: usize = 512;
|
||||
const MAX_FILE_SIZE: usize = 64 * 1024; // 64K max for init binary
|
||||
|
||||
struct BootFat16 {
|
||||
reserved_sectors: u16,
|
||||
sectors_per_fat: u16,
|
||||
root_dir_start: u32,
|
||||
root_dir_sectors: u32,
|
||||
data_start: u32,
|
||||
@@ -31,12 +29,11 @@ impl BootFat16 {
|
||||
|
||||
let fat_start = reserved_sectors as u32;
|
||||
let root_dir_start = fat_start + (num_fats as u32) * (sectors_per_fat as u32);
|
||||
let root_dir_sectors = ((root_entry_count as u32) * 32 + 511) / 512;
|
||||
let root_dir_sectors = ((root_entry_count as u32) * 32).div_ceil(512);
|
||||
let data_start = root_dir_start + root_dir_sectors;
|
||||
|
||||
Some(Self {
|
||||
reserved_sectors,
|
||||
sectors_per_fat,
|
||||
root_dir_start,
|
||||
root_dir_sectors,
|
||||
data_start,
|
||||
@@ -84,7 +81,7 @@ pub fn read_file_from_disk(
|
||||
let mut found = false;
|
||||
|
||||
for s in 0..fs.root_dir_sectors {
|
||||
if !blk.read_sector((fs.root_dir_start + s) as u32, &mut sector_buf) {
|
||||
if !blk.read_sector(fs.root_dir_start + s, &mut sector_buf) {
|
||||
continue;
|
||||
}
|
||||
for i in 0..(SECTOR_SIZE / 32) {
|
||||
@@ -121,7 +118,7 @@ pub fn read_file_from_disk(
|
||||
let mut cluster = first_cluster;
|
||||
let sectors_per_cluster = fs.sectors_per_cluster as u32;
|
||||
|
||||
while read < size && cluster >= 2 && cluster < 0xFFF8 {
|
||||
while read < size && (2..0xFFF8).contains(&cluster) {
|
||||
for s in 0..sectors_per_cluster {
|
||||
if read >= size {
|
||||
break;
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
use super::{dma_buffer, info_page, kern, map_mmio};
|
||||
use crate::hw::uart::Uart;
|
||||
use crate::hw::virtio_gpu::VirtioGpu;
|
||||
use crate::mmu;
|
||||
use types::display::Dimension;
|
||||
use types::id::ProcessId;
|
||||
|
||||
fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
|
||||
unsafe { &mut *crate::kernel_ptr() }
|
||||
}
|
||||
|
||||
const GPU_VA: usize = 0x20000;
|
||||
const VQUEUE_VA: usize = 0x21000;
|
||||
const CMDBUF_VA: usize = 0x23000;
|
||||
@@ -15,6 +11,8 @@ const PHYSINFO_VA: usize = 0x24000;
|
||||
const FB_VA: usize = 0x100000;
|
||||
const DEFAULT_WIDTH: Dimension = Dimension::new(640);
|
||||
const DEFAULT_HEIGHT: Dimension = Dimension::new(480);
|
||||
/// The framebuffer alone is ~1000 pages at 1280x800.
|
||||
const DISPLAY_PAGE_LIMIT: usize = 2048;
|
||||
|
||||
pub fn configure(uart: &Uart, display_pid: ProcessId) {
|
||||
let gpu = match VirtioGpu::probe() {
|
||||
@@ -25,16 +23,13 @@ pub fn configure(uart: &Uart, display_pid: ProcessId) {
|
||||
}
|
||||
};
|
||||
|
||||
let pt_addr = kern().platform_mut().page_table_for(display_pid);
|
||||
let pt = unsafe { &mut *(pt_addr as *mut mmu::PageTable) };
|
||||
kern()
|
||||
.set_page_limit(display_pid, DISPLAY_PAGE_LIMIT)
|
||||
.unwrap_or_else(|_| crate::halt(uart, "display quota"));
|
||||
|
||||
map_page(pt, GPU_VA, gpu.base());
|
||||
|
||||
let vq_phys = alloc_contiguous(2);
|
||||
map_range(pt, VQUEUE_VA, vq_phys, 2);
|
||||
|
||||
let cmd_phys = mmu::allocator::alloc_zeroed_page();
|
||||
map_page(pt, CMDBUF_VA, cmd_phys);
|
||||
map_mmio(uart, display_pid, GPU_VA, gpu.base(), 1);
|
||||
let vq_phys = dma_buffer(uart, display_pid, VQUEUE_VA, 2);
|
||||
let cmd_phys = dma_buffer(uart, display_pid, CMDBUF_VA, 1);
|
||||
|
||||
gpu.init_device(vq_phys);
|
||||
|
||||
@@ -45,7 +40,6 @@ pub fn configure(uart: &Uart, display_pid: ProcessId) {
|
||||
cmd_va: types::address::VirtualAddress::from_raw(cmd_phys),
|
||||
resp_pa: types::address::PhysicalAddress::from_raw(cmd_phys + 512),
|
||||
};
|
||||
use hw_virtio::mmio::MmioAccess;
|
||||
|
||||
let (width, height) = match hw_virtio::gpu::get_display_info(gpu.mmio(), &queue, &dma) {
|
||||
Some((w, h)) => {
|
||||
@@ -62,20 +56,21 @@ pub fn configure(uart: &Uart, display_pid: ProcessId) {
|
||||
}
|
||||
};
|
||||
|
||||
let fb_pages = (width.raw() as usize * height.raw() as usize * 4 + 4095) / 4096;
|
||||
let fb_phys = alloc_contiguous(fb_pages);
|
||||
map_range(pt, FB_VA, fb_phys, fb_pages);
|
||||
let fb_pages = (width.raw() as usize * height.raw() as usize * 4).div_ceil(4096);
|
||||
let fb_phys = dma_buffer(uart, display_pid, FB_VA, fb_pages);
|
||||
|
||||
let info_phys = mmu::allocator::alloc_zeroed_page();
|
||||
map_page(pt, PHYSINFO_VA, info_phys);
|
||||
unsafe {
|
||||
let info = info_phys as *mut u64;
|
||||
core::ptr::write_volatile(info, cmd_phys as u64);
|
||||
core::ptr::write_volatile(info.add(1), (cmd_phys + 512) as u64);
|
||||
core::ptr::write_volatile(info.add(2), fb_phys as u64);
|
||||
core::ptr::write_volatile(info.add(3), width.raw() as u64);
|
||||
core::ptr::write_volatile(info.add(4), height.raw() as u64);
|
||||
}
|
||||
info_page(
|
||||
uart,
|
||||
display_pid,
|
||||
PHYSINFO_VA,
|
||||
&[
|
||||
cmd_phys as u64,
|
||||
(cmd_phys + 512) as u64,
|
||||
fb_phys as u64,
|
||||
width.raw() as u64,
|
||||
height.raw() as u64,
|
||||
],
|
||||
);
|
||||
|
||||
if gpu.setup_framebuffer(cmd_phys, vq_phys, fb_phys, width, height) {
|
||||
uart.write_str("k-os: VirtIO GPU configured\n");
|
||||
@@ -83,29 +78,3 @@ pub fn configure(uart: &Uart, display_pid: ProcessId) {
|
||||
uart.write_str("k-os: VirtIO GPU setup failed (display will be headless)\n");
|
||||
}
|
||||
}
|
||||
|
||||
fn map_page(pt: &mut mmu::PageTable, va: usize, pa: usize) {
|
||||
mmu::map(
|
||||
pt,
|
||||
mmu::Mapping {
|
||||
va: types::address::VirtualAddress::from_raw(va),
|
||||
pa: types::address::PhysicalAddress::from_raw(pa),
|
||||
pages: 1,
|
||||
flags: mmu::Sv39Flags::user_rw(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn map_range(pt: &mut mmu::PageTable, va_base: usize, pa_base: usize, count: usize) {
|
||||
for i in 0..count {
|
||||
map_page(pt, va_base + i * 4096, pa_base + i * 4096);
|
||||
}
|
||||
}
|
||||
|
||||
fn alloc_contiguous(pages: usize) -> usize {
|
||||
let first = mmu::allocator::alloc_zeroed_page();
|
||||
for _ in 1..pages {
|
||||
mmu::allocator::alloc_zeroed_page();
|
||||
}
|
||||
first
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::{dma_buffer, info_page, map_mmio};
|
||||
use crate::board;
|
||||
use crate::hw::uart::Uart;
|
||||
use crate::{board, mmu};
|
||||
use types::id::ProcessId;
|
||||
|
||||
const UART_VA: usize = 0x20000;
|
||||
@@ -8,34 +9,22 @@ const INPUT_VQUEUE_VA: usize = 0x26000;
|
||||
const INPUT_EVBUF_VA: usize = 0x28000;
|
||||
const INPUT_PHYSINFO_VA: usize = 0x29000;
|
||||
|
||||
fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
|
||||
unsafe { &mut *crate::kernel_ptr() }
|
||||
}
|
||||
|
||||
pub fn configure(uart: &Uart, input_pid: ProcessId) {
|
||||
let pt_addr = kern().platform_mut().page_table_for(input_pid);
|
||||
let pt = unsafe { &mut *(pt_addr as *mut mmu::PageTable) };
|
||||
|
||||
// Always map UART for fallback
|
||||
map_page(pt, UART_VA, board::UART_BASE);
|
||||
map_mmio(uart, input_pid, UART_VA, board::UART_BASE, 1);
|
||||
|
||||
// Try to find VirtIO input device
|
||||
if let Some(mmio) = hw_virtio::mmio::scan_for_device(hw_virtio::mmio::DEVICE_ID_INPUT) {
|
||||
let base = mmio.base();
|
||||
map_page(pt, INPUT_MMIO_VA, base);
|
||||
|
||||
let vq_phys = alloc_contiguous(2);
|
||||
map_range(pt, INPUT_VQUEUE_VA, vq_phys, 2);
|
||||
|
||||
let evbuf_phys = mmu::allocator::alloc_zeroed_page();
|
||||
map_page(pt, INPUT_EVBUF_VA, evbuf_phys);
|
||||
|
||||
let info_phys = mmu::allocator::alloc_zeroed_page();
|
||||
map_page(pt, INPUT_PHYSINFO_VA, info_phys);
|
||||
unsafe {
|
||||
core::ptr::write_volatile(info_phys as *mut u64, evbuf_phys as u64);
|
||||
core::ptr::write_volatile((info_phys as *mut u64).add(1), vq_phys as u64);
|
||||
}
|
||||
map_mmio(uart, input_pid, INPUT_MMIO_VA, base, 1);
|
||||
let vq_phys = dma_buffer(uart, input_pid, INPUT_VQUEUE_VA, 2);
|
||||
let evbuf_phys = dma_buffer(uart, input_pid, INPUT_EVBUF_VA, 1);
|
||||
info_page(
|
||||
uart,
|
||||
input_pid,
|
||||
INPUT_PHYSINFO_VA,
|
||||
&[evbuf_phys as u64, vq_phys as u64],
|
||||
);
|
||||
|
||||
hw_virtio::mmio::init_legacy_device(&mmio, vq_phys, 32);
|
||||
uart.write_str("k-os: VirtIO keyboard configured\n");
|
||||
@@ -43,29 +32,3 @@ pub fn configure(uart: &Uart, input_pid: ProcessId) {
|
||||
uart.write_str("k-os: UART mapped for input\n");
|
||||
}
|
||||
}
|
||||
|
||||
fn map_page(pt: &mut mmu::PageTable, va: usize, pa: usize) {
|
||||
mmu::map(
|
||||
pt,
|
||||
mmu::Mapping {
|
||||
va: types::address::VirtualAddress::from_raw(va),
|
||||
pa: types::address::PhysicalAddress::from_raw(pa),
|
||||
pages: 1,
|
||||
flags: mmu::Sv39Flags::user_rw(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn map_range(pt: &mut mmu::PageTable, va_base: usize, pa_base: usize, count: usize) {
|
||||
for i in 0..count {
|
||||
map_page(pt, va_base + i * 4096, pa_base + i * 4096);
|
||||
}
|
||||
}
|
||||
|
||||
fn alloc_contiguous(pages: usize) -> usize {
|
||||
let first = mmu::allocator::alloc_zeroed_page();
|
||||
for _ in 1..pages {
|
||||
mmu::allocator::alloc_zeroed_page();
|
||||
}
|
||||
first
|
||||
}
|
||||
|
||||
@@ -1,3 +1,84 @@
|
||||
pub mod gpu_setup;
|
||||
pub mod input_setup;
|
||||
pub mod storage_setup;
|
||||
|
||||
use crate::hw::uart::Uart;
|
||||
use types::address::{PhysicalAddress, VirtualAddress};
|
||||
use types::bounded::PageCount;
|
||||
use types::id::ProcessId;
|
||||
use types::mapping::PagePerms;
|
||||
|
||||
// Boot-time device wiring. Everything here goes through the kernel's
|
||||
// mapping API so the kernel knows about every page a driver can touch.
|
||||
// P5 replaces these hardcoded grants with capabilities handed to init.
|
||||
|
||||
fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
|
||||
unsafe { &mut *crate::kernel_ptr() }
|
||||
}
|
||||
|
||||
fn pages(uart: &Uart, n: usize) -> PageCount {
|
||||
u16::try_from(n)
|
||||
.ok()
|
||||
.and_then(PageCount::new)
|
||||
.unwrap_or_else(|| crate::halt(uart, "bad page count"))
|
||||
}
|
||||
|
||||
/// Maps device MMIO at `pa` into `pid` at `va`.
|
||||
pub(crate) fn map_mmio(uart: &Uart, pid: ProcessId, va: usize, pa: usize, n: usize) {
|
||||
let id = kern()
|
||||
.create_device_region(pid, PhysicalAddress::from_raw(pa), pages(uart, n))
|
||||
.unwrap_or_else(|_| crate::halt(uart, "device region failed"));
|
||||
kern()
|
||||
.map_region(id, pid, VirtualAddress::from_raw(va), PagePerms::ReadWrite)
|
||||
.unwrap_or_else(|_| crate::halt(uart, "device map failed"));
|
||||
}
|
||||
|
||||
/// Physically contiguous, zeroed DMA memory for `owner`, mapped read-write
|
||||
/// at `va`. Returns its physical base for programming the device.
|
||||
pub(crate) fn dma_buffer(uart: &Uart, owner: ProcessId, va: usize, n: usize) -> usize {
|
||||
let id = kern()
|
||||
.create_pool_region(owner, pages(uart, n))
|
||||
.unwrap_or_else(|_| crate::halt(uart, "DMA region failed"));
|
||||
kern()
|
||||
.map_region(
|
||||
id,
|
||||
owner,
|
||||
VirtualAddress::from_raw(va),
|
||||
PagePerms::ReadWrite,
|
||||
)
|
||||
.unwrap_or_else(|_| crate::halt(uart, "DMA map failed"));
|
||||
kern()
|
||||
.region_base(id)
|
||||
.unwrap_or_else(|| crate::halt(uart, "DMA region vanished"))
|
||||
.raw()
|
||||
}
|
||||
|
||||
/// Shares a fresh region between `owner` and `other` at the same `va`.
|
||||
pub(crate) fn shared_buffer(uart: &Uart, owner: ProcessId, other: ProcessId, va: usize, n: usize) {
|
||||
let id = kern()
|
||||
.create_pool_region(owner, pages(uart, n))
|
||||
.unwrap_or_else(|_| crate::halt(uart, "shared region failed"));
|
||||
for pid in [owner, other] {
|
||||
kern()
|
||||
.map_region(id, pid, VirtualAddress::from_raw(va), PagePerms::ReadWrite)
|
||||
.unwrap_or_else(|_| crate::halt(uart, "shared map failed"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes `words` into a fresh page mapped at `va` in `pid`, so a driver
|
||||
/// can learn the physical addresses of its DMA buffers.
|
||||
pub(crate) fn info_page(uart: &Uart, pid: ProcessId, va: usize, words: &[u64]) {
|
||||
let va = VirtualAddress::from_raw(va);
|
||||
kern()
|
||||
.map_fresh_range(pid, va, 1, PagePerms::ReadOnly)
|
||||
.unwrap_or_else(|_| crate::halt(uart, "info page failed"));
|
||||
let pa = kern()
|
||||
.user_page_frame(pid, va)
|
||||
.unwrap_or_else(|| crate::halt(uart, "info page vanished"));
|
||||
let info = pa.raw() as *mut u64;
|
||||
for (i, &w) in words.iter().enumerate() {
|
||||
// SAFETY: `pa` is a pool frame just mapped for `pid`, identity
|
||||
// mapped in kernel space, and `words` fits in one page.
|
||||
unsafe { core::ptr::write_volatile(info.add(i), w) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
use super::{dma_buffer, info_page, map_mmio, shared_buffer};
|
||||
use crate::hw::uart::Uart;
|
||||
use crate::mmu;
|
||||
use types::id::ProcessId;
|
||||
|
||||
fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
|
||||
unsafe { &mut *crate::kernel_ptr() }
|
||||
}
|
||||
|
||||
const VIRTIO_VA: usize = 0x20000;
|
||||
const VQUEUE_VA: usize = 0x21000;
|
||||
const REQHDR_VA: usize = 0x23000;
|
||||
@@ -14,111 +10,27 @@ const SHARED_BUF_VA: usize = 0x30000;
|
||||
const SHARED_BUF_PAGES: usize = 16;
|
||||
|
||||
pub fn configure(uart: &Uart, init_pid: ProcessId, storage_pid: ProcessId, virtio_base: usize) {
|
||||
let storage_pt = get_pt(storage_pid);
|
||||
let init_pt = get_pt(init_pid);
|
||||
|
||||
map_virtio_mmio(storage_pt, virtio_base);
|
||||
let vqueue_phys = alloc_and_map_virtqueue(storage_pt);
|
||||
let reqhdr_phys = alloc_and_map_reqhdr(storage_pt);
|
||||
write_phys_info(storage_pt, reqhdr_phys);
|
||||
map_mmio(uart, storage_pid, VIRTIO_VA, virtio_base, 1);
|
||||
let vqueue_phys = dma_buffer(uart, storage_pid, VQUEUE_VA, 2);
|
||||
let reqhdr_phys = dma_buffer(uart, storage_pid, REQHDR_VA, 1);
|
||||
info_page(
|
||||
uart,
|
||||
storage_pid,
|
||||
PHYSINFO_VA,
|
||||
&[
|
||||
reqhdr_phys as u64,
|
||||
(reqhdr_phys + 16) as u64,
|
||||
(reqhdr_phys + 32) as u64,
|
||||
],
|
||||
);
|
||||
reinit_device(virtio_base, vqueue_phys);
|
||||
uart.write_str("k-os: VirtIO configured for storage\n");
|
||||
|
||||
map_shared_buffer(init_pt, storage_pt);
|
||||
shared_buffer(uart, init_pid, storage_pid, SHARED_BUF_VA, SHARED_BUF_PAGES);
|
||||
uart.write_str("k-os: shared buffer mapped\n");
|
||||
}
|
||||
|
||||
fn get_pt(pid: ProcessId) -> &'static mut mmu::PageTable {
|
||||
let addr = kern().platform_mut().page_table_for(pid);
|
||||
unsafe { &mut *(addr as *mut mmu::PageTable) }
|
||||
}
|
||||
|
||||
fn map_virtio_mmio(pt: &mut mmu::PageTable, virtio_base: usize) {
|
||||
mmu::map(
|
||||
pt,
|
||||
mmu::Mapping {
|
||||
va: types::address::VirtualAddress::from_raw(VIRTIO_VA),
|
||||
pa: types::address::PhysicalAddress::from_raw(virtio_base),
|
||||
pages: 1,
|
||||
flags: mmu::Sv39Flags::user_rw(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn alloc_and_map_virtqueue(pt: &mut mmu::PageTable) -> usize {
|
||||
let phys = mmu::allocator::alloc_zeroed_page();
|
||||
mmu::allocator::alloc_zeroed_page();
|
||||
for i in 0..2usize {
|
||||
mmu::map(
|
||||
pt,
|
||||
mmu::Mapping {
|
||||
va: types::address::VirtualAddress::from_raw(VQUEUE_VA + i * 4096),
|
||||
pa: types::address::PhysicalAddress::from_raw(phys + i * 4096),
|
||||
pages: 1,
|
||||
flags: mmu::Sv39Flags::user_rw(),
|
||||
},
|
||||
);
|
||||
}
|
||||
phys
|
||||
}
|
||||
|
||||
fn alloc_and_map_reqhdr(pt: &mut mmu::PageTable) -> usize {
|
||||
let phys = mmu::allocator::alloc_zeroed_page();
|
||||
mmu::map(
|
||||
pt,
|
||||
mmu::Mapping {
|
||||
va: types::address::VirtualAddress::from_raw(REQHDR_VA),
|
||||
pa: types::address::PhysicalAddress::from_raw(phys),
|
||||
pages: 1,
|
||||
flags: mmu::Sv39Flags::user_rw(),
|
||||
},
|
||||
);
|
||||
phys
|
||||
}
|
||||
|
||||
fn write_phys_info(pt: &mut mmu::PageTable, reqhdr_phys: usize) {
|
||||
let phys = mmu::allocator::alloc_zeroed_page();
|
||||
mmu::map(
|
||||
pt,
|
||||
mmu::Mapping {
|
||||
va: types::address::VirtualAddress::from_raw(PHYSINFO_VA),
|
||||
pa: types::address::PhysicalAddress::from_raw(phys),
|
||||
pages: 1,
|
||||
flags: mmu::Sv39Flags::user_rw(),
|
||||
},
|
||||
);
|
||||
unsafe {
|
||||
let info = phys as *mut u64;
|
||||
core::ptr::write_volatile(info, reqhdr_phys as u64);
|
||||
core::ptr::write_volatile(info.add(1), (reqhdr_phys + 16) as u64);
|
||||
core::ptr::write_volatile(info.add(2), (reqhdr_phys + 32) as u64);
|
||||
}
|
||||
}
|
||||
|
||||
fn reinit_device(base: usize, vqueue_phys: usize) {
|
||||
let mmio = hw_virtio::mmio::DirectMmio::new(base);
|
||||
hw_virtio::mmio::init_legacy_device(&mmio, vqueue_phys, hw_virtio::queue::QUEUE_SIZE as u32);
|
||||
}
|
||||
|
||||
fn map_shared_buffer(init_pt: &mut mmu::PageTable, storage_pt: &mut mmu::PageTable) {
|
||||
for i in 0..SHARED_BUF_PAGES {
|
||||
let page = mmu::allocator::alloc_zeroed_page();
|
||||
let va = SHARED_BUF_VA + i * 4096;
|
||||
let mapping = mmu::Mapping {
|
||||
va: types::address::VirtualAddress::from_raw(va),
|
||||
pa: types::address::PhysicalAddress::from_raw(page),
|
||||
pages: 1,
|
||||
flags: mmu::Sv39Flags::user_rw(),
|
||||
};
|
||||
mmu::map(init_pt, mapping);
|
||||
mmu::map(
|
||||
storage_pt,
|
||||
mmu::Mapping {
|
||||
va: types::address::VirtualAddress::from_raw(va),
|
||||
pa: types::address::PhysicalAddress::from_raw(page),
|
||||
pages: 1,
|
||||
flags: mmu::Sv39Flags::user_rw(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,20 +29,18 @@ impl VirtioBlk {
|
||||
pub fn probe() -> Option<Self> {
|
||||
let mmio = mmio::scan_for_device(mmio::DEVICE_ID_BLOCK)?;
|
||||
|
||||
let vq_phys = unsafe { core::ptr::addr_of!(VQUEUE) as usize };
|
||||
let vq_phys = core::ptr::addr_of!(VQUEUE) as usize;
|
||||
mmio::init_legacy_device(&mmio, vq_phys, QUEUE_SIZE as u32);
|
||||
|
||||
let queue = VirtioQueue::new(QueueAddrs::from_base(vq_phys));
|
||||
|
||||
let dma = unsafe {
|
||||
DmaAddrs {
|
||||
let dma = DmaAddrs {
|
||||
header_pa: PhysicalAddress::from_raw(core::ptr::addr_of!(REQ_HEADER) as usize),
|
||||
header_va: VirtualAddress::from_raw(core::ptr::addr_of_mut!(REQ_HEADER) as usize),
|
||||
status_pa: PhysicalAddress::from_raw(core::ptr::addr_of!(REQ_STATUS) as usize),
|
||||
status_va: VirtualAddress::from_raw(core::ptr::addr_of_mut!(REQ_STATUS) as usize),
|
||||
data_pa: PhysicalAddress::from_raw(core::ptr::addr_of!(SECTOR_BUF) as usize),
|
||||
data_va: VirtualAddress::from_raw(core::ptr::addr_of_mut!(SECTOR_BUF) as usize),
|
||||
}
|
||||
};
|
||||
|
||||
Some(Self { mmio, queue, dma })
|
||||
|
||||
@@ -18,8 +18,13 @@ impl<T> KernelCell<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(&self, value: T) {
|
||||
unsafe { (*self.inner.get()).write(value) };
|
||||
/// Initializes the cell by letting `build` write the value straight
|
||||
/// into its storage, so a large `T` never lives on the stack.
|
||||
///
|
||||
/// # Safety
|
||||
/// `build` must fully initialize the `T` behind the pointer.
|
||||
pub unsafe fn init_with(&self, build: impl FnOnce(*mut T)) {
|
||||
build(unsafe { (*self.inner.get()).as_mut_ptr() });
|
||||
self.initialized.store(true, Ordering::Release);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,16 +41,15 @@ extern "C" fn kmain() -> ! {
|
||||
let uart = Uart::new();
|
||||
uart.write_str("k-os: booted on RISC-V\n");
|
||||
|
||||
let kernel_pt = boot::hardware::init(&uart);
|
||||
boot::hardware::init(&uart);
|
||||
let mut blk =
|
||||
boot::hardware::probe_block_device(&uart).unwrap_or_else(|| halt(&uart, "no block device"));
|
||||
|
||||
let (init, _) = boot::loader::load_init(&uart, &mut blk, kernel_pt);
|
||||
let storage = boot::loader::load_server(&uart, &mut blk, kernel_pt, b"storage.elf", "storage");
|
||||
let display = boot::loader::load_server(&uart, &mut blk, kernel_pt, b"display.elf", "display");
|
||||
let input = boot::loader::load_server(&uart, &mut blk, kernel_pt, b"input.elf", "input");
|
||||
let (init, _) = boot::loader::load_init(&uart, &mut blk);
|
||||
let storage = boot::loader::load_server(&uart, &mut blk, b"storage.elf", "storage");
|
||||
let display = boot::loader::load_server(&uart, &mut blk, b"display.elf", "display");
|
||||
let input = boot::loader::load_server(&uart, &mut blk, b"input.elf", "input");
|
||||
let virtio_base = blk.base();
|
||||
drop(blk);
|
||||
|
||||
devices::storage_setup::configure(&uart, init.pid, storage.pid, virtio_base);
|
||||
devices::gpu_setup::configure(&uart, display.pid);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use super::page_table::{PAGE_SIZE, PageTable};
|
||||
use crate::board;
|
||||
|
||||
// Boot-time bump allocator for page tables.
|
||||
// Only used during init before the scheduler starts.
|
||||
// Boot-time bump allocator for kernel-only memory: the kernel page table
|
||||
// and boot scratch buffers. User memory and user page tables come from the
|
||||
// kernel's frame table instead. It must never reach the user pool.
|
||||
static mut NEXT_FREE: usize = 0;
|
||||
|
||||
pub fn init_allocator(start: usize) {
|
||||
@@ -13,6 +15,9 @@ pub fn init_allocator(start: usize) {
|
||||
pub fn alloc_zeroed_page() -> usize {
|
||||
unsafe {
|
||||
let addr = NEXT_FREE;
|
||||
if addr + PAGE_SIZE > board::USER_POOL_BASE {
|
||||
crate::halt(&crate::hw::uart::Uart::new(), "boot allocator exhausted");
|
||||
}
|
||||
NEXT_FREE += PAGE_SIZE;
|
||||
core::ptr::write_bytes(addr as *mut u8, 0, PAGE_SIZE);
|
||||
addr
|
||||
@@ -20,9 +25,5 @@ pub fn alloc_zeroed_page() -> usize {
|
||||
}
|
||||
|
||||
pub fn alloc_page_table() -> &'static mut PageTable {
|
||||
unsafe {
|
||||
let addr = alloc_zeroed_page();
|
||||
let table = &mut *(addr as *mut PageTable);
|
||||
table
|
||||
}
|
||||
unsafe { &mut *(alloc_zeroed_page() as *mut PageTable) }
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@ pub mod page_table;
|
||||
|
||||
pub use allocator::init_allocator;
|
||||
pub use page_table::{
|
||||
Mapping, PAGE_SIZE, PageTable, Sv39Flags, activate, create_user_page_table,
|
||||
init_kernel_page_table, map, translate,
|
||||
PAGE_SIZE, PageTable, Sv39Flags, USER_VA_END, USER_VA_START, activate, active_root,
|
||||
create_user_root, init_kernel_page_table, map_user_page, unmap_user_page,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use super::allocator::alloc_page_table;
|
||||
use crate::board;
|
||||
use platform::{FrameSource, MapError};
|
||||
use types::address::{PhysicalAddress, VirtualAddress};
|
||||
use types::mapping::PagePerms;
|
||||
|
||||
pub use types::mapping::Mapping;
|
||||
pub const PAGE_SIZE: usize = 4096;
|
||||
@@ -32,29 +34,12 @@ impl Sv39Flags {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user_rx() -> Self {
|
||||
Self {
|
||||
bits: PTE_R | PTE_X | PTE_U,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user_rw() -> Self {
|
||||
Self {
|
||||
bits: PTE_R | PTE_W | PTE_U,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user(read: bool, write: bool, exec: bool) -> Self {
|
||||
let mut bits = PTE_U;
|
||||
if read {
|
||||
bits |= PTE_R;
|
||||
}
|
||||
if write {
|
||||
bits |= PTE_W;
|
||||
}
|
||||
if exec {
|
||||
bits |= PTE_X;
|
||||
}
|
||||
pub fn user(perms: PagePerms) -> Self {
|
||||
let bits = match perms {
|
||||
PagePerms::ReadOnly => PTE_R | PTE_U,
|
||||
PagePerms::ReadWrite => PTE_R | PTE_W | PTE_U,
|
||||
PagePerms::ReadExecute => PTE_R | PTE_X | PTE_U,
|
||||
};
|
||||
Self { bits }
|
||||
}
|
||||
|
||||
@@ -106,27 +91,6 @@ fn vpn(va: usize, level: usize) -> usize {
|
||||
(va >> (12 + 9 * level)) & 0x1FF
|
||||
}
|
||||
|
||||
pub fn translate(root: &PageTable, va: usize) -> Option<usize> {
|
||||
let vpn2 = vpn(va, 2);
|
||||
if !root.entries[vpn2].is_valid() {
|
||||
return None;
|
||||
}
|
||||
let l1 = unsafe { &*((root.entries[vpn2].ppn() << 12) as *const PageTable) };
|
||||
|
||||
let vpn1 = vpn(va, 1);
|
||||
if !l1.entries[vpn1].is_valid() {
|
||||
return None;
|
||||
}
|
||||
let l0 = unsafe { &*((l1.entries[vpn1].ppn() << 12) as *const PageTable) };
|
||||
|
||||
let vpn0 = vpn(va, 0);
|
||||
if !l0.entries[vpn0].is_valid() {
|
||||
return None;
|
||||
}
|
||||
let pa = (l0.entries[vpn0].ppn() << 12) as usize | (va & 0xFFF);
|
||||
Some(pa)
|
||||
}
|
||||
|
||||
fn map_page(root: &mut PageTable, va: usize, pa: usize, flags: Sv39Flags) {
|
||||
let vpn2 = vpn(va, 2);
|
||||
let vpn1 = vpn(va, 1);
|
||||
@@ -246,34 +210,113 @@ pub fn activate(root: &PageTable) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_user_page_table(
|
||||
kernel_root: &PageTable,
|
||||
_program_pa: PhysicalAddress,
|
||||
_program_pages: usize,
|
||||
_user_va: VirtualAddress,
|
||||
) -> &'static mut PageTable {
|
||||
let root = alloc_page_table();
|
||||
// ---- user address spaces ----
|
||||
//
|
||||
// User mappings live in [USER_VA_START, USER_VA_END), which is root slot 0,
|
||||
// L1 slots 0..USER_L1_SLOTS. Every address space gets its own root and its
|
||||
// own L1 for slot 0, and the kernel maps nothing in those L1 slots, so all
|
||||
// page tables below the user window are private to one process. Kernel
|
||||
// mappings elsewhere are shared by copying the kernel's entries.
|
||||
|
||||
for i in 0..512 {
|
||||
if !kernel_root.entries[i].is_valid() {
|
||||
continue;
|
||||
}
|
||||
if i == 0 {
|
||||
// User mappings land in VPN[2]=0 — each process needs its own L1
|
||||
let kernel_l1_addr = (kernel_root.entries[i].ppn() << 12) as *const PageTable;
|
||||
let kernel_l1 = unsafe { &*kernel_l1_addr };
|
||||
let new_l1 = alloc_page_table();
|
||||
for j in 0..512 {
|
||||
if kernel_l1.entries[j].is_valid() {
|
||||
new_l1.entries[j] = kernel_l1.entries[j];
|
||||
}
|
||||
}
|
||||
let ppn = (new_l1 as *const PageTable as u64) >> 12;
|
||||
root.entries[i] = PageTableEntry::new_branch(ppn);
|
||||
} else {
|
||||
root.entries[i] = kernel_root.entries[i];
|
||||
}
|
||||
}
|
||||
pub const USER_VA_START: usize = 0x1000;
|
||||
pub const USER_VA_END: usize = 0x0200_0000;
|
||||
const USER_L1_SLOTS: usize = USER_VA_END >> 21;
|
||||
|
||||
root
|
||||
fn table_at(pa: usize) -> &'static mut PageTable {
|
||||
// SAFETY: page tables are identity mapped in kernel space.
|
||||
unsafe { &mut *(pa as *mut PageTable) }
|
||||
}
|
||||
|
||||
fn fresh_table(frames: &mut dyn FrameSource) -> Result<usize, MapError> {
|
||||
let pa = frames
|
||||
.alloc_table_frame()
|
||||
.ok_or(MapError::OutOfFrames)?
|
||||
.raw();
|
||||
table_at(pa).zero();
|
||||
Ok(pa)
|
||||
}
|
||||
|
||||
fn in_user_window(va: usize) -> bool {
|
||||
va.is_multiple_of(PAGE_SIZE) && (USER_VA_START..USER_VA_END).contains(&va)
|
||||
}
|
||||
|
||||
/// Builds a root table sharing the kernel's mappings, with a private L1 for
|
||||
/// the user window. Returns the root's physical address.
|
||||
pub fn create_user_root(
|
||||
kernel_root: &PageTable,
|
||||
frames: &mut dyn FrameSource,
|
||||
) -> Result<usize, MapError> {
|
||||
let root_pa = fresh_table(frames)?;
|
||||
let l1_pa = fresh_table(frames)?;
|
||||
let root = table_at(root_pa);
|
||||
let l1 = table_at(l1_pa);
|
||||
|
||||
for (i, entry) in kernel_root.entries.iter().enumerate() {
|
||||
if i != 0 && entry.is_valid() {
|
||||
root.entries[i] = *entry;
|
||||
}
|
||||
}
|
||||
if kernel_root.entries[0].is_valid() {
|
||||
let kernel_l1 = table_at((kernel_root.entries[0].ppn() << 12) as usize);
|
||||
for j in USER_L1_SLOTS..512 {
|
||||
l1.entries[j] = kernel_l1.entries[j];
|
||||
}
|
||||
}
|
||||
root.entries[0] = PageTableEntry::new_branch((l1_pa as u64) >> 12);
|
||||
Ok(root_pa)
|
||||
}
|
||||
|
||||
fn user_leaf_table(
|
||||
root_pa: usize,
|
||||
va: usize,
|
||||
frames: Option<&mut dyn FrameSource>,
|
||||
) -> Result<&'static mut PageTable, MapError> {
|
||||
let root = table_at(root_pa);
|
||||
let l1 = table_at((root.entries[0].ppn() << 12) as usize);
|
||||
let slot = &mut l1.entries[vpn(va, 1)];
|
||||
if !slot.is_valid() {
|
||||
let frames = frames.ok_or(MapError::NoAddressSpace)?;
|
||||
let l0_pa = fresh_table(frames)?;
|
||||
*slot = PageTableEntry::new_branch((l0_pa as u64) >> 12);
|
||||
}
|
||||
Ok(table_at((slot.ppn() << 12) as usize))
|
||||
}
|
||||
|
||||
pub fn map_user_page(
|
||||
root_pa: usize,
|
||||
va: usize,
|
||||
pa: usize,
|
||||
flags: Sv39Flags,
|
||||
frames: &mut dyn FrameSource,
|
||||
) -> Result<(), MapError> {
|
||||
if !in_user_window(va) || !pa.is_multiple_of(PAGE_SIZE) {
|
||||
return Err(MapError::InvalidAddress);
|
||||
}
|
||||
let l0 = user_leaf_table(root_pa, va, Some(frames))?;
|
||||
l0.entries[vpn(va, 0)] = PageTableEntry::new_leaf((pa as u64) >> 12, flags);
|
||||
flush_tlb(va);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unmap_user_page(root_pa: usize, va: usize) {
|
||||
if !in_user_window(va) {
|
||||
return;
|
||||
}
|
||||
if let Ok(l0) = user_leaf_table(root_pa, va, None) {
|
||||
l0.entries[vpn(va, 0)] = PageTableEntry::empty();
|
||||
flush_tlb(va);
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_tlb(va: usize) {
|
||||
// SAFETY: sfence.vma only invalidates translation caches.
|
||||
unsafe { core::arch::asm!("sfence.vma {0}, zero", in(reg) va) };
|
||||
}
|
||||
|
||||
/// Physical address of the root table the hart is translating with.
|
||||
pub fn active_root() -> usize {
|
||||
let satp: usize;
|
||||
// SAFETY: reading satp has no side effects.
|
||||
unsafe { core::arch::asm!("csrr {0}, satp", out(reg) satp) };
|
||||
(satp & ((1 << 44) - 1)) << 12
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use domain::memory::MemoryRegion;
|
||||
use domain::process::MAX_PROCESSES;
|
||||
use platform::Platform;
|
||||
use platform::{FrameSource, MapError, Platform};
|
||||
use types::{
|
||||
address::{EntryPoint, PhysicalAddress, StackPointer, VirtualAddress},
|
||||
bounded::PageCount,
|
||||
id::{CoreId, IrqId, ProcessId},
|
||||
permission::{Permission, PermissionSet},
|
||||
mapping::PagePerms,
|
||||
time::{Ticks, TimerDuration},
|
||||
};
|
||||
|
||||
@@ -17,6 +15,7 @@ use crate::trap;
|
||||
pub struct RiscVPlatform {
|
||||
num_cores: u8,
|
||||
kernel_pt: usize,
|
||||
/// Root table of each process's address space, 0 if it has none.
|
||||
page_tables: [usize; MAX_PROCESSES],
|
||||
}
|
||||
|
||||
@@ -33,40 +32,76 @@ impl RiscVPlatform {
|
||||
self.page_tables[pid.raw() as usize]
|
||||
}
|
||||
|
||||
pub fn set_page_table(&mut self, pid: ProcessId, addr: usize) {
|
||||
self.page_tables[pid.raw() as usize] = addr;
|
||||
fn root(&self, pid: ProcessId) -> Option<usize> {
|
||||
match self.page_tables.get(pid.raw() as usize) {
|
||||
Some(&0) | None => None,
|
||||
Some(&root) => Some(root),
|
||||
}
|
||||
|
||||
fn ensure_page_table(&mut self, pid: ProcessId) -> &mut mmu::PageTable {
|
||||
let idx = pid.raw() as usize;
|
||||
if self.page_tables[idx] == 0 {
|
||||
let kernel_pt = unsafe { &*(self.kernel_pt as *const mmu::PageTable) };
|
||||
let user_pt = mmu::create_user_page_table(
|
||||
kernel_pt,
|
||||
PhysicalAddress::from_raw(0),
|
||||
0,
|
||||
VirtualAddress::from_raw(0x10000),
|
||||
);
|
||||
self.page_tables[idx] = user_pt as *const mmu::PageTable as usize;
|
||||
}
|
||||
unsafe { &mut *(self.page_tables[idx] as *mut mmu::PageTable) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Platform for RiscVPlatform {
|
||||
fn configure_protection(&mut self, process: ProcessId, regions: &[MemoryRegion]) {
|
||||
let pt = self.ensure_page_table(process);
|
||||
for region in regions {
|
||||
mmu::map(
|
||||
pt,
|
||||
mmu::Mapping {
|
||||
va: VirtualAddress::from_raw(region.base().raw()),
|
||||
pa: region.base(),
|
||||
pages: region.size().value() as usize,
|
||||
flags: mmu::Sv39Flags::user(true, true, true),
|
||||
},
|
||||
);
|
||||
const USER_VA_START: usize = mmu::USER_VA_START;
|
||||
const USER_VA_END: usize = mmu::USER_VA_END;
|
||||
|
||||
fn create_address_space(
|
||||
&mut self,
|
||||
process: ProcessId,
|
||||
frames: &mut dyn FrameSource,
|
||||
) -> Result<(), MapError> {
|
||||
let slot = self
|
||||
.page_tables
|
||||
.get_mut(process.raw() as usize)
|
||||
.ok_or(MapError::NoAddressSpace)?;
|
||||
// SAFETY: the kernel page table is identity mapped and lives forever.
|
||||
let kernel_root = unsafe { &*(self.kernel_pt as *const mmu::PageTable) };
|
||||
*slot = mmu::create_user_root(kernel_root, frames)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn destroy_address_space(&mut self, process: ProcessId) {
|
||||
let Some(root) = self.root(process) else {
|
||||
return;
|
||||
};
|
||||
// The caller may be tearing down the space we are running on; get
|
||||
// off it before the kernel frees its frames.
|
||||
if mmu::active_root() == root {
|
||||
// SAFETY: the kernel page table is identity mapped and lives forever.
|
||||
mmu::activate(unsafe { &*(self.kernel_pt as *const mmu::PageTable) });
|
||||
}
|
||||
self.page_tables[process.raw() as usize] = 0;
|
||||
// SAFETY: flushing translations has no other effect.
|
||||
unsafe { core::arch::asm!("sfence.vma zero, zero") };
|
||||
}
|
||||
|
||||
fn map_page(
|
||||
&mut self,
|
||||
process: ProcessId,
|
||||
va: VirtualAddress,
|
||||
pa: PhysicalAddress,
|
||||
perms: PagePerms,
|
||||
frames: &mut dyn FrameSource,
|
||||
) -> Result<(), MapError> {
|
||||
let root = self.root(process).ok_or(MapError::NoAddressSpace)?;
|
||||
mmu::map_user_page(
|
||||
root,
|
||||
va.raw(),
|
||||
pa.raw(),
|
||||
mmu::Sv39Flags::user(perms),
|
||||
frames,
|
||||
)
|
||||
}
|
||||
|
||||
fn unmap_page(&mut self, process: ProcessId, va: VirtualAddress) {
|
||||
if let Some(root) = self.root(process) {
|
||||
mmu::unmap_user_page(root, va.raw());
|
||||
}
|
||||
}
|
||||
|
||||
fn zero_frame(&mut self, pa: PhysicalAddress) {
|
||||
// SAFETY: pool frames are identity mapped kernel RAM and the kernel
|
||||
// only zeroes frames it has just allocated.
|
||||
unsafe { core::ptr::write_bytes(pa.raw() as *mut u8, 0, mmu::PAGE_SIZE) };
|
||||
}
|
||||
|
||||
fn enable_user_mode(&mut self, entry: EntryPoint, stack: StackPointer) -> ! {
|
||||
@@ -117,63 +152,10 @@ impl Platform for RiscVPlatform {
|
||||
self.num_cores
|
||||
}
|
||||
|
||||
fn map_shared(
|
||||
&mut self,
|
||||
process: ProcessId,
|
||||
base: PhysicalAddress,
|
||||
size: PageCount,
|
||||
permissions: PermissionSet,
|
||||
) {
|
||||
let pt = self.ensure_page_table(process);
|
||||
mmu::map(
|
||||
pt,
|
||||
mmu::Mapping {
|
||||
va: VirtualAddress::from_raw(base.raw()),
|
||||
pa: base,
|
||||
pages: size.value() as usize,
|
||||
flags: mmu::Sv39Flags::user(
|
||||
permissions.allows(Permission::Read),
|
||||
permissions.allows(Permission::Write),
|
||||
permissions.allows(Permission::Execute),
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn debug_putchar(&self, byte: u8) {
|
||||
crate::hw::uart::Uart::new().put_char(byte);
|
||||
}
|
||||
|
||||
fn translate_va(&self, process: ProcessId, va: VirtualAddress) -> Option<PhysicalAddress> {
|
||||
let pt_addr = self.page_tables[process.raw() as usize];
|
||||
if pt_addr == 0 {
|
||||
return None;
|
||||
}
|
||||
let pt = unsafe { &*(pt_addr as *const mmu::PageTable) };
|
||||
mmu::translate(pt, va.raw()).map(PhysicalAddress::from_raw)
|
||||
}
|
||||
|
||||
fn map_user_page(
|
||||
&mut self,
|
||||
process: ProcessId,
|
||||
va: VirtualAddress,
|
||||
pa: PhysicalAddress,
|
||||
writable: bool,
|
||||
executable: bool,
|
||||
) {
|
||||
let pt = self.ensure_page_table(process);
|
||||
let flags = mmu::Sv39Flags::user(true, writable, executable);
|
||||
mmu::map(
|
||||
pt,
|
||||
mmu::Mapping {
|
||||
va,
|
||||
pa,
|
||||
pages: 1,
|
||||
flags,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn halt(&self) -> ! {
|
||||
unsafe {
|
||||
core::arch::asm!(
|
||||
|
||||
@@ -7,16 +7,6 @@ pub struct TrapFrame {
|
||||
pub sstatus: usize,
|
||||
}
|
||||
|
||||
impl TrapFrame {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
regs: [0; 32],
|
||||
sepc: 0,
|
||||
sstatus: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
global_asm!(
|
||||
r#"
|
||||
.align 4
|
||||
@@ -134,7 +124,7 @@ pub fn init() {
|
||||
fn trap_vector();
|
||||
}
|
||||
unsafe {
|
||||
let addr = trap_vector as usize;
|
||||
let addr = trap_vector as *const () as usize;
|
||||
core::arch::asm!("csrw stvec, {0}", in(reg) addr);
|
||||
core::arch::asm!("csrw sscratch, zero");
|
||||
// SIE stays clear — interrupts are enabled only when entering userspace via sret
|
||||
@@ -357,15 +347,15 @@ fn switch_to_thread(
|
||||
|
||||
apply_pending_result(frame, next_tid);
|
||||
|
||||
if next_pid != current_pid {
|
||||
if let Some(pid) = next_pid {
|
||||
if next_pid != current_pid
|
||||
&& let Some(pid) = next_pid
|
||||
{
|
||||
let pt_addr = kern.platform_mut().page_table_for(pid);
|
||||
if pt_addr != 0 {
|
||||
let pt = unsafe { &*(pt_addr as *const crate::mmu::PageTable) };
|
||||
crate::mmu::activate(pt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn switch_to_next(frame: &mut TrapFrame, current_tid: types::id::ThreadId) {
|
||||
@@ -388,15 +378,15 @@ fn switch_to_next(frame: &mut TrapFrame, current_tid: types::id::ThreadId) {
|
||||
|
||||
apply_pending_result(frame, next_tid);
|
||||
|
||||
if next_pid != current_pid {
|
||||
if let Some(pid) = next_pid {
|
||||
if next_pid != current_pid
|
||||
&& let Some(pid) = next_pid
|
||||
{
|
||||
let pt_addr = kern.platform_mut().page_table_for(pid);
|
||||
if pt_addr != 0 {
|
||||
let pt = unsafe { &*(pt_addr as *const crate::mmu::PageTable) };
|
||||
crate::mmu::activate(pt);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loop {
|
||||
unsafe { core::arch::asm!("wfi") };
|
||||
|
||||
@@ -8,7 +8,10 @@ use types::{
|
||||
pub enum KernelError {
|
||||
InvalidSlot(CapSlot),
|
||||
RevokedCapability(CapSlot),
|
||||
PermissionDenied { slot: CapSlot, needed: Permission },
|
||||
PermissionDenied {
|
||||
slot: CapSlot,
|
||||
needed: Permission,
|
||||
},
|
||||
NoFreeSlot,
|
||||
InvalidProcess(ProcessId),
|
||||
WouldBlock,
|
||||
@@ -23,4 +26,9 @@ pub enum KernelError {
|
||||
EndpointFull,
|
||||
ProcessCreateDenied,
|
||||
IrqAlreadyOwned(IrqId),
|
||||
/// Address outside the user window, unaligned, or not owned by the caller.
|
||||
InvalidAddress,
|
||||
/// The target virtual range already holds a mapping.
|
||||
AddressInUse,
|
||||
InvalidRegion,
|
||||
}
|
||||
|
||||
217
crates/domain/src/memory/frame/mod.rs
Normal file
217
crates/domain/src/memory/frame/mod.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
//! Physical frame ownership. The kernel's single source of truth for what
|
||||
//! every frame of the user pool is used for and who is charged for it.
|
||||
|
||||
use types::{
|
||||
address::{PhysicalAddress, VirtualAddress},
|
||||
id::{ProcessId, RegionId},
|
||||
mapping::PagePerms,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests.rs"]
|
||||
mod tests;
|
||||
|
||||
#[cfg(kani)]
|
||||
#[path = "proofs.rs"]
|
||||
mod proofs;
|
||||
|
||||
pub const PAGE_SIZE: usize = 4096;
|
||||
|
||||
/// Frames the kernel can track. The board's user pool must fit.
|
||||
pub const MAX_FRAMES: usize = 4096;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FrameUse {
|
||||
/// Mapped exactly once, at `page` in `owner`'s address space.
|
||||
User {
|
||||
owner: ProcessId,
|
||||
page: u32,
|
||||
perms: PagePerms,
|
||||
},
|
||||
/// Holds part of `owner`'s page tables.
|
||||
PageTable { owner: ProcessId },
|
||||
/// Backs a shared memory region.
|
||||
Region { region: RegionId },
|
||||
}
|
||||
|
||||
impl FrameUse {
|
||||
/// A user mapping at `va`, which must be page aligned.
|
||||
pub fn user(owner: ProcessId, va: VirtualAddress, perms: PagePerms) -> Option<Self> {
|
||||
if !va.raw().is_multiple_of(PAGE_SIZE) {
|
||||
return None;
|
||||
}
|
||||
let page = u32::try_from(va.raw() / PAGE_SIZE).ok()?;
|
||||
Some(Self::User { owner, page, perms })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FrameTable<const N: usize = MAX_FRAMES> {
|
||||
base: PhysicalAddress,
|
||||
total: usize,
|
||||
frames: [Option<FrameUse>; N],
|
||||
free: usize,
|
||||
}
|
||||
|
||||
impl<const N: usize> FrameTable<N> {
|
||||
pub fn new(base: PhysicalAddress, total: usize) -> Self {
|
||||
let total = total.min(N);
|
||||
Self {
|
||||
base,
|
||||
total,
|
||||
frames: [None; N],
|
||||
free: total,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn base(&self) -> PhysicalAddress {
|
||||
self.base
|
||||
}
|
||||
|
||||
pub fn capacity(&self) -> usize {
|
||||
self.total
|
||||
}
|
||||
|
||||
pub fn free_count(&self) -> usize {
|
||||
self.free
|
||||
}
|
||||
|
||||
fn index_of(&self, pa: PhysicalAddress) -> Option<usize> {
|
||||
let offset = pa.raw().checked_sub(self.base.raw())?;
|
||||
if !offset.is_multiple_of(PAGE_SIZE) {
|
||||
return None;
|
||||
}
|
||||
let idx = offset / PAGE_SIZE;
|
||||
(idx < self.total).then_some(idx)
|
||||
}
|
||||
|
||||
fn address_of(&self, idx: usize) -> PhysicalAddress {
|
||||
self.base.offset(idx)
|
||||
}
|
||||
|
||||
pub fn get(&self, pa: PhysicalAddress) -> Option<&FrameUse> {
|
||||
self.frames[self.index_of(pa)?].as_ref()
|
||||
}
|
||||
|
||||
pub fn alloc(&mut self, usage: FrameUse) -> Option<PhysicalAddress> {
|
||||
let idx = self.frames[..self.total].iter().position(Option::is_none)?;
|
||||
self.frames[idx] = Some(usage);
|
||||
self.free -= 1;
|
||||
Some(self.address_of(idx))
|
||||
}
|
||||
|
||||
/// Allocates `count` physically adjacent frames; frame `i` gets `usage(i)`.
|
||||
pub fn alloc_contiguous(
|
||||
&mut self,
|
||||
count: usize,
|
||||
usage: impl Fn(usize) -> FrameUse,
|
||||
) -> Option<PhysicalAddress> {
|
||||
if count == 0 {
|
||||
return None;
|
||||
}
|
||||
let mut run = 0;
|
||||
let mut start = None;
|
||||
for idx in 0..self.total {
|
||||
if self.frames[idx].is_some() {
|
||||
run = 0;
|
||||
continue;
|
||||
}
|
||||
run += 1;
|
||||
if run == count {
|
||||
start = Some(idx + 1 - count);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let start = start?;
|
||||
for i in 0..count {
|
||||
self.frames[start + i] = Some(usage(i));
|
||||
}
|
||||
self.free -= count;
|
||||
Some(self.address_of(start))
|
||||
}
|
||||
|
||||
/// Frees an allocated frame and reports what it was used for.
|
||||
pub fn free(&mut self, pa: PhysicalAddress) -> Option<FrameUse> {
|
||||
let idx = self.index_of(pa)?;
|
||||
let usage = self.frames[idx].take()?;
|
||||
self.free += 1;
|
||||
Some(usage)
|
||||
}
|
||||
|
||||
/// Changes the use of an allocated frame (e.g. moving it to another
|
||||
/// process). Fails on free frames.
|
||||
pub fn retag(&mut self, pa: PhysicalAddress, usage: FrameUse) -> bool {
|
||||
match self.index_of(pa).and_then(|i| self.frames[i].as_mut()) {
|
||||
Some(slot) => {
|
||||
*slot = usage;
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The frame mapped at `va` in `owner`, if `owner` holds it as a user page.
|
||||
pub fn find_user(
|
||||
&self,
|
||||
owner: ProcessId,
|
||||
va: VirtualAddress,
|
||||
) -> Option<(PhysicalAddress, PagePerms)> {
|
||||
if !va.raw().is_multiple_of(PAGE_SIZE) {
|
||||
return None;
|
||||
}
|
||||
let page = u32::try_from(va.raw() / PAGE_SIZE).ok()?;
|
||||
self.frames[..self.total]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(idx, f)| match f {
|
||||
Some(FrameUse::User {
|
||||
owner: o,
|
||||
page: p,
|
||||
perms,
|
||||
}) if *o == owner && *p == page => Some((self.address_of(idx), *perms)),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Every allocated frame and its use.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (PhysicalAddress, &FrameUse)> + '_ {
|
||||
self.frames[..self.total]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(idx, f)| f.as_ref().map(|u| (self.address_of(idx), u)))
|
||||
}
|
||||
|
||||
/// User pages of `owner` as (frame, virtual address) pairs.
|
||||
pub fn user_frames_of(
|
||||
&self,
|
||||
owner: ProcessId,
|
||||
) -> impl Iterator<Item = (PhysicalAddress, VirtualAddress)> + '_ {
|
||||
self.frames[..self.total]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(move |(idx, f)| match f {
|
||||
Some(FrameUse::User { owner: o, page, .. }) if *o == owner => Some((
|
||||
self.address_of(idx),
|
||||
VirtualAddress::from_raw(*page as usize * PAGE_SIZE),
|
||||
)),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Frees every user and page-table frame charged to `owner`; returns
|
||||
/// how many were freed.
|
||||
pub fn free_owned_by(&mut self, owner: ProcessId) -> usize {
|
||||
let mut freed = 0;
|
||||
for slot in &mut self.frames[..self.total] {
|
||||
let owned = matches!(
|
||||
slot,
|
||||
Some(FrameUse::User { owner: o, .. } | FrameUse::PageTable { owner: o }) if *o == owner
|
||||
);
|
||||
if owned {
|
||||
*slot = None;
|
||||
freed += 1;
|
||||
}
|
||||
}
|
||||
self.free += freed;
|
||||
freed
|
||||
}
|
||||
}
|
||||
195
crates/domain/src/memory/frame/proofs.rs
Normal file
195
crates/domain/src/memory/frame/proofs.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
//! Bounded proofs over every reachable state of a small frame table.
|
||||
//! Each harness starts from an arbitrary *well-formed* table (free count
|
||||
//! matches the number of empty slots) and checks one operation.
|
||||
|
||||
use super::*;
|
||||
use types::address::{PhysicalAddress, VirtualAddress};
|
||||
use types::id::{ProcessId, RegionId};
|
||||
use types::mapping::PagePerms;
|
||||
|
||||
const N: usize = 4;
|
||||
const BASE: usize = 0x8400_0000;
|
||||
|
||||
fn any_perms() -> PagePerms {
|
||||
match kani::any::<u8>() % 3 {
|
||||
0 => PagePerms::ReadOnly,
|
||||
1 => PagePerms::ReadWrite,
|
||||
_ => PagePerms::ReadExecute,
|
||||
}
|
||||
}
|
||||
|
||||
fn any_use() -> FrameUse {
|
||||
// Small id ranges keep owner collisions likely, which is where the
|
||||
// interesting cases are.
|
||||
let owner = ProcessId::from_raw(kani::any::<u16>() % 3);
|
||||
match kani::any::<u8>() % 3 {
|
||||
0 => FrameUse::User {
|
||||
owner,
|
||||
page: kani::any::<u32>() % 4,
|
||||
perms: any_perms(),
|
||||
},
|
||||
1 => FrameUse::PageTable { owner },
|
||||
_ => FrameUse::Region {
|
||||
region: RegionId::from_raw(kani::any::<u16>() % 2),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn any_slot() -> Option<FrameUse> {
|
||||
if kani::any() { Some(any_use()) } else { None }
|
||||
}
|
||||
|
||||
fn any_table() -> FrameTable<N> {
|
||||
let frames = [any_slot(), any_slot(), any_slot(), any_slot()];
|
||||
let free = frames.iter().filter(|f| f.is_none()).count();
|
||||
FrameTable {
|
||||
base: PhysicalAddress::from_raw(BASE),
|
||||
total: N,
|
||||
frames,
|
||||
free,
|
||||
}
|
||||
}
|
||||
|
||||
fn well_formed(t: &FrameTable<N>) -> bool {
|
||||
t.free == t.frames[..t.total].iter().filter(|f| f.is_none()).count()
|
||||
}
|
||||
|
||||
fn any_pa() -> PhysicalAddress {
|
||||
// Anything from below the pool to past its end, aligned or not.
|
||||
PhysicalAddress::from_raw(BASE - 4096 + kani::any::<usize>() % ((N + 2) * 4096))
|
||||
}
|
||||
|
||||
#[kani::proof]
|
||||
#[kani::unwind(6)]
|
||||
fn alloc_only_hands_out_free_frames() {
|
||||
let mut t = any_table();
|
||||
let before = t.frames;
|
||||
let usage = any_use();
|
||||
match t.alloc(usage) {
|
||||
Some(pa) => {
|
||||
let idx = (pa.raw() - BASE) / PAGE_SIZE;
|
||||
assert!(idx < N);
|
||||
assert!(before[idx].is_none());
|
||||
assert!(t.frames[idx] == Some(usage));
|
||||
for i in 0..N {
|
||||
if i != idx {
|
||||
assert!(t.frames[i] == before[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
assert!(before.iter().all(Option::is_some));
|
||||
assert!(t.frames == before);
|
||||
}
|
||||
}
|
||||
assert!(well_formed(&t));
|
||||
}
|
||||
|
||||
#[kani::proof]
|
||||
#[kani::unwind(6)]
|
||||
fn free_releases_exactly_one_allocated_frame_or_nothing() {
|
||||
let mut t = any_table();
|
||||
let before = t.frames;
|
||||
let pa = any_pa();
|
||||
match t.free(pa) {
|
||||
Some(usage) => {
|
||||
let idx = (pa.raw() - BASE) / PAGE_SIZE;
|
||||
assert!(pa.raw() % PAGE_SIZE == 0);
|
||||
assert!(before[idx] == Some(usage));
|
||||
assert!(t.frames[idx].is_none());
|
||||
for i in 0..N {
|
||||
if i != idx {
|
||||
assert!(t.frames[i] == before[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => assert!(t.frames == before),
|
||||
}
|
||||
assert!(well_formed(&t));
|
||||
}
|
||||
|
||||
#[kani::proof]
|
||||
#[kani::unwind(6)]
|
||||
fn retag_never_allocates() {
|
||||
let mut t = any_table();
|
||||
let before = t.frames;
|
||||
let pa = any_pa();
|
||||
let usage = any_use();
|
||||
let changed = t.retag(pa, usage);
|
||||
for i in 0..N {
|
||||
if before[i].is_none() {
|
||||
assert!(t.frames[i].is_none());
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
assert!(t.frames == before);
|
||||
}
|
||||
assert!(well_formed(&t));
|
||||
}
|
||||
|
||||
#[kani::proof]
|
||||
#[kani::unwind(6)]
|
||||
fn free_owned_by_spares_everyone_else() {
|
||||
let mut t = any_table();
|
||||
let before = t.frames;
|
||||
let owner = ProcessId::from_raw(kani::any::<u16>() % 3);
|
||||
t.free_owned_by(owner);
|
||||
for i in 0..N {
|
||||
let was_owned = matches!(
|
||||
before[i],
|
||||
Some(FrameUse::User { owner: o, .. } | FrameUse::PageTable { owner: o }) if o == owner
|
||||
);
|
||||
if was_owned {
|
||||
assert!(t.frames[i].is_none());
|
||||
} else {
|
||||
assert!(t.frames[i] == before[i]);
|
||||
}
|
||||
}
|
||||
assert!(well_formed(&t));
|
||||
}
|
||||
|
||||
#[kani::proof]
|
||||
#[kani::unwind(6)]
|
||||
fn find_user_only_reports_the_owners_mapping() {
|
||||
let t = any_table();
|
||||
let owner = ProcessId::from_raw(kani::any::<u16>() % 3);
|
||||
let va = VirtualAddress::from_raw(kani::any::<usize>() % (5 * 4096));
|
||||
if let Some((pa, perms)) = t.find_user(owner, va) {
|
||||
let idx = (pa.raw() - BASE) / PAGE_SIZE;
|
||||
assert!(idx < N);
|
||||
assert!(va.raw() % PAGE_SIZE == 0);
|
||||
assert!(
|
||||
t.frames[idx]
|
||||
== Some(FrameUse::User {
|
||||
owner,
|
||||
page: (va.raw() / PAGE_SIZE) as u32,
|
||||
perms,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[kani::proof]
|
||||
#[kani::unwind(6)]
|
||||
fn alloc_contiguous_only_claims_a_free_run() {
|
||||
let mut t = any_table();
|
||||
let before = t.frames;
|
||||
let count = kani::any::<usize>() % (N + 2);
|
||||
let region = RegionId::from_raw(1);
|
||||
match t.alloc_contiguous(count, |_| FrameUse::Region { region }) {
|
||||
Some(pa) => {
|
||||
let start = (pa.raw() - BASE) / PAGE_SIZE;
|
||||
assert!(count > 0 && start + count <= N);
|
||||
for i in 0..N {
|
||||
if i >= start && i < start + count {
|
||||
assert!(before[i].is_none());
|
||||
assert!(t.frames[i] == Some(FrameUse::Region { region }));
|
||||
} else {
|
||||
assert!(t.frames[i] == before[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => assert!(t.frames == before),
|
||||
}
|
||||
assert!(well_formed(&t));
|
||||
}
|
||||
188
crates/domain/src/memory/frame/tests.rs
Normal file
188
crates/domain/src/memory/frame/tests.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
use super::*;
|
||||
use types::address::{PhysicalAddress, VirtualAddress};
|
||||
use types::id::{ProcessId, RegionId};
|
||||
use types::mapping::PagePerms;
|
||||
|
||||
const BASE: usize = 0x8400_0000;
|
||||
|
||||
fn table() -> FrameTable<8> {
|
||||
FrameTable::new(PhysicalAddress::from_raw(BASE), 8)
|
||||
}
|
||||
|
||||
fn pid(n: u16) -> ProcessId {
|
||||
ProcessId::from_raw(n)
|
||||
}
|
||||
|
||||
fn va(addr: usize) -> VirtualAddress {
|
||||
VirtualAddress::from_raw(addr)
|
||||
}
|
||||
|
||||
fn user(owner: u16, addr: usize) -> FrameUse {
|
||||
FrameUse::user(pid(owner), va(addr), PagePerms::ReadWrite).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_table_has_every_frame_free() {
|
||||
let t = table();
|
||||
assert_eq!(t.free_count(), 8);
|
||||
assert_eq!(t.capacity(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn total_is_clamped_to_capacity() {
|
||||
let t: FrameTable<4> = FrameTable::new(PhysicalAddress::from_raw(BASE), 100);
|
||||
assert_eq!(t.free_count(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_use_requires_page_aligned_va() {
|
||||
assert!(FrameUse::user(pid(0), va(0x1234), PagePerms::ReadOnly).is_none());
|
||||
assert!(FrameUse::user(pid(0), va(0x1000), PagePerms::ReadOnly).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alloc_hands_out_distinct_frames_inside_the_pool() {
|
||||
let mut t = table();
|
||||
let a = t.alloc(user(0, 0x1000)).unwrap();
|
||||
let b = t.alloc(user(0, 0x2000)).unwrap();
|
||||
assert_ne!(a, b);
|
||||
for pa in [a, b] {
|
||||
assert!(pa.raw() >= BASE && pa.raw() < BASE + 8 * 4096);
|
||||
assert_eq!(pa.raw() % 4096, 0);
|
||||
}
|
||||
assert_eq!(t.free_count(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alloc_fails_when_exhausted() {
|
||||
let mut t = table();
|
||||
for i in 0..8 {
|
||||
t.alloc(user(0, (i + 1) * 0x1000)).unwrap();
|
||||
}
|
||||
assert!(t.alloc(user(0, 0x9000)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_returns_the_frame_and_its_use() {
|
||||
let mut t = table();
|
||||
let pa = t.alloc(user(3, 0x5000)).unwrap();
|
||||
assert_eq!(t.free(pa), Some(user(3, 0x5000)));
|
||||
assert_eq!(t.free_count(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn freeing_a_free_frame_is_rejected() {
|
||||
let mut t = table();
|
||||
let pa = t.alloc(user(0, 0x1000)).unwrap();
|
||||
t.free(pa).unwrap();
|
||||
assert_eq!(t.free(pa), None);
|
||||
assert_eq!(t.free_count(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn freeing_outside_the_pool_or_unaligned_is_rejected() {
|
||||
let mut t = table();
|
||||
t.alloc(user(0, 0x1000)).unwrap();
|
||||
assert_eq!(t.free(PhysicalAddress::from_raw(BASE - 4096)), None);
|
||||
assert_eq!(t.free(PhysicalAddress::from_raw(BASE + 8 * 4096)), None);
|
||||
assert_eq!(t.free(PhysicalAddress::from_raw(BASE + 1)), None);
|
||||
assert_eq!(t.free_count(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_user_matches_owner_and_va() {
|
||||
let mut t = table();
|
||||
let pa = t.alloc(user(1, 0x3000)).unwrap();
|
||||
assert_eq!(
|
||||
t.find_user(pid(1), va(0x3000)),
|
||||
Some((pa, PagePerms::ReadWrite))
|
||||
);
|
||||
assert_eq!(t.find_user(pid(2), va(0x3000)), None);
|
||||
assert_eq!(t.find_user(pid(1), va(0x4000)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retag_moves_a_frame_to_a_new_use() {
|
||||
let mut t = table();
|
||||
let pa = t.alloc(user(1, 0x3000)).unwrap();
|
||||
assert!(t.retag(pa, user(2, 0x7000)));
|
||||
assert_eq!(t.find_user(pid(1), va(0x3000)), None);
|
||||
assert_eq!(t.find_user(pid(2), va(0x7000)).map(|(p, _)| p), Some(pa));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retag_of_a_free_frame_is_rejected() {
|
||||
let mut t = table();
|
||||
let pa = t.alloc(user(1, 0x3000)).unwrap();
|
||||
t.free(pa);
|
||||
assert!(!t.retag(pa, user(2, 0x7000)));
|
||||
assert_eq!(t.free_count(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alloc_contiguous_returns_adjacent_frames() {
|
||||
let mut t = table();
|
||||
let base = t
|
||||
.alloc_contiguous(3, |_| FrameUse::Region {
|
||||
region: RegionId::from_raw(0),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(t.free_count(), 5);
|
||||
for i in 0..3 {
|
||||
assert_eq!(
|
||||
t.get(base.offset(i)),
|
||||
Some(&FrameUse::Region {
|
||||
region: RegionId::from_raw(0)
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alloc_contiguous_fails_without_a_long_enough_run() {
|
||||
let mut t: FrameTable<4> = FrameTable::new(PhysicalAddress::from_raw(BASE), 4);
|
||||
let a = t.alloc(user(0, 0x1000)).unwrap();
|
||||
let _b = t.alloc(user(0, 0x2000)).unwrap();
|
||||
let _c = t.alloc(user(0, 0x3000)).unwrap();
|
||||
t.free(a);
|
||||
// free frames: 0 and 3 — no run of 2
|
||||
assert!(
|
||||
t.alloc_contiguous(2, |_| FrameUse::PageTable { owner: pid(0) })
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(t.free_count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_owned_by_releases_only_that_processes_frames() {
|
||||
let mut t = table();
|
||||
t.alloc(user(1, 0x1000)).unwrap();
|
||||
t.alloc(FrameUse::PageTable { owner: pid(1) }).unwrap();
|
||||
let other = t.alloc(user(2, 0x1000)).unwrap();
|
||||
let region = t
|
||||
.alloc(FrameUse::Region {
|
||||
region: RegionId::from_raw(1),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(t.free_owned_by(pid(1)), 2);
|
||||
assert_eq!(t.free_count(), 6);
|
||||
assert!(t.get(other).is_some());
|
||||
assert!(t.get(region).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_frames_of_lists_mappings_of_one_process() {
|
||||
let mut t = table();
|
||||
t.alloc(user(1, 0x1000)).unwrap();
|
||||
t.alloc(user(2, 0x2000)).unwrap();
|
||||
t.alloc(user(1, 0x3000)).unwrap();
|
||||
t.alloc(FrameUse::PageTable { owner: pid(1) }).unwrap();
|
||||
let mut vas = [0usize; 4];
|
||||
let mut n = 0;
|
||||
for (_, v) in t.user_frames_of(pid(1)) {
|
||||
vas[n] = v.raw();
|
||||
n += 1;
|
||||
}
|
||||
assert_eq!(&vas[..n], &[0x1000, 0x3000]);
|
||||
}
|
||||
@@ -1,58 +1,83 @@
|
||||
use types::{
|
||||
address::PhysicalAddress,
|
||||
address::{PhysicalAddress, VirtualAddress},
|
||||
bounded::PageCount,
|
||||
generation::Generation,
|
||||
id::{ProcessId, RegionId},
|
||||
mapping::PagePerms,
|
||||
};
|
||||
|
||||
pub mod page_pool;
|
||||
pub mod process_memory;
|
||||
pub mod frame;
|
||||
pub mod quota;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "quota_tests.rs"]
|
||||
mod quota_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests.rs"]
|
||||
mod tests;
|
||||
|
||||
const MAX_REGIONS: usize = 256;
|
||||
pub const MAX_REGIONS: usize = 64;
|
||||
pub const MAX_REGION_MAPPINGS: usize = 4;
|
||||
|
||||
pub struct MemoryRegion {
|
||||
id: RegionId,
|
||||
owner: ProcessId,
|
||||
base: PhysicalAddress,
|
||||
size: PageCount,
|
||||
generation: Generation,
|
||||
/// Where a region's frames come from.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RegionBacking {
|
||||
/// Frames from the user pool, freed with the region.
|
||||
Pool,
|
||||
/// Device MMIO; never freed.
|
||||
Device,
|
||||
}
|
||||
|
||||
impl MemoryRegion {
|
||||
pub fn new(
|
||||
id: RegionId,
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct RegionMapping {
|
||||
pub process: ProcessId,
|
||||
pub va: VirtualAddress,
|
||||
pub perms: PagePerms,
|
||||
}
|
||||
|
||||
/// Physically contiguous memory that may be mapped into several address
|
||||
/// spaces. `owner` is charged for pool-backed frames.
|
||||
pub struct Region {
|
||||
owner: ProcessId,
|
||||
backing: RegionBacking,
|
||||
base: PhysicalAddress,
|
||||
size: PageCount,
|
||||
pages: PageCount,
|
||||
generation: Generation,
|
||||
mappings: [Option<RegionMapping>; MAX_REGION_MAPPINGS],
|
||||
}
|
||||
|
||||
impl Region {
|
||||
pub fn new(
|
||||
owner: ProcessId,
|
||||
backing: RegionBacking,
|
||||
base: PhysicalAddress,
|
||||
pages: PageCount,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
owner,
|
||||
backing,
|
||||
base,
|
||||
size,
|
||||
generation,
|
||||
pages,
|
||||
generation: Generation::initial(),
|
||||
mappings: [None; MAX_REGION_MAPPINGS],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> RegionId {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn owner(&self) -> ProcessId {
|
||||
self.owner
|
||||
}
|
||||
|
||||
pub fn backing(&self) -> RegionBacking {
|
||||
self.backing
|
||||
}
|
||||
|
||||
pub fn base(&self) -> PhysicalAddress {
|
||||
self.base
|
||||
}
|
||||
|
||||
pub fn size(&self) -> PageCount {
|
||||
self.size
|
||||
pub fn pages(&self) -> PageCount {
|
||||
self.pages
|
||||
}
|
||||
|
||||
pub fn generation(&self) -> &Generation {
|
||||
@@ -62,10 +87,43 @@ impl MemoryRegion {
|
||||
pub fn revoke(&mut self) {
|
||||
self.generation = self.generation.next();
|
||||
}
|
||||
|
||||
pub fn mappings(&self) -> impl Iterator<Item = &RegionMapping> {
|
||||
self.mappings.iter().flatten()
|
||||
}
|
||||
|
||||
/// Records that `mapping.process` maps this region. Each process maps a
|
||||
/// region at most once.
|
||||
pub fn add_mapping(&mut self, mapping: RegionMapping) -> bool {
|
||||
if self.mappings().any(|m| m.process == mapping.process) {
|
||||
return false;
|
||||
}
|
||||
match self.mappings.iter_mut().find(|m| m.is_none()) {
|
||||
Some(slot) => {
|
||||
*slot = Some(mapping);
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_mapping(&mut self, process: ProcessId) -> Option<RegionMapping> {
|
||||
self.mappings
|
||||
.iter_mut()
|
||||
.find(|m| m.is_some_and(|m| m.process == process))?
|
||||
.take()
|
||||
}
|
||||
|
||||
/// Whether `va` falls inside this region's mapping in `process`.
|
||||
pub fn covers(&self, process: ProcessId, va: VirtualAddress) -> bool {
|
||||
let len = self.pages.value() as usize * frame::PAGE_SIZE;
|
||||
self.mappings()
|
||||
.any(|m| m.process == process && va.raw() >= m.va.raw() && va.raw() - m.va.raw() < len)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RegionTable {
|
||||
regions: [Option<MemoryRegion>; MAX_REGIONS],
|
||||
regions: [Option<Region>; MAX_REGIONS],
|
||||
}
|
||||
|
||||
impl RegionTable {
|
||||
@@ -75,50 +133,60 @@ impl RegionTable {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, region: MemoryRegion) -> Option<RegionId> {
|
||||
/// The id the next `insert` will return.
|
||||
pub fn next_free_id(&self) -> Option<RegionId> {
|
||||
let idx = self.regions.iter().position(|s| s.is_none())?;
|
||||
Some(RegionId::from_raw(idx as u16))
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, region: Region) -> Option<RegionId> {
|
||||
let idx = self.regions.iter().position(|s| s.is_none())?;
|
||||
let id = RegionId::from_raw(idx as u16);
|
||||
self.regions[idx] = Some(region);
|
||||
Some(id)
|
||||
Some(RegionId::from_raw(idx as u16))
|
||||
}
|
||||
|
||||
pub fn get(&self, id: RegionId) -> Option<&MemoryRegion> {
|
||||
self.regions.get(id.raw() as usize).and_then(|s| s.as_ref())
|
||||
pub fn get(&self, id: RegionId) -> Option<&Region> {
|
||||
self.regions.get(id.raw() as usize)?.as_ref()
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, id: RegionId) -> Option<&mut MemoryRegion> {
|
||||
self.regions
|
||||
.get_mut(id.raw() as usize)
|
||||
.and_then(|s| s.as_mut())
|
||||
pub fn get_mut(&mut self, id: RegionId) -> Option<&mut Region> {
|
||||
self.regions.get_mut(id.raw() as usize)?.as_mut()
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, id: RegionId) -> Option<MemoryRegion> {
|
||||
self.regions
|
||||
.get_mut(id.raw() as usize)
|
||||
.and_then(|s| s.take())
|
||||
pub fn remove(&mut self, id: RegionId) -> Option<Region> {
|
||||
self.regions.get_mut(id.raw() as usize)?.take()
|
||||
}
|
||||
|
||||
pub fn find_by_base(
|
||||
&self,
|
||||
owner: ProcessId,
|
||||
base: PhysicalAddress,
|
||||
) -> Option<(RegionId, &MemoryRegion)> {
|
||||
for (i, slot) in self.regions.iter().enumerate() {
|
||||
if let Some(region) = slot
|
||||
&& region.owner() == owner
|
||||
&& region.base() == base
|
||||
{
|
||||
return Some((RegionId::from_raw(i as u16), region));
|
||||
}
|
||||
}
|
||||
None
|
||||
pub fn ids_owned_by(&self, owner: ProcessId) -> impl Iterator<Item = RegionId> + '_ {
|
||||
self.regions.iter().enumerate().filter_map(move |(i, r)| {
|
||||
r.as_ref()
|
||||
.filter(|r| r.owner == owner)
|
||||
.map(|_| RegionId::from_raw(i as u16))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn regions_for_process(&self, process: ProcessId) -> impl Iterator<Item = &MemoryRegion> {
|
||||
pub fn iter(&self) -> impl Iterator<Item = (RegionId, &Region)> {
|
||||
self.regions
|
||||
.iter()
|
||||
.filter_map(|s| s.as_ref())
|
||||
.filter(move |r| r.owner() == process)
|
||||
.enumerate()
|
||||
.filter_map(|(i, r)| r.as_ref().map(|r| (RegionId::from_raw(i as u16), r)))
|
||||
}
|
||||
|
||||
/// Every region, by id, so callers can drop a process's mappings.
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = (RegionId, &mut Region)> {
|
||||
self.regions
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.filter_map(|(i, r)| r.as_mut().map(|r| (RegionId::from_raw(i as u16), r)))
|
||||
}
|
||||
|
||||
/// The region whose mapping in `process` covers `va`.
|
||||
pub fn covering(&self, process: ProcessId, va: VirtualAddress) -> Option<RegionId> {
|
||||
self.regions.iter().enumerate().find_map(|(i, r)| {
|
||||
r.as_ref()
|
||||
.filter(|r| r.covers(process, va))
|
||||
.map(|_| RegionId::from_raw(i as u16))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
use types::address::PhysicalAddress;
|
||||
|
||||
const MAX_PAGES: usize = 16384;
|
||||
|
||||
pub struct PagePool {
|
||||
pages: [bool; MAX_PAGES],
|
||||
base: PhysicalAddress,
|
||||
total: usize,
|
||||
free_count: usize,
|
||||
}
|
||||
|
||||
impl PagePool {
|
||||
pub fn new(base: PhysicalAddress, total_pages: usize) -> Self {
|
||||
let mut pages = [false; MAX_PAGES];
|
||||
pages[..total_pages].fill(true);
|
||||
Self {
|
||||
pages,
|
||||
base,
|
||||
total: total_pages,
|
||||
free_count: total_pages,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn allocate(&mut self, count: usize) -> Option<PhysicalAddress> {
|
||||
let start = self.find_contiguous(count)?;
|
||||
for i in start..start + count {
|
||||
self.pages[i] = false;
|
||||
}
|
||||
self.free_count -= count;
|
||||
Some(self.base.offset(start))
|
||||
}
|
||||
|
||||
pub fn free(&mut self, addr: PhysicalAddress, count: usize) -> bool {
|
||||
if addr.raw() < self.base.raw() {
|
||||
return false;
|
||||
}
|
||||
let start = (addr.raw() - self.base.raw()) / 4096;
|
||||
if start + count > self.total {
|
||||
return false;
|
||||
}
|
||||
for i in start..start + count {
|
||||
self.pages[i] = true;
|
||||
}
|
||||
self.free_count += count;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn free_count(&self) -> usize {
|
||||
self.free_count
|
||||
}
|
||||
|
||||
fn find_contiguous(&self, count: usize) -> Option<usize> {
|
||||
let mut run = 0;
|
||||
for i in 0..self.total {
|
||||
if self.pages[i] {
|
||||
run += 1;
|
||||
if run == count {
|
||||
return Some(i + 1 - count);
|
||||
}
|
||||
} else {
|
||||
run = 0;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
use types::address::{PhysicalAddress, VirtualAddress};
|
||||
use types::id::ProcessId;
|
||||
|
||||
use crate::error::KernelError;
|
||||
|
||||
const USER_HEAP_START: usize = 0x40000;
|
||||
const PAGE_SIZE: usize = 4096;
|
||||
const MAX_ALLOCATIONS: usize = 16;
|
||||
|
||||
pub struct ProcessMemory {
|
||||
process: ProcessId,
|
||||
page_count: usize,
|
||||
limit: usize,
|
||||
next_heap_va: usize,
|
||||
allocations: [(PhysicalAddress, usize); MAX_ALLOCATIONS],
|
||||
alloc_count: usize,
|
||||
}
|
||||
|
||||
impl ProcessMemory {
|
||||
pub const DEFAULT_PAGE_LIMIT: usize = 1024;
|
||||
|
||||
pub fn new(process: ProcessId, limit: usize) -> Self {
|
||||
Self {
|
||||
process,
|
||||
page_count: 0,
|
||||
limit,
|
||||
next_heap_va: USER_HEAP_START,
|
||||
allocations: [(PhysicalAddress::from_raw(0), 0); MAX_ALLOCATIONS],
|
||||
alloc_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn alloc_user_va(&mut self, pages: usize) -> VirtualAddress {
|
||||
let va = self.next_heap_va;
|
||||
self.next_heap_va += pages * PAGE_SIZE;
|
||||
VirtualAddress::from_raw(va)
|
||||
}
|
||||
|
||||
pub fn record_allocation(&mut self, pa: PhysicalAddress, count: usize) {
|
||||
if self.alloc_count < MAX_ALLOCATIONS {
|
||||
self.allocations[self.alloc_count] = (pa, count);
|
||||
self.alloc_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn allocations(&self) -> &[(PhysicalAddress, usize)] {
|
||||
&self.allocations[..self.alloc_count]
|
||||
}
|
||||
|
||||
pub fn process(&self) -> ProcessId {
|
||||
self.process
|
||||
}
|
||||
|
||||
pub fn page_count(&self) -> usize {
|
||||
self.page_count
|
||||
}
|
||||
|
||||
pub fn limit(&self) -> usize {
|
||||
self.limit
|
||||
}
|
||||
|
||||
pub fn grant(&mut self, count: usize) -> Result<(), KernelError> {
|
||||
if self.page_count + count > self.limit {
|
||||
return Err(KernelError::OutOfMemory);
|
||||
}
|
||||
self.page_count += count;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn release(&mut self, count: usize) -> Result<(), KernelError> {
|
||||
if count > self.page_count {
|
||||
return Err(KernelError::OutOfMemory);
|
||||
}
|
||||
self.page_count -= count;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
57
crates/domain/src/memory/quota.rs
Normal file
57
crates/domain/src/memory/quota.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
/// Per-process page budget. Every frame charged to a process (user pages
|
||||
/// and its page tables) counts against it.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Quota {
|
||||
used: usize,
|
||||
limit: usize,
|
||||
}
|
||||
|
||||
impl Quota {
|
||||
pub const DEFAULT_LIMIT: usize = 1024;
|
||||
|
||||
pub const fn new(limit: usize) -> Self {
|
||||
Self { used: 0, limit }
|
||||
}
|
||||
|
||||
pub fn used(&self) -> usize {
|
||||
self.used
|
||||
}
|
||||
|
||||
pub fn limit(&self) -> usize {
|
||||
self.limit
|
||||
}
|
||||
|
||||
pub fn charge(&mut self, pages: usize) -> bool {
|
||||
match self.used.checked_add(pages) {
|
||||
Some(total) if total <= self.limit => {
|
||||
self.used = total;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uncharge(&mut self, pages: usize) -> bool {
|
||||
match self.used.checked_sub(pages) {
|
||||
Some(rest) => {
|
||||
self.used = rest;
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_limit(&mut self, limit: usize) -> bool {
|
||||
if limit < self.used {
|
||||
return false;
|
||||
}
|
||||
self.limit = limit;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Quota {
|
||||
fn default() -> Self {
|
||||
Self::new(Self::DEFAULT_LIMIT)
|
||||
}
|
||||
}
|
||||
37
crates/domain/src/memory/quota_tests.rs
Normal file
37
crates/domain/src/memory/quota_tests.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use super::quota::Quota;
|
||||
|
||||
#[test]
|
||||
fn charge_within_limit_succeeds() {
|
||||
let mut q = Quota::new(4);
|
||||
assert!(q.charge(3));
|
||||
assert_eq!(q.used(), 3);
|
||||
assert!(q.charge(1));
|
||||
assert_eq!(q.used(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn charge_past_limit_fails_and_changes_nothing() {
|
||||
let mut q = Quota::new(4);
|
||||
q.charge(3);
|
||||
assert!(!q.charge(2));
|
||||
assert_eq!(q.used(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uncharge_cannot_underflow() {
|
||||
let mut q = Quota::new(4);
|
||||
q.charge(1);
|
||||
assert!(!q.uncharge(2));
|
||||
assert_eq!(q.used(), 1);
|
||||
assert!(q.uncharge(1));
|
||||
assert_eq!(q.used(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limit_can_be_raised_but_not_below_usage() {
|
||||
let mut q = Quota::new(4);
|
||||
q.charge(3);
|
||||
assert!(!q.set_limit(2));
|
||||
assert!(q.set_limit(10));
|
||||
assert!(q.charge(7));
|
||||
}
|
||||
@@ -1,221 +1,117 @@
|
||||
use super::page_pool::PagePool;
|
||||
use super::process_memory::ProcessMemory;
|
||||
use super::*;
|
||||
use types::address::PhysicalAddress;
|
||||
use types::address::{PhysicalAddress, VirtualAddress};
|
||||
use types::bounded::PageCount;
|
||||
use types::generation::Generation;
|
||||
use types::id::{ProcessId, RegionId};
|
||||
use types::mapping::PagePerms;
|
||||
|
||||
fn base() -> PhysicalAddress {
|
||||
PhysicalAddress::from_raw(0x8000_0000)
|
||||
fn pid(n: u16) -> ProcessId {
|
||||
ProcessId::from_raw(n)
|
||||
}
|
||||
|
||||
fn process(id: u16) -> ProcessId {
|
||||
ProcessId::from_raw(id)
|
||||
fn va(addr: usize) -> VirtualAddress {
|
||||
VirtualAddress::from_raw(addr)
|
||||
}
|
||||
|
||||
fn pages(n: u16) -> PageCount {
|
||||
PageCount::new(n).unwrap()
|
||||
fn pool_region(owner: u16, pages: u16) -> Region {
|
||||
Region::new(
|
||||
pid(owner),
|
||||
RegionBacking::Pool,
|
||||
PhysicalAddress::from_raw(0x8400_0000),
|
||||
PageCount::new(pages).unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
// --- PagePool tests ---
|
||||
|
||||
#[test]
|
||||
fn new_pool_has_all_pages_free() {
|
||||
let pool = PagePool::new(base(), 100);
|
||||
assert_eq!(pool.free_count(), 100);
|
||||
fn mapping(process: u16, addr: usize) -> RegionMapping {
|
||||
RegionMapping {
|
||||
process: pid(process),
|
||||
va: va(addr),
|
||||
perms: PagePerms::ReadWrite,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocate_reduces_free_count() {
|
||||
let mut pool = PagePool::new(base(), 100);
|
||||
pool.allocate(10).unwrap();
|
||||
assert_eq!(pool.free_count(), 90);
|
||||
fn new_region_has_no_mappings() {
|
||||
let r = pool_region(0, 4);
|
||||
assert_eq!(r.mappings().count(), 0);
|
||||
assert_eq!(r.owner(), pid(0));
|
||||
assert_eq!(r.backing(), RegionBacking::Pool);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocate_returns_base_aligned_address() {
|
||||
let mut pool = PagePool::new(base(), 100);
|
||||
let addr = pool.allocate(1).unwrap();
|
||||
assert_eq!(addr, base());
|
||||
fn a_process_maps_a_region_at_most_once() {
|
||||
let mut r = pool_region(0, 4);
|
||||
assert!(r.add_mapping(mapping(1, 0x30000)));
|
||||
assert!(!r.add_mapping(mapping(1, 0x50000)));
|
||||
assert!(r.add_mapping(mapping(2, 0x30000)));
|
||||
assert_eq!(r.mappings().count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocate_sequential_returns_contiguous() {
|
||||
let mut pool = PagePool::new(base(), 100);
|
||||
let a = pool.allocate(4).unwrap();
|
||||
let b = pool.allocate(4).unwrap();
|
||||
assert_eq!(a, base());
|
||||
assert_eq!(b, base().offset(4));
|
||||
fn mapping_slots_are_bounded() {
|
||||
let mut r = pool_region(0, 1);
|
||||
for p in 0..MAX_REGION_MAPPINGS as u16 {
|
||||
assert!(r.add_mapping(mapping(p, 0x30000)));
|
||||
}
|
||||
assert!(!r.add_mapping(mapping(99, 0x30000)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocate_too_many_returns_none() {
|
||||
let mut pool = PagePool::new(base(), 10);
|
||||
assert!(pool.allocate(11).is_none());
|
||||
fn remove_mapping_returns_it() {
|
||||
let mut r = pool_region(0, 4);
|
||||
r.add_mapping(mapping(1, 0x30000));
|
||||
assert_eq!(r.remove_mapping(pid(1)), Some(mapping(1, 0x30000)));
|
||||
assert_eq!(r.remove_mapping(pid(1)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_then_reallocate() {
|
||||
let mut pool = PagePool::new(base(), 10);
|
||||
let addr = pool.allocate(5).unwrap();
|
||||
assert_eq!(pool.free_count(), 5);
|
||||
assert!(pool.free(addr, 5));
|
||||
assert_eq!(pool.free_count(), 10);
|
||||
let addr2 = pool.allocate(5).unwrap();
|
||||
assert_eq!(addr2, base());
|
||||
fn covers_checks_every_page_of_a_mapping() {
|
||||
let mut r = pool_region(0, 4);
|
||||
r.add_mapping(mapping(1, 0x30000));
|
||||
assert!(r.covers(pid(1), va(0x30000)));
|
||||
assert!(r.covers(pid(1), va(0x33000)));
|
||||
assert!(!r.covers(pid(1), va(0x34000)));
|
||||
assert!(!r.covers(pid(1), va(0x2F000)));
|
||||
assert!(!r.covers(pid(2), va(0x30000)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_out_of_range_returns_false() {
|
||||
let mut pool = PagePool::new(base(), 10);
|
||||
assert!(!pool.free(PhysicalAddress::from_raw(0), 1));
|
||||
fn region_table_insert_get_remove() {
|
||||
let mut t = RegionTable::new();
|
||||
let id = t.insert(pool_region(0, 2)).unwrap();
|
||||
assert_eq!(t.get(id).map(|r| r.owner()), Some(pid(0)));
|
||||
assert!(t.remove(id).is_some());
|
||||
assert!(t.get(id).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocate_fragmented_finds_gap() {
|
||||
let mut pool = PagePool::new(base(), 10);
|
||||
let a = pool.allocate(3).unwrap();
|
||||
let _b = pool.allocate(3).unwrap();
|
||||
let _c = pool.allocate(3).unwrap();
|
||||
pool.free(a, 3);
|
||||
// now pages 0-2 are free, 3-8 allocated, 9 free
|
||||
// asking for 3 contiguous should find 0-2
|
||||
let d = pool.allocate(3).unwrap();
|
||||
assert_eq!(d, base());
|
||||
fn region_table_is_bounded() {
|
||||
let mut t = RegionTable::new();
|
||||
for _ in 0..MAX_REGIONS {
|
||||
assert!(t.insert(pool_region(0, 1)).is_some());
|
||||
}
|
||||
assert!(t.insert(pool_region(0, 1)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocate_exact_remaining() {
|
||||
let mut pool = PagePool::new(base(), 5);
|
||||
let addr = pool.allocate(5).unwrap();
|
||||
assert_eq!(addr, base());
|
||||
assert_eq!(pool.free_count(), 0);
|
||||
assert!(pool.allocate(1).is_none());
|
||||
}
|
||||
|
||||
// --- ProcessMemory tests ---
|
||||
|
||||
#[test]
|
||||
fn new_process_memory_has_zero_pages() {
|
||||
let pm = ProcessMemory::new(process(0), 100);
|
||||
assert_eq!(pm.page_count(), 0);
|
||||
assert_eq!(pm.limit(), 100);
|
||||
fn region_table_finds_regions_owned_by_a_process() {
|
||||
let mut t = RegionTable::new();
|
||||
let a = t.insert(pool_region(1, 1)).unwrap();
|
||||
t.insert(pool_region(2, 1)).unwrap();
|
||||
let c = t.insert(pool_region(1, 1)).unwrap();
|
||||
let mut ids = [RegionId::from_raw(u16::MAX); 4];
|
||||
let mut n = 0;
|
||||
for id in t.ids_owned_by(pid(1)) {
|
||||
ids[n] = id;
|
||||
n += 1;
|
||||
}
|
||||
assert_eq!(&ids[..n], &[a, c]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grant_pages_within_limit() {
|
||||
let mut pm = ProcessMemory::new(process(0), 100);
|
||||
assert!(pm.grant(10).is_ok());
|
||||
assert_eq!(pm.page_count(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grant_pages_exceeding_limit_fails() {
|
||||
let mut pm = ProcessMemory::new(process(0), 10);
|
||||
assert!(pm.grant(11).is_err());
|
||||
assert_eq!(pm.page_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_pages_reduces_count() {
|
||||
let mut pm = ProcessMemory::new(process(0), 100);
|
||||
pm.grant(20).unwrap();
|
||||
assert!(pm.release(5).is_ok());
|
||||
assert_eq!(pm.page_count(), 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_more_than_owned_fails() {
|
||||
let mut pm = ProcessMemory::new(process(0), 100);
|
||||
pm.grant(5).unwrap();
|
||||
assert!(pm.release(10).is_err());
|
||||
assert_eq!(pm.page_count(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grant_up_to_exact_limit() {
|
||||
let mut pm = ProcessMemory::new(process(0), 10);
|
||||
pm.grant(10).unwrap();
|
||||
assert!(pm.grant(1).is_err());
|
||||
}
|
||||
|
||||
// --- MemoryRegion tests ---
|
||||
|
||||
#[test]
|
||||
fn memory_region_construction_and_accessors() {
|
||||
let region = MemoryRegion::new(
|
||||
RegionId::from_raw(1),
|
||||
process(0),
|
||||
base(),
|
||||
pages(4),
|
||||
Generation::initial(),
|
||||
);
|
||||
assert_eq!(region.id(), RegionId::from_raw(1));
|
||||
assert_eq!(region.owner(), process(0));
|
||||
assert_eq!(region.base(), base());
|
||||
assert_eq!(region.size(), pages(4));
|
||||
assert_eq!(region.generation(), &Generation::initial());
|
||||
}
|
||||
|
||||
// --- RegionTable tests ---
|
||||
|
||||
#[test]
|
||||
fn region_table_insert_and_get() {
|
||||
let mut table = RegionTable::new();
|
||||
let id = table.insert(MemoryRegion::new(
|
||||
RegionId::from_raw(0),
|
||||
process(0),
|
||||
base(),
|
||||
pages(4),
|
||||
Generation::initial(),
|
||||
));
|
||||
assert!(id.is_some());
|
||||
let rid = id.unwrap();
|
||||
assert!(table.get(rid).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn region_table_remove() {
|
||||
let mut table = RegionTable::new();
|
||||
let id = table
|
||||
.insert(MemoryRegion::new(
|
||||
RegionId::from_raw(0),
|
||||
process(0),
|
||||
base(),
|
||||
pages(4),
|
||||
Generation::initial(),
|
||||
))
|
||||
.unwrap();
|
||||
let removed = table.remove(id);
|
||||
assert!(removed.is_some());
|
||||
assert!(table.get(id).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn region_table_regions_for_process() {
|
||||
let mut table = RegionTable::new();
|
||||
table.insert(MemoryRegion::new(
|
||||
RegionId::from_raw(0),
|
||||
process(0),
|
||||
base(),
|
||||
pages(4),
|
||||
Generation::initial(),
|
||||
));
|
||||
table.insert(MemoryRegion::new(
|
||||
RegionId::from_raw(1),
|
||||
process(1),
|
||||
base(),
|
||||
pages(4),
|
||||
Generation::initial(),
|
||||
));
|
||||
table.insert(MemoryRegion::new(
|
||||
RegionId::from_raw(2),
|
||||
process(0),
|
||||
base(),
|
||||
pages(2),
|
||||
Generation::initial(),
|
||||
));
|
||||
assert_eq!(table.regions_for_process(process(0)).count(), 2);
|
||||
assert_eq!(table.regions_for_process(process(1)).count(), 1);
|
||||
fn region_table_finds_mapping_covering_a_va() {
|
||||
let mut t = RegionTable::new();
|
||||
let id = t.insert(pool_region(1, 2)).unwrap();
|
||||
t.get_mut(id).unwrap().add_mapping(mapping(3, 0x30000));
|
||||
assert_eq!(t.covering(pid(3), va(0x31000)), Some(id));
|
||||
assert_eq!(t.covering(pid(3), va(0x32000)), None);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![no_std]
|
||||
|
||||
pub mod interrupt;
|
||||
mod memory;
|
||||
#[cfg(kani)]
|
||||
pub mod proofs;
|
||||
pub mod syscall;
|
||||
@@ -16,7 +17,7 @@ use domain::{
|
||||
endpoint::EndpointTable,
|
||||
error::KernelError,
|
||||
irq::IrqRoutingTable,
|
||||
memory::{RegionTable, page_pool::PagePool, process_memory::ProcessMemory},
|
||||
memory::{RegionTable, frame::FrameTable, quota::Quota},
|
||||
message::Message,
|
||||
process::{MAX_PROCESSES, ProcessTable},
|
||||
resource::Resource,
|
||||
@@ -26,6 +27,7 @@ use domain::{
|
||||
};
|
||||
use platform::Platform;
|
||||
use types::{
|
||||
address::PhysicalAddress,
|
||||
bounded::{CapSlot, Priority},
|
||||
generation::Generation,
|
||||
id::{ProcessId, ThreadId},
|
||||
@@ -39,8 +41,8 @@ pub struct Kernel<P: Platform> {
|
||||
threads: ThreadTable,
|
||||
endpoints: EndpointTable,
|
||||
scheduler: MultiCoreScheduler,
|
||||
page_pool: PagePool,
|
||||
process_memory: [Option<ProcessMemory>; MAX_PROCESSES],
|
||||
frames: FrameTable,
|
||||
quotas: [Quota; MAX_PROCESSES],
|
||||
regions: RegionTable,
|
||||
irqs: IrqRoutingTable,
|
||||
pending_messages: [Option<Message>; MAX_THREADS],
|
||||
@@ -49,7 +51,7 @@ pub struct Kernel<P: Platform> {
|
||||
}
|
||||
|
||||
impl<P: Platform> Kernel<P> {
|
||||
pub fn new(platform: P, page_pool: PagePool) -> Self {
|
||||
pub fn new(platform: P, pool_base: PhysicalAddress, pool_pages: usize) -> Self {
|
||||
let num_cores = platform.num_cores();
|
||||
Self {
|
||||
platform,
|
||||
@@ -57,8 +59,8 @@ impl<P: Platform> Kernel<P> {
|
||||
threads: ThreadTable::new(),
|
||||
endpoints: EndpointTable::new(),
|
||||
scheduler: MultiCoreScheduler::new(num_cores),
|
||||
page_pool,
|
||||
process_memory: [const { None }; MAX_PROCESSES],
|
||||
frames: FrameTable::new(pool_base, pool_pages),
|
||||
quotas: [Quota::default(); MAX_PROCESSES],
|
||||
regions: RegionTable::new(),
|
||||
irqs: IrqRoutingTable::new(),
|
||||
pending_messages: [const { None }; MAX_THREADS],
|
||||
@@ -67,18 +69,51 @@ impl<P: Platform> Kernel<P> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the kernel directly in `slot`, one table at a time, so the
|
||||
/// whole structure never has to fit on the (small) boot stack.
|
||||
///
|
||||
/// # Safety
|
||||
/// `slot` must be valid for writes and properly aligned. Any previous
|
||||
/// value is overwritten without being dropped.
|
||||
pub unsafe fn init_in_place(
|
||||
slot: *mut Self,
|
||||
platform: P,
|
||||
pool_base: PhysicalAddress,
|
||||
pool_pages: usize,
|
||||
) {
|
||||
use core::ptr::addr_of_mut;
|
||||
let num_cores = platform.num_cores();
|
||||
// SAFETY: the caller guarantees `slot` is valid and aligned; each
|
||||
// field is written exactly once.
|
||||
unsafe {
|
||||
addr_of_mut!((*slot).platform).write(platform);
|
||||
addr_of_mut!((*slot).processes).write(ProcessTable::new());
|
||||
addr_of_mut!((*slot).threads).write(ThreadTable::new());
|
||||
addr_of_mut!((*slot).endpoints).write(EndpointTable::new());
|
||||
addr_of_mut!((*slot).scheduler).write(MultiCoreScheduler::new(num_cores));
|
||||
addr_of_mut!((*slot).frames).write(FrameTable::new(pool_base, pool_pages));
|
||||
addr_of_mut!((*slot).quotas).write([Quota::default(); MAX_PROCESSES]);
|
||||
addr_of_mut!((*slot).regions).write(RegionTable::new());
|
||||
addr_of_mut!((*slot).irqs).write(IrqRoutingTable::new());
|
||||
addr_of_mut!((*slot).pending_messages).write([const { None }; MAX_THREADS]);
|
||||
addr_of_mut!((*slot).pending_results).write([const { None }; MAX_THREADS]);
|
||||
addr_of_mut!((*slot).wait_sets).write([const { None }; MAX_THREADS]);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn boot(
|
||||
&mut self,
|
||||
priority: Priority,
|
||||
time_slice: TimeSlice,
|
||||
) -> Result<(ProcessId, ThreadId), KernelError> {
|
||||
let pid = self.processes.create(CapabilityTable::new(), None)?;
|
||||
if let Err(e) = self.create_address_space(pid) {
|
||||
let _ = self.processes.destroy(pid);
|
||||
return Err(e);
|
||||
}
|
||||
let tid = self.threads.spawn(pid, priority, time_slice, None)?;
|
||||
self.processes.get_mut(pid)?.add_thread(tid)?;
|
||||
|
||||
self.process_memory[pid.raw() as usize] =
|
||||
Some(ProcessMemory::new(pid, ProcessMemory::DEFAULT_PAGE_LIMIT));
|
||||
|
||||
let cap = Capability::new(
|
||||
Resource::ProcessCreate,
|
||||
PermissionSet::empty().with(Permission::Execute),
|
||||
@@ -116,10 +151,6 @@ impl<P: Platform> Kernel<P> {
|
||||
self.threads.get_mut(tid)?.set_running(core)
|
||||
}
|
||||
|
||||
pub fn free_page_count(&self) -> usize {
|
||||
self.page_pool.free_count()
|
||||
}
|
||||
|
||||
pub fn process_exists(&self, pid: ProcessId) -> bool {
|
||||
self.processes.get(pid).is_ok()
|
||||
}
|
||||
@@ -155,11 +186,10 @@ impl<P: Platform> Kernel<P> {
|
||||
|
||||
pub fn create_process_internal(&mut self) -> Option<ProcessId> {
|
||||
let pid = self.processes.create(CapabilityTable::new(), None).ok()?;
|
||||
self.process_memory[pid.raw() as usize] =
|
||||
Some(domain::memory::process_memory::ProcessMemory::new(
|
||||
pid,
|
||||
ProcessMemory::DEFAULT_PAGE_LIMIT,
|
||||
));
|
||||
if self.create_address_space(pid).is_err() {
|
||||
let _ = self.processes.destroy(pid);
|
||||
return None;
|
||||
}
|
||||
Some(pid)
|
||||
}
|
||||
|
||||
@@ -245,12 +275,7 @@ impl<P: Platform> Kernel<P> {
|
||||
|
||||
self.cleanup_threads(&thread_ids);
|
||||
|
||||
// Free allocated pages back to pool
|
||||
if let Some(pm) = &self.process_memory[pid.raw() as usize] {
|
||||
for &(pa, count) in pm.allocations() {
|
||||
self.page_pool.free(pa, count);
|
||||
}
|
||||
}
|
||||
self.teardown_memory(pid);
|
||||
|
||||
// Unblock threads in other processes that were blocked waiting
|
||||
// on endpoints where this process's threads were the other side.
|
||||
@@ -260,7 +285,6 @@ impl<P: Platform> Kernel<P> {
|
||||
// when the endpoint is revoked.
|
||||
|
||||
let _ = self.processes.destroy(pid);
|
||||
self.process_memory[pid.raw() as usize] = None;
|
||||
}
|
||||
|
||||
pub(crate) fn cleanup_threads(&mut self, thread_ids: &[Option<ThreadId>]) {
|
||||
|
||||
422
crates/kernel/src/memory.rs
Normal file
422
crates/kernel/src/memory.rs
Normal file
@@ -0,0 +1,422 @@
|
||||
//! Address-space bookkeeping. The kernel decides every user mapping from
|
||||
//! its own records (`FrameTable`, `RegionTable`, quotas) and only then tells
|
||||
//! the platform to apply it; it never asks the hardware what is mapped.
|
||||
|
||||
use domain::{
|
||||
error::KernelError,
|
||||
memory::{
|
||||
Region, RegionBacking, RegionMapping,
|
||||
frame::{FrameTable, FrameUse, PAGE_SIZE},
|
||||
quota::Quota,
|
||||
},
|
||||
};
|
||||
use platform::{FrameSource, MapError, Platform};
|
||||
use types::{
|
||||
address::{PhysicalAddress, VirtualAddress},
|
||||
bounded::PageCount,
|
||||
id::{ProcessId, RegionId},
|
||||
mapping::PagePerms,
|
||||
};
|
||||
|
||||
use crate::Kernel;
|
||||
|
||||
/// First VA the kernel considers when picking a spot for `RequestPages`.
|
||||
/// Below it sit the program image and boot-time device windows.
|
||||
pub(crate) const USER_HEAP_START: usize = 0x40000;
|
||||
|
||||
/// Page-table frames for one address space, charged to its owner.
|
||||
struct ChargedFrames<'a> {
|
||||
frames: &'a mut FrameTable,
|
||||
quota: &'a mut Quota,
|
||||
owner: ProcessId,
|
||||
}
|
||||
|
||||
impl FrameSource for ChargedFrames<'_> {
|
||||
fn alloc_table_frame(&mut self) -> Option<PhysicalAddress> {
|
||||
if !self.quota.charge(1) {
|
||||
return None;
|
||||
}
|
||||
match self.frames.alloc(FrameUse::PageTable { owner: self.owner }) {
|
||||
Some(pa) => Some(pa),
|
||||
None => {
|
||||
self.quota.uncharge(1);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn map_error(e: MapError) -> KernelError {
|
||||
match e {
|
||||
MapError::OutOfFrames => KernelError::OutOfMemory,
|
||||
MapError::InvalidAddress => KernelError::InvalidAddress,
|
||||
MapError::NoAddressSpace => KernelError::OutOfMemory,
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Platform> Kernel<P> {
|
||||
/// Whether `[va, va + pages)` is page aligned and inside the user window.
|
||||
pub(crate) fn in_user_window(va: VirtualAddress, pages: usize) -> bool {
|
||||
let start = va.raw();
|
||||
let Some(len) = pages.checked_mul(PAGE_SIZE) else {
|
||||
return false;
|
||||
};
|
||||
let Some(end) = start.checked_add(len) else {
|
||||
return false;
|
||||
};
|
||||
pages > 0
|
||||
&& start.is_multiple_of(PAGE_SIZE)
|
||||
&& start >= P::USER_VA_START
|
||||
&& end <= P::USER_VA_END
|
||||
}
|
||||
|
||||
fn is_mapped(&self, pid: ProcessId, va: VirtualAddress) -> bool {
|
||||
self.frames.find_user(pid, va).is_some() || self.regions.covering(pid, va).is_some()
|
||||
}
|
||||
|
||||
fn range_is_free(&self, pid: ProcessId, va: VirtualAddress, pages: usize) -> bool {
|
||||
(0..pages).all(|i| !self.is_mapped(pid, page_at(va, i)))
|
||||
}
|
||||
|
||||
fn quota_mut(&mut self, pid: ProcessId) -> Result<&mut Quota, KernelError> {
|
||||
self.quotas
|
||||
.get_mut(pid.raw() as usize)
|
||||
.ok_or(KernelError::InvalidProcess(pid))
|
||||
}
|
||||
|
||||
pub fn page_usage(&self, pid: ProcessId) -> usize {
|
||||
self.quotas.get(pid.raw() as usize).map_or(0, Quota::used)
|
||||
}
|
||||
|
||||
pub fn set_page_limit(&mut self, pid: ProcessId, limit: usize) -> Result<(), KernelError> {
|
||||
self.processes.get(pid)?;
|
||||
if self.quota_mut(pid)?.set_limit(limit) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(KernelError::OutOfMemory)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn free_page_count(&self) -> usize {
|
||||
self.frames.free_count()
|
||||
}
|
||||
|
||||
/// Maps `pa` at `va` in `pid`, building page tables on `pid`'s quota.
|
||||
fn platform_map(
|
||||
&mut self,
|
||||
pid: ProcessId,
|
||||
va: VirtualAddress,
|
||||
pa: PhysicalAddress,
|
||||
perms: PagePerms,
|
||||
) -> Result<(), KernelError> {
|
||||
let quota = self
|
||||
.quotas
|
||||
.get_mut(pid.raw() as usize)
|
||||
.ok_or(KernelError::InvalidProcess(pid))?;
|
||||
let mut source = ChargedFrames {
|
||||
frames: &mut self.frames,
|
||||
quota,
|
||||
owner: pid,
|
||||
};
|
||||
self.platform
|
||||
.map_page(pid, va, pa, perms, &mut source)
|
||||
.map_err(map_error)
|
||||
}
|
||||
|
||||
pub(crate) fn create_address_space(&mut self, pid: ProcessId) -> Result<(), KernelError> {
|
||||
self.quotas[pid.raw() as usize] = Quota::default();
|
||||
let mut source = ChargedFrames {
|
||||
frames: &mut self.frames,
|
||||
quota: &mut self.quotas[pid.raw() as usize],
|
||||
owner: pid,
|
||||
};
|
||||
let created = self
|
||||
.platform
|
||||
.create_address_space(pid, &mut source)
|
||||
.map_err(map_error);
|
||||
if created.is_err() {
|
||||
self.frames.free_owned_by(pid);
|
||||
self.quotas[pid.raw() as usize] = Quota::default();
|
||||
}
|
||||
created
|
||||
}
|
||||
|
||||
/// Allocates, zeroes and maps one fresh page for `pid` at `va`.
|
||||
fn map_fresh_page(
|
||||
&mut self,
|
||||
pid: ProcessId,
|
||||
va: VirtualAddress,
|
||||
perms: PagePerms,
|
||||
) -> Result<PhysicalAddress, KernelError> {
|
||||
let usage = FrameUse::user(pid, va, perms).ok_or(KernelError::InvalidAddress)?;
|
||||
if !self.quota_mut(pid)?.charge(1) {
|
||||
return Err(KernelError::OutOfMemory);
|
||||
}
|
||||
let Some(pa) = self.frames.alloc(usage) else {
|
||||
self.quota_mut(pid)?.uncharge(1);
|
||||
return Err(KernelError::OutOfMemory);
|
||||
};
|
||||
self.platform.zero_frame(pa);
|
||||
if let Err(e) = self.platform_map(pid, va, pa, perms) {
|
||||
self.frames.free(pa);
|
||||
self.quota_mut(pid)?.uncharge(1);
|
||||
return Err(e);
|
||||
}
|
||||
Ok(pa)
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
self.platform.unmap_page(pid, va);
|
||||
self.frames.free(pa);
|
||||
if let Some(q) = self.quotas.get_mut(pid.raw() as usize) {
|
||||
q.uncharge(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// Maps `pages` fresh zeroed pages at `va`, all or nothing.
|
||||
pub fn map_fresh_range(
|
||||
&mut self,
|
||||
pid: ProcessId,
|
||||
va: VirtualAddress,
|
||||
pages: usize,
|
||||
perms: PagePerms,
|
||||
) -> Result<(), KernelError> {
|
||||
if !Self::in_user_window(va, pages) {
|
||||
return Err(KernelError::InvalidAddress);
|
||||
}
|
||||
if !self.range_is_free(pid, va, pages) {
|
||||
return Err(KernelError::AddressInUse);
|
||||
}
|
||||
for i in 0..pages {
|
||||
if let Err(e) = self.map_fresh_page(pid, page_at(va, i), perms) {
|
||||
for j in 0..i {
|
||||
self.unmap_and_free(pid, page_at(va, j));
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// First-fit search for `pages` free pages in the heap part of the window.
|
||||
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);
|
||||
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))) {
|
||||
None => return Some(va),
|
||||
Some(busy) => candidate += (busy + 1) * PAGE_SIZE,
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Validates that every page of the range is a user page `pid` owns,
|
||||
/// then releases them all.
|
||||
pub(crate) fn release_range(
|
||||
&mut self,
|
||||
pid: ProcessId,
|
||||
va: VirtualAddress,
|
||||
pages: usize,
|
||||
) -> Result<(), KernelError> {
|
||||
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()) {
|
||||
return Err(KernelError::InvalidAddress);
|
||||
}
|
||||
for i in 0..pages {
|
||||
self.unmap_and_free(pid, page_at(va, i));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Moves the page at `src` in `from` to `dst` in `to`. The frame and its
|
||||
/// quota charge change hands; `from` loses access.
|
||||
pub(crate) fn move_page(
|
||||
&mut self,
|
||||
from: ProcessId,
|
||||
src: VirtualAddress,
|
||||
to: ProcessId,
|
||||
dst: VirtualAddress,
|
||||
perms: PagePerms,
|
||||
) -> Result<(), KernelError> {
|
||||
if !Self::in_user_window(src, 1) || !Self::in_user_window(dst, 1) {
|
||||
return Err(KernelError::InvalidAddress);
|
||||
}
|
||||
let (pa, _) = self
|
||||
.frames
|
||||
.find_user(from, src)
|
||||
.ok_or(KernelError::InvalidAddress)?;
|
||||
self.processes.get(to)?;
|
||||
if !self.range_is_free(to, dst, 1) {
|
||||
return Err(KernelError::AddressInUse);
|
||||
}
|
||||
let usage = FrameUse::user(to, dst, perms).ok_or(KernelError::InvalidAddress)?;
|
||||
if !self.quota_mut(to)?.charge(1) {
|
||||
return Err(KernelError::OutOfMemory);
|
||||
}
|
||||
if let Err(e) = self.platform_map(to, dst, pa, perms) {
|
||||
self.quota_mut(to)?.uncharge(1);
|
||||
return Err(e);
|
||||
}
|
||||
self.platform.unmap_page(from, src);
|
||||
self.frames.retag(pa, usage);
|
||||
self.quota_mut(from)?.uncharge(1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Allocates a physically contiguous, zeroed region charged to `owner`.
|
||||
pub fn create_pool_region(
|
||||
&mut self,
|
||||
owner: ProcessId,
|
||||
pages: PageCount,
|
||||
) -> Result<RegionId, KernelError> {
|
||||
self.processes.get(owner)?;
|
||||
let id = self.regions.next_free_id().ok_or(KernelError::NoFreeSlot)?;
|
||||
let count = pages.value() as usize;
|
||||
if !self.quota_mut(owner)?.charge(count) {
|
||||
return Err(KernelError::OutOfMemory);
|
||||
}
|
||||
let Some(base) = self
|
||||
.frames
|
||||
.alloc_contiguous(count, |_| FrameUse::Region { region: id })
|
||||
else {
|
||||
self.quota_mut(owner)?.uncharge(count);
|
||||
return Err(KernelError::OutOfMemory);
|
||||
};
|
||||
for i in 0..count {
|
||||
self.platform.zero_frame(base.offset(i));
|
||||
}
|
||||
self.regions
|
||||
.insert(Region::new(owner, RegionBacking::Pool, base, pages))
|
||||
.ok_or(KernelError::NoFreeSlot)
|
||||
}
|
||||
|
||||
/// Describes device memory at `base` so it can be mapped like a region.
|
||||
pub fn create_device_region(
|
||||
&mut self,
|
||||
owner: ProcessId,
|
||||
base: PhysicalAddress,
|
||||
pages: PageCount,
|
||||
) -> Result<RegionId, KernelError> {
|
||||
self.processes.get(owner)?;
|
||||
if !base.raw().is_multiple_of(PAGE_SIZE) {
|
||||
return Err(KernelError::InvalidAddress);
|
||||
}
|
||||
// Device memory must never alias frames the kernel hands out.
|
||||
let len = pages.value() as usize * PAGE_SIZE;
|
||||
let pool_start = self.frames.base().raw();
|
||||
let pool_end = pool_start + self.frames.capacity() * PAGE_SIZE;
|
||||
if base.raw() < pool_end && base.raw() + len > pool_start {
|
||||
return Err(KernelError::InvalidAddress);
|
||||
}
|
||||
self.regions
|
||||
.insert(Region::new(owner, RegionBacking::Device, base, pages))
|
||||
.ok_or(KernelError::NoFreeSlot)
|
||||
}
|
||||
|
||||
pub fn region_base(&self, id: RegionId) -> Option<PhysicalAddress> {
|
||||
self.regions.get(id).map(Region::base)
|
||||
}
|
||||
|
||||
/// Maps all of region `id` at `va` in `pid`, all or nothing.
|
||||
pub fn map_region(
|
||||
&mut self,
|
||||
id: RegionId,
|
||||
pid: ProcessId,
|
||||
va: VirtualAddress,
|
||||
perms: PagePerms,
|
||||
) -> Result<(), KernelError> {
|
||||
self.processes.get(pid)?;
|
||||
let region = self.regions.get(id).ok_or(KernelError::InvalidRegion)?;
|
||||
let (base, pages) = (region.base(), region.pages().value() as usize);
|
||||
if region.mappings().any(|m| m.process == pid) {
|
||||
return Err(KernelError::AddressInUse);
|
||||
}
|
||||
if !Self::in_user_window(va, pages) {
|
||||
return Err(KernelError::InvalidAddress);
|
||||
}
|
||||
if !self.range_is_free(pid, va, pages) {
|
||||
return Err(KernelError::AddressInUse);
|
||||
}
|
||||
for i in 0..pages {
|
||||
if let Err(e) = self.platform_map(pid, page_at(va, i), base.offset(i), perms) {
|
||||
for j in 0..i {
|
||||
self.platform.unmap_page(pid, page_at(va, j));
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
let recorded = self.regions.get_mut(id).is_some_and(|r| {
|
||||
r.add_mapping(RegionMapping {
|
||||
process: pid,
|
||||
va,
|
||||
perms,
|
||||
})
|
||||
});
|
||||
if !recorded {
|
||||
for i in 0..pages {
|
||||
self.platform.unmap_page(pid, page_at(va, i));
|
||||
}
|
||||
return Err(KernelError::NoFreeSlot);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Frees regions owned by `pid` (unmapping them from every other
|
||||
/// process) and forgets `pid`'s mappings of other regions.
|
||||
fn teardown_regions(&mut self, pid: ProcessId) {
|
||||
let mut owned = [None; domain::memory::MAX_REGIONS];
|
||||
for (slot, id) in owned.iter_mut().zip(self.regions.ids_owned_by(pid)) {
|
||||
*slot = Some(id);
|
||||
}
|
||||
for id in owned.into_iter().flatten() {
|
||||
let Some(region) = self.regions.remove(id) else {
|
||||
continue;
|
||||
};
|
||||
let pages = region.pages().value() as usize;
|
||||
for m in region.mappings().filter(|m| m.process != pid) {
|
||||
for i in 0..pages {
|
||||
self.platform.unmap_page(m.process, page_at(m.va, i));
|
||||
}
|
||||
}
|
||||
if region.backing() == RegionBacking::Pool {
|
||||
for i in 0..pages {
|
||||
self.frames.free(region.base().offset(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (_, region) in self.regions.iter_mut() {
|
||||
region.remove_mapping(pid);
|
||||
}
|
||||
}
|
||||
|
||||
/// Releases everything `pid` holds in memory: its address space, page
|
||||
/// tables, user pages and owned regions.
|
||||
pub(crate) fn teardown_memory(&mut self, pid: ProcessId) {
|
||||
self.teardown_regions(pid);
|
||||
self.platform.destroy_address_space(pid);
|
||||
self.frames.free_owned_by(pid);
|
||||
if let Some(q) = self.quotas.get_mut(pid.raw() as usize) {
|
||||
*q = Quota::default();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn page_at(base: VirtualAddress, i: usize) -> VirtualAddress {
|
||||
VirtualAddress::from_raw(base.raw() + i * PAGE_SIZE)
|
||||
}
|
||||
@@ -38,17 +38,10 @@ impl<P: Platform> Kernel<P> {
|
||||
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
|
||||
};
|
||||
|
||||
if let Err(e) = self.grant_cap(target_pid, resource.clone(), restricted) {
|
||||
if let Err(e) = self.grant_cap(target_pid, resource, restricted) {
|
||||
return SyscallOutcome::Complete(SyscallResult::Error(e));
|
||||
}
|
||||
|
||||
if let domain::resource::Resource::Memory(region_id) = &resource
|
||||
&& let Some(region) = self.regions.get(*region_id)
|
||||
{
|
||||
self.platform
|
||||
.map_shared(target_pid, region.base(), region.size(), restricted);
|
||||
}
|
||||
|
||||
SyscallOutcome::Complete(SyscallResult::Ok)
|
||||
}
|
||||
|
||||
|
||||
@@ -57,11 +57,10 @@ impl<P: Platform> Kernel<P> {
|
||||
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
|
||||
};
|
||||
|
||||
self.process_memory[new_pid.raw() as usize] =
|
||||
Some(domain::memory::process_memory::ProcessMemory::new(
|
||||
new_pid,
|
||||
domain::memory::process_memory::ProcessMemory::DEFAULT_PAGE_LIMIT,
|
||||
));
|
||||
if let Err(e) = self.create_address_space(new_pid) {
|
||||
let _ = self.processes.destroy(new_pid);
|
||||
return SyscallOutcome::Complete(SyscallResult::Error(e));
|
||||
}
|
||||
|
||||
match self.grant_cap(
|
||||
caller_pid,
|
||||
@@ -73,8 +72,8 @@ impl<P: Platform> Kernel<P> {
|
||||
Ok(slot) => SyscallOutcome::Complete(SyscallResult::Slot(slot)),
|
||||
Err(e) => {
|
||||
// Rollback: destroy orphaned process
|
||||
self.teardown_memory(new_pid);
|
||||
let _ = self.processes.destroy(new_pid);
|
||||
self.process_memory[new_pid.raw() as usize] = None;
|
||||
SyscallOutcome::Complete(SyscallResult::Error(e))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,168 +1,78 @@
|
||||
use platform::Platform;
|
||||
use types::address::{PhysicalAddress, VirtualAddress};
|
||||
use types::{
|
||||
address::VirtualAddress, bounded::CapSlot, bounded::PageCount, id::ProcessId,
|
||||
mapping::PagePerms,
|
||||
};
|
||||
|
||||
use super::{SyscallOutcome, SyscallResult};
|
||||
use crate::Kernel;
|
||||
use domain::{error::KernelError, resource::Resource};
|
||||
use types::permission::Permission;
|
||||
|
||||
const USER_VA_LIMIT: usize = 0x80000000;
|
||||
fn complete(result: Result<SyscallResult, KernelError>) -> SyscallOutcome {
|
||||
SyscallOutcome::Complete(result.unwrap_or_else(SyscallResult::Error))
|
||||
}
|
||||
|
||||
impl<P: Platform> Kernel<P> {
|
||||
pub(crate) fn handle_request_pages(
|
||||
&mut self,
|
||||
pid: types::id::ProcessId,
|
||||
count: types::bounded::PageCount,
|
||||
pid: ProcessId,
|
||||
count: PageCount,
|
||||
) -> SyscallOutcome {
|
||||
let pages = count.value() as usize;
|
||||
|
||||
match &mut self.process_memory[pid.raw() as usize] {
|
||||
Some(pm) => {
|
||||
if pm.grant(pages).is_err() {
|
||||
return SyscallOutcome::Complete(SyscallResult::Error(
|
||||
domain::error::KernelError::OutOfMemory,
|
||||
));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return SyscallOutcome::Complete(SyscallResult::Error(
|
||||
domain::error::KernelError::InvalidProcess(pid),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let phys_addr = match self.page_pool.allocate(pages) {
|
||||
Some(a) => a,
|
||||
None => {
|
||||
if let Some(pm) = &mut self.process_memory[pid.raw() as usize] {
|
||||
let _ = pm.release(pages);
|
||||
}
|
||||
return SyscallOutcome::Complete(SyscallResult::Error(
|
||||
domain::error::KernelError::OutOfMemory,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let user_va = match &mut self.process_memory[pid.raw() as usize] {
|
||||
Some(pm) => {
|
||||
let va = pm.alloc_user_va(pages);
|
||||
pm.record_allocation(phys_addr, pages);
|
||||
va
|
||||
}
|
||||
None => {
|
||||
self.page_pool.free(phys_addr, pages);
|
||||
return SyscallOutcome::Complete(SyscallResult::Error(
|
||||
domain::error::KernelError::InvalidProcess(pid),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
for i in 0..pages {
|
||||
let page_va = VirtualAddress::from_raw(user_va.raw() + i * 4096);
|
||||
let page_pa = PhysicalAddress::from_raw(phys_addr.raw() + i * 4096);
|
||||
self.platform
|
||||
.map_user_page(pid, page_va, page_pa, true, false);
|
||||
}
|
||||
|
||||
let region = domain::memory::MemoryRegion::new(
|
||||
types::id::RegionId::from_raw(0),
|
||||
pid,
|
||||
phys_addr,
|
||||
count,
|
||||
types::generation::Generation::initial(),
|
||||
);
|
||||
let _ = self.regions.insert(region);
|
||||
|
||||
SyscallOutcome::Complete(SyscallResult::Value(user_va.raw()))
|
||||
}
|
||||
|
||||
pub(crate) fn handle_map_page(
|
||||
&mut self,
|
||||
caller_pid: types::id::ProcessId,
|
||||
process_cap: types::bounded::CapSlot,
|
||||
src_va: VirtualAddress,
|
||||
dst_va: VirtualAddress,
|
||||
executable: bool,
|
||||
) -> SyscallOutcome {
|
||||
// Prevent mapping kernel memory into userspace
|
||||
if src_va.raw() >= USER_VA_LIMIT {
|
||||
return SyscallOutcome::Complete(SyscallResult::Error(
|
||||
domain::error::KernelError::PermissionDenied {
|
||||
slot: process_cap,
|
||||
needed: types::permission::Permission::Read,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let target_pid = match self.check_cap(
|
||||
caller_pid,
|
||||
process_cap,
|
||||
types::permission::Permission::Execute,
|
||||
) {
|
||||
Ok(cap) => match cap.resource() {
|
||||
domain::resource::Resource::Process(pid) => *pid,
|
||||
_ => {
|
||||
return SyscallOutcome::Complete(SyscallResult::Error(
|
||||
domain::error::KernelError::PermissionDenied {
|
||||
slot: process_cap,
|
||||
needed: types::permission::Permission::Execute,
|
||||
},
|
||||
));
|
||||
}
|
||||
},
|
||||
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
|
||||
};
|
||||
|
||||
let pa = match self.platform.translate_va(caller_pid, src_va) {
|
||||
Some(pa) => pa,
|
||||
None => {
|
||||
return SyscallOutcome::Complete(SyscallResult::Error(
|
||||
domain::error::KernelError::OutOfMemory,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
self.platform
|
||||
.map_user_page(target_pid, dst_va, pa, !executable, executable);
|
||||
|
||||
SyscallOutcome::Complete(SyscallResult::Ok)
|
||||
complete(
|
||||
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()))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn handle_release_pages(
|
||||
&mut self,
|
||||
pid: types::id::ProcessId,
|
||||
pid: ProcessId,
|
||||
addr: VirtualAddress,
|
||||
count: types::bounded::PageCount,
|
||||
count: PageCount,
|
||||
) -> SyscallOutcome {
|
||||
let pages = count.value() as usize;
|
||||
let phys = match self.platform.translate_va(pid, addr) {
|
||||
Some(pa) => pa,
|
||||
None => {
|
||||
return SyscallOutcome::Complete(SyscallResult::Error(
|
||||
domain::error::KernelError::OutOfMemory,
|
||||
));
|
||||
complete(
|
||||
self.release_range(pid, addr, count.value() as usize)
|
||||
.map(|()| SyscallResult::Ok),
|
||||
)
|
||||
}
|
||||
|
||||
/// Moves one of the caller's pages into the process named by
|
||||
/// `process_cap`. Executable pages are mapped read-execute, others
|
||||
/// read-write.
|
||||
pub(crate) fn handle_map_page(
|
||||
&mut self,
|
||||
caller_pid: ProcessId,
|
||||
process_cap: CapSlot,
|
||||
src_va: VirtualAddress,
|
||||
dst_va: VirtualAddress,
|
||||
executable: bool,
|
||||
) -> SyscallOutcome {
|
||||
let target = match self.check_cap(caller_pid, process_cap, Permission::Execute) {
|
||||
Ok(cap) => match cap.resource() {
|
||||
Resource::Process(pid) => *pid,
|
||||
_ => {
|
||||
return complete(Err(KernelError::PermissionDenied {
|
||||
slot: process_cap,
|
||||
needed: Permission::Execute,
|
||||
}));
|
||||
}
|
||||
},
|
||||
Err(e) => return complete(Err(e)),
|
||||
};
|
||||
|
||||
let region_id = match self.regions.find_by_base(pid, phys) {
|
||||
Some((rid, _)) => rid,
|
||||
None => {
|
||||
return SyscallOutcome::Complete(SyscallResult::Error(
|
||||
domain::error::KernelError::OutOfMemory,
|
||||
));
|
||||
}
|
||||
let perms = if executable {
|
||||
PagePerms::ReadExecute
|
||||
} else {
|
||||
PagePerms::ReadWrite
|
||||
};
|
||||
|
||||
if !self.page_pool.free(phys, pages) {
|
||||
return SyscallOutcome::Complete(SyscallResult::Error(
|
||||
domain::error::KernelError::OutOfMemory,
|
||||
));
|
||||
}
|
||||
|
||||
let _ = self.regions.remove(region_id);
|
||||
|
||||
if let Some(pm) = &mut self.process_memory[pid.raw() as usize] {
|
||||
let _ = pm.release(pages);
|
||||
}
|
||||
|
||||
SyscallOutcome::Complete(SyscallResult::Ok)
|
||||
complete(
|
||||
self.move_page(caller_pid, src_va, target, dst_va, perms)
|
||||
.map(|()| SyscallResult::Ok),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -5,4 +5,3 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
types = { workspace = true }
|
||||
domain = { workspace = true }
|
||||
|
||||
@@ -1,16 +1,58 @@
|
||||
#![no_std]
|
||||
|
||||
use domain::memory::MemoryRegion;
|
||||
use types::{
|
||||
address::{EntryPoint, PhysicalAddress, StackPointer, VirtualAddress},
|
||||
bounded::PageCount,
|
||||
id::{CoreId, IrqId, ProcessId},
|
||||
permission::PermissionSet,
|
||||
mapping::PagePerms,
|
||||
time::{Ticks, TimerDuration},
|
||||
};
|
||||
|
||||
/// Supplies frames for page tables while the platform builds an address
|
||||
/// space. The kernel charges each frame to the address space's owner.
|
||||
pub trait FrameSource {
|
||||
fn alloc_table_frame(&mut self) -> Option<PhysicalAddress>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MapError {
|
||||
/// The frame source refused to supply a page-table frame.
|
||||
OutOfFrames,
|
||||
/// The address is outside the user window or not page aligned.
|
||||
InvalidAddress,
|
||||
/// The process has no address space.
|
||||
NoAddressSpace,
|
||||
}
|
||||
|
||||
/// The kernel's contract with an architecture. The kernel decides what is
|
||||
/// mapped where; the platform only applies those decisions to hardware.
|
||||
pub trait Platform {
|
||||
fn configure_protection(&mut self, process: ProcessId, regions: &[MemoryRegion]);
|
||||
/// User mappings live in `[USER_VA_START, USER_VA_END)`. The platform
|
||||
/// guarantees nothing else is mapped in that window in any address
|
||||
/// space, so page tables covering it are private to each process.
|
||||
const USER_VA_START: usize;
|
||||
const USER_VA_END: usize;
|
||||
|
||||
fn create_address_space(
|
||||
&mut self,
|
||||
process: ProcessId,
|
||||
frames: &mut dyn FrameSource,
|
||||
) -> Result<(), MapError>;
|
||||
/// Forgets the address space. The kernel frees its frames afterwards,
|
||||
/// so the platform must stop using them before returning.
|
||||
fn destroy_address_space(&mut self, process: ProcessId);
|
||||
fn map_page(
|
||||
&mut self,
|
||||
process: ProcessId,
|
||||
va: VirtualAddress,
|
||||
pa: PhysicalAddress,
|
||||
perms: PagePerms,
|
||||
frames: &mut dyn FrameSource,
|
||||
) -> Result<(), MapError>;
|
||||
/// Removes the mapping at `va` and flushes it from the TLB.
|
||||
fn unmap_page(&mut self, process: ProcessId, va: VirtualAddress);
|
||||
/// Fills a frame with zeros before it is handed to a process.
|
||||
fn zero_frame(&mut self, pa: PhysicalAddress);
|
||||
|
||||
fn enable_user_mode(&mut self, entry: EntryPoint, stack: StackPointer) -> !;
|
||||
fn set_timer(&mut self, duration: TimerDuration);
|
||||
fn current_ticks(&self) -> Ticks;
|
||||
@@ -19,22 +61,6 @@ pub trait Platform {
|
||||
fn send_ipi(&mut self, target: CoreId);
|
||||
fn core_id(&self) -> CoreId;
|
||||
fn num_cores(&self) -> u8;
|
||||
fn map_shared(
|
||||
&mut self,
|
||||
process: ProcessId,
|
||||
base: PhysicalAddress,
|
||||
size: PageCount,
|
||||
permissions: PermissionSet,
|
||||
);
|
||||
fn halt(&self) -> !;
|
||||
fn debug_putchar(&self, byte: u8);
|
||||
fn translate_va(&self, process: ProcessId, va: VirtualAddress) -> Option<PhysicalAddress>;
|
||||
fn map_user_page(
|
||||
&mut self,
|
||||
process: ProcessId,
|
||||
va: VirtualAddress,
|
||||
pa: PhysicalAddress,
|
||||
writable: bool,
|
||||
executable: bool,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,3 +6,22 @@ pub struct Mapping<F> {
|
||||
pub pages: usize,
|
||||
pub flags: F,
|
||||
}
|
||||
|
||||
/// Access rights of a user page. Writable-and-executable has no variant,
|
||||
/// so W^X holds for every mapping the kernel can express.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PagePerms {
|
||||
ReadOnly,
|
||||
ReadWrite,
|
||||
ReadExecute,
|
||||
}
|
||||
|
||||
impl PagePerms {
|
||||
pub const fn writable(self) -> bool {
|
||||
matches!(self, Self::ReadWrite)
|
||||
}
|
||||
|
||||
pub const fn executable(self) -> bool {
|
||||
matches!(self, Self::ReadExecute)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user