From 8d2c40cb1e4175d90681c1a45c888e5593097be7 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Mon, 26 Oct 2026 19:40:50 +0100 Subject: [PATCH] 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 --- Cargo.lock | 1 - Cargo.toml | 7 + Makefile | 1 + crates/adapter-riscv/src/boot/hardware.rs | 21 +- crates/adapter-riscv/src/boot/loader.rs | 191 ++++---- crates/adapter-riscv/src/boot_fs.rs | 9 +- crates/adapter-riscv/src/devices/gpu_setup.rs | 77 +--- .../adapter-riscv/src/devices/input_setup.rs | 61 +-- crates/adapter-riscv/src/devices/mod.rs | 81 ++++ .../src/devices/storage_setup.rs | 118 +---- crates/adapter-riscv/src/hw/virtio_blk.rs | 18 +- crates/adapter-riscv/src/kernel_cell.rs | 9 +- crates/adapter-riscv/src/main.rs | 11 +- crates/adapter-riscv/src/mmu/allocator.rs | 15 +- crates/adapter-riscv/src/mmu/mod.rs | 4 +- crates/adapter-riscv/src/mmu/page_table.rs | 185 +++++--- crates/adapter-riscv/src/platform.rs | 152 +++---- crates/adapter-riscv/src/trap/mod.rs | 40 +- crates/domain/src/error.rs | 10 +- crates/domain/src/memory/frame/mod.rs | 217 +++++++++ crates/domain/src/memory/frame/proofs.rs | 195 ++++++++ crates/domain/src/memory/frame/tests.rs | 188 ++++++++ crates/domain/src/memory/mod.rs | 176 +++++--- crates/domain/src/memory/page_pool.rs | 66 --- crates/domain/src/memory/process_memory.rs | 77 ---- crates/domain/src/memory/quota.rs | 57 +++ crates/domain/src/memory/quota_tests.rs | 37 ++ crates/domain/src/memory/tests.rs | 262 ++++------- crates/kernel/src/lib.rs | 74 +-- crates/kernel/src/memory.rs | 422 ++++++++++++++++++ crates/kernel/src/syscall/capability.rs | 9 +- crates/kernel/src/syscall/lifecycle.rs | 11 +- crates/kernel/src/syscall/memory.rs | 204 +++------ crates/kernel/src/tests/helpers.rs | 4 +- crates/kernel/src/tests/invariants.rs | 261 +++++++++++ crates/kernel/src/tests/isolation.rs | 391 ++++++++++++++++ crates/kernel/src/tests/memory.rs | 21 +- crates/kernel/src/tests/mock_platform.rs | 212 +++++++-- crates/kernel/src/tests/mod.rs | 3 +- crates/kernel/src/tests/tick.rs | 3 +- crates/platform/Cargo.toml | 1 - crates/platform/src/lib.rs | 66 ++- crates/types/src/mapping.rs | 19 + 43 files changed, 2815 insertions(+), 1172 deletions(-) create mode 100644 crates/domain/src/memory/frame/mod.rs create mode 100644 crates/domain/src/memory/frame/proofs.rs create mode 100644 crates/domain/src/memory/frame/tests.rs delete mode 100644 crates/domain/src/memory/page_pool.rs delete mode 100644 crates/domain/src/memory/process_memory.rs create mode 100644 crates/domain/src/memory/quota.rs create mode 100644 crates/domain/src/memory/quota_tests.rs create mode 100644 crates/kernel/src/memory.rs create mode 100644 crates/kernel/src/tests/invariants.rs create mode 100644 crates/kernel/src/tests/isolation.rs diff --git a/Cargo.lock b/Cargo.lock index f3f0caf..514fbfa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -245,7 +245,6 @@ dependencies = [ name = "platform" version = "0.1.0" dependencies = [ - "domain", "types", ] diff --git a/Cargo.toml b/Cargo.toml index fdbac26..8b6d481 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/Makefile b/Makefile index fb2cf7e..3ee4d56 100644 --- a/Makefile +++ b/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) diff --git a/crates/adapter-riscv/src/boot/hardware.rs b/crates/adapter-riscv/src/boot/hardware.rs index f1d38d4..25824a9 100644 --- a/crates/adapter-riscv/src/boot/hardware.rs +++ b/crates/adapter-riscv/src/boot/hardware.rs @@ -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( - types::address::PhysicalAddress::from_raw(board::USER_POOL_BASE), - board::USER_POOL_PAGES, - ); - KERNEL.init(kernel::Kernel::new(riscv_platform, pool)); + // 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_pt } diff --git a/crates/adapter-riscv/src/boot/loader.rs b/crates/adapter-riscv/src/boot/loader.rs index fd28ece..5b3fa17 100644 --- a/crates/adapter-riscv/src/boot/loader.rs +++ b/crates/adapter-riscv/src/boot/loader.rs @@ -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 { 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(); +/// 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 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( + data.as_ptr().add(done), + (pa.raw() as *mut u8).add(offset), + len, + ); + } + done += len; } - let buf = unsafe { core::slice::from_raw_parts_mut(buf_base as *mut u8, MAX_FILE_SIZE) }; + true +} +fn segment_perms(flags: elf::SegmentFlags) -> Option { + match (flags.write, flags.execute) { + (true, true) => None, + (false, true) => Some(PagePerms::ReadExecute), + (true, false) => Some(PagePerms::ReadWrite), + (false, false) => Some(PagePerms::ReadOnly), + } +} + +/// 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 { - return 0; + crate::halt(uart, "ELF not found"); } - - let elf_info = match elf::parse(&buf[..file_size]) { - Ok(info) => info, - Err(_) => return 0, - }; - - 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), - ); + 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 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; + 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; + } - 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 - }; + kern() + .map_fresh_range(pid, VirtualAddress::from_raw(page_start), pages, perms) + .unwrap_or_else(|_| crate::halt(uart, "cannot map segment")); - if copy_len > 0 { - let src = segment.file_offset + seg_file_start; - if src + copy_len <= file_size { - unsafe { - core::ptr::copy_nonoverlapping( - buf.as_ptr().add(src), - (page as *mut u8).add(dest_off), - copy_len, - ); - } - } - } - - 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, - }, - ); + 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"); } } - 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(), - }, - ); - } - + let stack_bottom = USER_VA_BASE - USER_STACK_PAGES * PAGE_SIZE; kern() - .platform_mut() - .set_page_table(pid, user_pt as *const mmu::PageTable as usize); + .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 } diff --git a/crates/adapter-riscv/src/boot_fs.rs b/crates/adapter-riscv/src/boot_fs.rs index 52f3e68..0519898 100644 --- a/crates/adapter-riscv/src/boot_fs.rs +++ b/crates/adapter-riscv/src/boot_fs.rs @@ -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; diff --git a/crates/adapter-riscv/src/devices/gpu_setup.rs b/crates/adapter-riscv/src/devices/gpu_setup.rs index f33d8f3..a54af1a 100644 --- a/crates/adapter-riscv/src/devices/gpu_setup.rs +++ b/crates/adapter-riscv/src/devices/gpu_setup.rs @@ -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 { - 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 -} diff --git a/crates/adapter-riscv/src/devices/input_setup.rs b/crates/adapter-riscv/src/devices/input_setup.rs index 9b6c0ff..1ff0b80 100644 --- a/crates/adapter-riscv/src/devices/input_setup.rs +++ b/crates/adapter-riscv/src/devices/input_setup.rs @@ -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 { - 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 -} diff --git a/crates/adapter-riscv/src/devices/mod.rs b/crates/adapter-riscv/src/devices/mod.rs index 2d23177..10507fb 100644 --- a/crates/adapter-riscv/src/devices/mod.rs +++ b/crates/adapter-riscv/src/devices/mod.rs @@ -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 { + 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) }; + } +} diff --git a/crates/adapter-riscv/src/devices/storage_setup.rs b/crates/adapter-riscv/src/devices/storage_setup.rs index 325afed..a1a7892 100644 --- a/crates/adapter-riscv/src/devices/storage_setup.rs +++ b/crates/adapter-riscv/src/devices/storage_setup.rs @@ -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 { - 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(), - }, - ); - } -} diff --git a/crates/adapter-riscv/src/hw/virtio_blk.rs b/crates/adapter-riscv/src/hw/virtio_blk.rs index 25e533e..47fb3cb 100644 --- a/crates/adapter-riscv/src/hw/virtio_blk.rs +++ b/crates/adapter-riscv/src/hw/virtio_blk.rs @@ -29,20 +29,18 @@ impl VirtioBlk { pub fn probe() -> Option { 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 { - 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), - } + 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 }) diff --git a/crates/adapter-riscv/src/kernel_cell.rs b/crates/adapter-riscv/src/kernel_cell.rs index fcfa3e9..8884e23 100644 --- a/crates/adapter-riscv/src/kernel_cell.rs +++ b/crates/adapter-riscv/src/kernel_cell.rs @@ -18,8 +18,13 @@ impl KernelCell { } } - 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); } diff --git a/crates/adapter-riscv/src/main.rs b/crates/adapter-riscv/src/main.rs index 9b2cfd9..c6c7227 100644 --- a/crates/adapter-riscv/src/main.rs +++ b/crates/adapter-riscv/src/main.rs @@ -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); diff --git a/crates/adapter-riscv/src/mmu/allocator.rs b/crates/adapter-riscv/src/mmu/allocator.rs index 978042f..8622e47 100644 --- a/crates/adapter-riscv/src/mmu/allocator.rs +++ b/crates/adapter-riscv/src/mmu/allocator.rs @@ -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) } } diff --git a/crates/adapter-riscv/src/mmu/mod.rs b/crates/adapter-riscv/src/mmu/mod.rs index f3b233b..ba983c4 100644 --- a/crates/adapter-riscv/src/mmu/mod.rs +++ b/crates/adapter-riscv/src/mmu/mod.rs @@ -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, }; diff --git a/crates/adapter-riscv/src/mmu/page_table.rs b/crates/adapter-riscv/src/mmu/page_table.rs index dbdd21b..6862a84 100644 --- a/crates/adapter-riscv/src/mmu/page_table.rs +++ b/crates/adapter-riscv/src/mmu/page_table.rs @@ -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 { - 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; + +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 { + 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 { + 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; } } - - root + 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 } diff --git a/crates/adapter-riscv/src/platform.rs b/crates/adapter-riscv/src/platform.rs index 175292d..1357b42 100644 --- a/crates/adapter-riscv/src/platform.rs +++ b/crates/adapter-riscv/src/platform.rs @@ -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 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; + fn root(&self, pid: ProcessId) -> Option { + match self.page_tables.get(pid.raw() as usize) { + Some(&0) | None => None, + Some(&root) => Some(root), } - 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 { - 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!( diff --git a/crates/adapter-riscv/src/trap/mod.rs b/crates/adapter-riscv/src/trap/mod.rs index 0ac8bb3..c206c0c 100644 --- a/crates/adapter-riscv/src/trap/mod.rs +++ b/crates/adapter-riscv/src/trap/mod.rs @@ -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,13 +347,13 @@ fn switch_to_thread( apply_pending_result(frame, next_tid); - if next_pid != current_pid { - if 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); - } + 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); } } } @@ -388,13 +378,13 @@ 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 { - 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); - } + 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 { diff --git a/crates/domain/src/error.rs b/crates/domain/src/error.rs index 4080f38..f5bb87f 100644 --- a/crates/domain/src/error.rs +++ b/crates/domain/src/error.rs @@ -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, } diff --git a/crates/domain/src/memory/frame/mod.rs b/crates/domain/src/memory/frame/mod.rs new file mode 100644 index 0000000..6d8b575 --- /dev/null +++ b/crates/domain/src/memory/frame/mod.rs @@ -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 { + 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 { + base: PhysicalAddress, + total: usize, + frames: [Option; N], + free: usize, +} + +impl FrameTable { + 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 { + 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 { + 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 { + 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 { + 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 + '_ { + 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 + '_ { + 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 + } +} diff --git a/crates/domain/src/memory/frame/proofs.rs b/crates/domain/src/memory/frame/proofs.rs new file mode 100644 index 0000000..5fb0bb9 --- /dev/null +++ b/crates/domain/src/memory/frame/proofs.rs @@ -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::() % 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::() % 3); + match kani::any::() % 3 { + 0 => FrameUse::User { + owner, + page: kani::any::() % 4, + perms: any_perms(), + }, + 1 => FrameUse::PageTable { owner }, + _ => FrameUse::Region { + region: RegionId::from_raw(kani::any::() % 2), + }, + } +} + +fn any_slot() -> Option { + if kani::any() { Some(any_use()) } else { None } +} + +fn any_table() -> FrameTable { + 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) -> 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::() % ((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::() % 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::() % 3); + let va = VirtualAddress::from_raw(kani::any::() % (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::() % (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)); +} diff --git a/crates/domain/src/memory/frame/tests.rs b/crates/domain/src/memory/frame/tests.rs new file mode 100644 index 0000000..39180b8 --- /dev/null +++ b/crates/domain/src/memory/frame/tests.rs @@ -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]); +} diff --git a/crates/domain/src/memory/mod.rs b/crates/domain/src/memory/mod.rs index 6702fa9..a8ed5ce 100644 --- a/crates/domain/src/memory/mod.rs +++ b/crates/domain/src/memory/mod.rs @@ -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 { +#[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, + pages: PageCount, + generation: Generation, + mappings: [Option; MAX_REGION_MAPPINGS], +} + +impl Region { pub fn new( - id: RegionId, owner: ProcessId, + backing: RegionBacking, base: PhysicalAddress, - size: PageCount, - generation: Generation, + 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 { + 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 { + 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; MAX_REGIONS], + regions: [Option; MAX_REGIONS], } impl RegionTable { @@ -75,50 +133,60 @@ impl RegionTable { } } - pub fn insert(&mut self, region: MemoryRegion) -> Option { + /// The id the next `insert` will return. + pub fn next_free_id(&self) -> Option { + 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 { 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 { - self.regions - .get_mut(id.raw() as usize) - .and_then(|s| s.take()) + pub fn remove(&mut self, id: RegionId) -> Option { + 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 + '_ { + 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 { + pub fn iter(&self) -> impl Iterator { 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 { + 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 { + self.regions.iter().enumerate().find_map(|(i, r)| { + r.as_ref() + .filter(|r| r.covers(process, va)) + .map(|_| RegionId::from_raw(i as u16)) + }) } } diff --git a/crates/domain/src/memory/page_pool.rs b/crates/domain/src/memory/page_pool.rs deleted file mode 100644 index 98a0c5b..0000000 --- a/crates/domain/src/memory/page_pool.rs +++ /dev/null @@ -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 { - 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 { - 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 - } -} diff --git a/crates/domain/src/memory/process_memory.rs b/crates/domain/src/memory/process_memory.rs deleted file mode 100644 index b458b41..0000000 --- a/crates/domain/src/memory/process_memory.rs +++ /dev/null @@ -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(()) - } -} diff --git a/crates/domain/src/memory/quota.rs b/crates/domain/src/memory/quota.rs new file mode 100644 index 0000000..b8d1238 --- /dev/null +++ b/crates/domain/src/memory/quota.rs @@ -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) + } +} diff --git a/crates/domain/src/memory/quota_tests.rs b/crates/domain/src/memory/quota_tests.rs new file mode 100644 index 0000000..4c622ba --- /dev/null +++ b/crates/domain/src/memory/quota_tests.rs @@ -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)); +} diff --git a/crates/domain/src/memory/tests.rs b/crates/domain/src/memory/tests.rs index b4bd2ee..b2855fa 100644 --- a/crates/domain/src/memory/tests.rs +++ b/crates/domain/src/memory/tests.rs @@ -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); } diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index a515529..7554483 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -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 { threads: ThreadTable, endpoints: EndpointTable, scheduler: MultiCoreScheduler, - page_pool: PagePool, - process_memory: [Option; MAX_PROCESSES], + frames: FrameTable, + quotas: [Quota; MAX_PROCESSES], regions: RegionTable, irqs: IrqRoutingTable, pending_messages: [Option; MAX_THREADS], @@ -49,7 +51,7 @@ pub struct Kernel { } impl Kernel

{ - 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 Kernel

{ 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 Kernel

{ } } + /// 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 Kernel

{ 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 Kernel

{ pub fn create_process_internal(&mut self) -> Option { 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 Kernel

{ 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 Kernel

{ // 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]) { diff --git a/crates/kernel/src/memory.rs b/crates/kernel/src/memory.rs new file mode 100644 index 0000000..b665ca6 --- /dev/null +++ b/crates/kernel/src/memory.rs @@ -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 { + 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 Kernel

{ + /// 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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) +} diff --git a/crates/kernel/src/syscall/capability.rs b/crates/kernel/src/syscall/capability.rs index fad7e3e..9cb2a3e 100644 --- a/crates/kernel/src/syscall/capability.rs +++ b/crates/kernel/src/syscall/capability.rs @@ -38,17 +38,10 @@ impl Kernel

{ 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) } diff --git a/crates/kernel/src/syscall/lifecycle.rs b/crates/kernel/src/syscall/lifecycle.rs index 4ed15ee..6234a43 100644 --- a/crates/kernel/src/syscall/lifecycle.rs +++ b/crates/kernel/src/syscall/lifecycle.rs @@ -57,11 +57,10 @@ impl Kernel

{ 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 Kernel

{ 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)) } } diff --git a/crates/kernel/src/syscall/memory.rs b/crates/kernel/src/syscall/memory.rs index 5b6f7bf..0fb8528 100644 --- a/crates/kernel/src/syscall/memory.rs +++ b/crates/kernel/src/syscall/memory.rs @@ -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) -> SyscallOutcome { + SyscallOutcome::Complete(result.unwrap_or_else(SyscallResult::Error)) +} impl Kernel

{ 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), + ) } } diff --git a/crates/kernel/src/tests/helpers.rs b/crates/kernel/src/tests/helpers.rs index 2704902..e23e6ae 100644 --- a/crates/kernel/src/tests/helpers.rs +++ b/crates/kernel/src/tests/helpers.rs @@ -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 { 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, ProcessId) { diff --git a/crates/kernel/src/tests/invariants.rs b/crates/kernel/src/tests/invariants.rs new file mode 100644 index 0000000..3620494 --- /dev/null +++ b/crates/kernel/src/tests/invariants.rs @@ -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, + 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(); + } + } +} diff --git a/crates/kernel/src/tests/isolation.rs b/crates/kernel/src/tests/isolation.rs new file mode 100644 index 0000000..f432f1c --- /dev/null +++ b/crates/kernel/src/tests/isolation.rs @@ -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, n: u16) -> Option { + match k.handle_syscall(Syscall::RequestPages { count: pages(n) }) { + SyscallOutcome::Complete(SyscallResult::Value(va)) => Some(va), + _ => None, + } +} + +fn release(k: &mut Kernel, 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, 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, + 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); +} diff --git a/crates/kernel/src/tests/memory.rs b/crates/kernel/src/tests/memory.rs index 5e74f6b..6c1a6b5 100644 --- a/crates/kernel/src/tests/memory.rs +++ b/crates/kernel/src/tests/memory.rs @@ -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); } diff --git a/crates/kernel/src/tests/mock_platform.rs b/crates/kernel/src/tests/mock_platform.rs index a357675..c12e072 100644 --- a/crates/kernel/src/tests/mock_platform.rs +++ b/crates/kernel/src/tests/mock_platform.rs @@ -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; MAX_MAPPINGS], + tables: [Option; MAX_TABLES], + zeroed: [Option; 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 { + 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 + '_ { + self.mappings + .iter() + .flatten() + .map(|m| (m.process, m.va, m.pa)) + } + + pub fn perms(&self, process: ProcessId, va: usize) -> Option { + 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 { - 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; - } - } - } } diff --git a/crates/kernel/src/tests/mod.rs b/crates/kernel/src/tests/mod.rs index c477364..f2323b4 100644 --- a/crates/kernel/src/tests/mod.rs +++ b/crates/kernel/src/tests/mod.rs @@ -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; diff --git a/crates/kernel/src/tests/tick.rs b/crates/kernel/src/tests/tick.rs index 407faa0..aff9720 100644 --- a/crates/kernel/src/tests/tick.rs +++ b/crates/kernel/src/tests/tick.rs @@ -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(); diff --git a/crates/platform/Cargo.toml b/crates/platform/Cargo.toml index 8176397..783eff6 100644 --- a/crates/platform/Cargo.toml +++ b/crates/platform/Cargo.toml @@ -5,4 +5,3 @@ edition = "2024" [dependencies] types = { workspace = true } -domain = { workspace = true } diff --git a/crates/platform/src/lib.rs b/crates/platform/src/lib.rs index 7111d41..5c897d5 100644 --- a/crates/platform/src/lib.rs +++ b/crates/platform/src/lib.rs @@ -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; +} + +#[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; - fn map_user_page( - &mut self, - process: ProcessId, - va: VirtualAddress, - pa: PhysicalAddress, - writable: bool, - executable: bool, - ); } diff --git a/crates/types/src/mapping.rs b/crates/types/src/mapping.rs index e3adf70..2764df0 100644 --- a/crates/types/src/mapping.rs +++ b/crates/types/src/mapping.rs @@ -6,3 +6,22 @@ pub struct Mapping { 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) + } +}