From bafc18a5f87a777f94eb1bb6b96b3a85dda83081 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Mon, 26 Oct 2026 23:59:57 +0100 Subject: [PATCH] feat(kernel): SystemControl cap for halt, board device table + boot caps to init, RegionAddress syscall, idle loop services irqs --- crates/adapter-riscv/src/board.rs | 26 ++ crates/adapter-riscv/src/devices/mod.rs | 15 +- crates/adapter-riscv/src/main.rs | 14 + crates/adapter-riscv/src/trap/mod.rs | 84 +++--- crates/domain/src/irq/mod.rs | 5 + crates/domain/src/resource.rs | 2 + crates/domain/src/syscall.rs | 10 +- crates/kernel/src/abi.rs | 7 +- crates/kernel/src/board.rs | 79 ++++++ crates/kernel/src/lib.rs | 6 +- crates/kernel/src/memory.rs | 18 +- crates/kernel/src/syscall/memory.rs | 19 +- crates/kernel/src/syscall/mod.rs | 6 +- crates/kernel/src/syscall/simple.rs | 15 +- crates/kernel/src/tests/abi.rs | 3 +- crates/kernel/src/tests/board.rs | 323 ++++++++++++++++++++++++ crates/kernel/src/tests/mod.rs | 1 + crates/kernel/src/tests/storm.rs | 13 +- crates/libkos/src/syscall.rs | 12 +- crates/types/src/syscall_abi.rs | 1 + docs/ipc.md | 20 ++ 21 files changed, 626 insertions(+), 53 deletions(-) create mode 100644 crates/kernel/src/board.rs create mode 100644 crates/kernel/src/tests/board.rs diff --git a/crates/adapter-riscv/src/board.rs b/crates/adapter-riscv/src/board.rs index 343ff71..b6146c4 100644 --- a/crates/adapter-riscv/src/board.rs +++ b/crates/adapter-riscv/src/board.rs @@ -8,3 +8,29 @@ pub const CLINT_BASE: usize = 0x02000000; pub const USER_POOL_BASE: usize = 0x84000000; pub const USER_POOL_PAGES: usize = 4096; pub const TICK_INTERVAL: u64 = 10_000_000; + +/// QEMU virt's PLIC lines. +const UART_IRQ: u16 = 10; +const VIRTIO_IRQ_BASE: u16 = 1; +const VIRTIO_SLOT_SIZE: usize = 0x1000; +const VIRTIO_SLOTS: usize = VIRTIO_SIZE / VIRTIO_SLOT_SIZE; + +/// Every device userspace may drive: the UART and each VirtIO MMIO slot. +/// The PLIC, CLINT and RAM stay the kernel's. +pub fn devices() -> Option<[kernel::BoardDevice; 1 + VIRTIO_SLOTS]> { + use types::{address::PhysicalAddress, bounded::PageCount, id::IrqId}; + let one = PageCount::new(1)?; + let device = |base: usize, irq: u16| kernel::BoardDevice { + base: PhysicalAddress::from_raw(base), + pages: one, + irq: Some(IrqId::from_raw(irq)), + }; + let mut all = [device(UART_BASE, UART_IRQ); 1 + VIRTIO_SLOTS]; + for (i, d) in all.iter_mut().skip(1).enumerate() { + *d = device( + VIRTIO_BASE + i * VIRTIO_SLOT_SIZE, + VIRTIO_IRQ_BASE + i as u16, + ); + } + Some(all) +} diff --git a/crates/adapter-riscv/src/devices/mod.rs b/crates/adapter-riscv/src/devices/mod.rs index 10507fb..06d7701 100644 --- a/crates/adapter-riscv/src/devices/mod.rs +++ b/crates/adapter-riscv/src/devices/mod.rs @@ -10,7 +10,9 @@ 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. +// Device regions come from the board table and belong to init, which +// also holds capabilities to them; moving drivers onto those caps is +// userspace work. fn kern() -> &'static mut kernel::Kernel { unsafe { &mut *crate::kernel_ptr() } @@ -23,11 +25,16 @@ fn pages(uart: &Uart, n: usize) -> PageCount { .unwrap_or_else(|| crate::halt(uart, "bad page count")) } -/// Maps device MMIO at `pa` into `pid` at `va`. +/// Maps the board device region at `pa` into `pid` at `va`. The region +/// belongs to init (see `kernel::Kernel::register_board`); `n` must match +/// its size. 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")); + .device_region_at(PhysicalAddress::from_raw(pa)) + .unwrap_or_else(|| crate::halt(uart, "no such board device")); + if kern().region_pages(id).map(|p| p.value() as usize) != Some(n) { + crate::halt(uart, "device size mismatch"); + } kern() .map_region(id, pid, VirtualAddress::from_raw(va), PagePerms::ReadWrite) .unwrap_or_else(|_| crate::halt(uart, "device map failed")); diff --git a/crates/adapter-riscv/src/main.rs b/crates/adapter-riscv/src/main.rs index c6c7227..706adfd 100644 --- a/crates/adapter-riscv/src/main.rs +++ b/crates/adapter-riscv/src/main.rs @@ -17,6 +17,12 @@ use kernel_cell::KernelCell; static KERNEL: KernelCell> = KernelCell::uninit(); +fn kern_mut() -> &'static mut kernel::Kernel { + // SAFETY: single core; boot code runs before any trap can touch the + // kernel. + unsafe { &mut *kernel_ptr() } +} + pub fn kernel_ptr() -> *mut kernel::Kernel { KERNEL.as_ptr() } @@ -51,10 +57,18 @@ extern "C" fn kmain() -> ! { let input = boot::loader::load_server(&uart, &mut blk, b"input.elf", "input"); let virtio_base = blk.base(); + let board = board::devices().unwrap_or_else(|| halt(&uart, "bad board table")); + kern_mut() + .register_board(init.pid, &board) + .unwrap_or_else(|_| halt(&uart, "board registration failed")); devices::storage_setup::configure(&uart, init.pid, storage.pid, virtio_base); devices::gpu_setup::configure(&uart, display.pid); devices::input_setup::configure(&uart, input.pid); boot::caps::grant_all(&uart, init.pid, storage.pid, display.pid, input.pid); + // After grant_all, so the slots init already relies on stay put. + kern_mut() + .grant_boot_authority(init.pid, &board) + .unwrap_or_else(|_| halt(&uart, "boot authority failed")); uart.write_str("k-os: entering userspace\n"); boot::loader::enter_userspace(init.pid, init.entry, SHARED_BUF_VA); diff --git a/crates/adapter-riscv/src/trap/mod.rs b/crates/adapter-riscv/src/trap/mod.rs index 3bce71a..053cbf2 100644 --- a/crates/adapter-riscv/src/trap/mod.rs +++ b/crates/adapter-riscv/src/trap/mod.rs @@ -218,32 +218,66 @@ fn switch_to_next(frame: &mut TrapFrame, current_tid: types::id::ThreadId) { save_frame_to_context(frame, ctx); } - if let Some(next_tid) = kern.pick_next_thread() { - let next_pid = kern.thread_process(next_tid); - let current_pid = kern.thread_process(current_tid); + let next_tid = next_runnable(); + let kern = unsafe { &mut *crate::kernel_ptr() }; + let next_pid = kern.thread_process(next_tid); + let current_pid = kern.thread_process(current_tid); - let core = types::id::CoreId::from_raw(0); - let _ = kern.set_thread_running(next_tid, core); + let core = types::id::CoreId::from_raw(0); + let _ = kern.set_thread_running(next_tid, core); - if let Some(ctx) = kern.thread_context(next_tid) { - load_context_to_frame(ctx, frame); + if let Some(ctx) = kern.thread_context(next_tid) { + load_context_to_frame(ctx, frame); + } + + apply_pending_result(frame, next_tid); + + 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); } + } +} - apply_pending_result(frame, next_tid); +/// The next thread to run, idling until an interrupt makes one ready. +/// +/// We are inside a trap, so SIE is clear and interrupts are not taken. +/// `wfi` still wakes once one is pending, and it is then serviced here by +/// polling `sip`, which is how a driver blocked on its IRQ notification +/// gets woken when nothing else can run. +fn next_runnable() -> types::id::ThreadId { + loop { + let kern = unsafe { &mut *crate::kernel_ptr() }; + if let Some(tid) = kern.pick_next_thread() { + return tid; + } + unsafe { core::arch::asm!("wfi") }; + let sip: usize; + unsafe { core::arch::asm!("csrr {0}, sip", out(reg) sip) }; + if sip & (1 << CAUSE_S_TIMER) != 0 { + // No thread is running, so there is no time slice to charge. + crate::hw::timer::handle_interrupt(); + } + if sip & (1 << CAUSE_S_EXTERNAL) != 0 { + dispatch_external(); + } + } +} - if next_pid != current_pid - && let Some(pid) = next_pid - { - let pt_addr = kern.platform_mut().page_table_for(pid); - if pt_addr != 0 { - let pt = unsafe { &*(pt_addr as *const crate::mmu::PageTable) }; - crate::mmu::activate(pt); - } - } - } else { - loop { - unsafe { core::arch::asm!("wfi") }; +/// Claims every pending PLIC interrupt and hands each to the kernel. +fn dispatch_external() { + loop { + let irq_num = crate::hw::plic::Plic::claim(); + if irq_num == 0 { + return; } + let kern = unsafe { &mut *crate::kernel_ptr() }; + kern.handle_interrupt(types::id::IrqId::from_raw(irq_num as u16)); + crate::hw::plic::Plic::complete(irq_num); } } @@ -272,15 +306,7 @@ fn handle_interrupt(frame: &mut TrapFrame, cause: usize) { } } } - CAUSE_S_EXTERNAL => { - let irq_num = crate::hw::plic::Plic::claim(); - if irq_num != 0 { - let kern = unsafe { &mut *crate::kernel_ptr() }; - let irq = types::id::IrqId::from_raw(irq_num as u16); - kern.handle_interrupt(irq); - crate::hw::plic::Plic::complete(irq_num); - } - } + CAUSE_S_EXTERNAL => dispatch_external(), _ => {} } } diff --git a/crates/domain/src/irq/mod.rs b/crates/domain/src/irq/mod.rs index 279c670..9a23f69 100644 --- a/crates/domain/src/irq/mod.rs +++ b/crates/domain/src/irq/mod.rs @@ -30,6 +30,11 @@ impl IrqRoutingTable { } } + /// Whether `irq` is a line this table can route. + pub fn is_valid(&self, irq: IrqId) -> bool { + (irq.raw() as usize) < MAX_IRQS + } + /// Routes `irq`, replacing any previous route. Fails for lines beyond /// the table. pub fn set(&mut self, irq: IrqId, route: IrqRoute) -> bool { diff --git a/crates/domain/src/resource.rs b/crates/domain/src/resource.rs index 5370a92..df3a5f2 100644 --- a/crates/domain/src/resource.rs +++ b/crates/domain/src/resource.rs @@ -8,4 +8,6 @@ pub enum Resource { Process(ProcessId), Interrupt(IrqId), ProcessCreate, + /// Machine-wide controls such as halting; given only to init at boot. + SystemControl, } diff --git a/crates/domain/src/syscall.rs b/crates/domain/src/syscall.rs index f33c164..7253d50 100644 --- a/crates/domain/src/syscall.rs +++ b/crates/domain/src/syscall.rs @@ -84,6 +84,11 @@ pub enum Syscall { va: VirtualAddress, perms: PagePerms, }, + /// The physical base of the region named by `region`, so a driver can + /// point a device's DMA at memory it has mapped. + RegionAddress { + region: CapSlot, + }, /// Removes a capability from the caller's own table. DropCap { slot: CapSlot, @@ -104,7 +109,10 @@ pub enum Syscall { GetProcessId, Yield, Exit, - Halt, + /// Stops the machine; needs a SystemControl capability. + Halt { + control: CapSlot, + }, CreateEndpoint, DebugPutchar { byte: u8, diff --git a/crates/kernel/src/abi.rs b/crates/kernel/src/abi.rs index 969e039..1b293dc 100644 --- a/crates/kernel/src/abi.rs +++ b/crates/kernel/src/abi.rs @@ -40,7 +40,9 @@ fn message(a: &[usize; 6]) -> Option { pub fn decode(nr: usize, a: &[usize; 6]) -> Option { Some(match nr { SYS_EXIT => Syscall::Exit, - SYS_HALT => Syscall::Halt, + SYS_HALT => Syscall::Halt { + control: slot(a[0])?, + }, SYS_EXIT_THREAD => Syscall::ExitThread, SYS_YIELD => Syscall::Yield, SYS_GET_TIME => Syscall::GetTime, @@ -125,6 +127,9 @@ pub fn decode(nr: usize, a: &[usize; 6]) -> Option { va: VirtualAddress::from_raw(a[1]), perms: reg_perms(a[2])?, }, + SYS_REGION_ADDRESS => Syscall::RegionAddress { + region: slot(a[0])?, + }, SYS_DEBUG_PUTCHAR => Syscall::DebugPutchar { byte: a[0] as u8 }, SYS_MAP_PAGE => Syscall::MapPage { process: slot(a[0])?, diff --git a/crates/kernel/src/board.rs b/crates/kernel/src/board.rs new file mode 100644 index 0000000..e838006 --- /dev/null +++ b/crates/kernel/src/board.rs @@ -0,0 +1,79 @@ +//! The authority init starts with. The board describes its devices; the +//! kernel turns each into capabilities (an MMIO region and an IRQ line) +//! and adds one SystemControl capability. Init then decides who gets what; +//! nothing else is privileged by its process id. + +use domain::{error::KernelError, resource::Resource}; +use platform::Platform; +use types::{ + address::PhysicalAddress, + bounded::PageCount, + id::{IrqId, ProcessId, RegionId}, + permission::{Permission, PermissionSet}, +}; + +use crate::Kernel; + +/// A device the board exposes to userspace: its MMIO window and, if it +/// raises interrupts, its IRQ line. +#[derive(Debug, Clone, Copy)] +pub struct BoardDevice { + pub base: PhysicalAddress, + pub pages: PageCount, + pub irq: Option, +} + +impl Kernel

{ + /// Creates one device region per board device, owned by `init`. Fails + /// on the first device that is misaligned, overlaps the frame pool or + /// another device, or names an IRQ the kernel cannot route. + pub fn register_board( + &mut self, + init: ProcessId, + devices: &[BoardDevice], + ) -> Result<(), KernelError> { + for device in devices { + if let Some(irq) = device.irq + && !self.irqs.is_valid(irq) + { + return Err(KernelError::InvalidIrq(irq)); + } + self.create_device_region(init, device.base, device.pages)?; + } + Ok(()) + } + + /// Gives `init` a capability to every registered device's region and + /// IRQ line, then one SystemControl capability. All carry Delegate so + /// init can hand them on. Done separately from `register_board` so + /// boot code can wire drivers to the regions first. + pub fn grant_boot_authority( + &mut self, + init: ProcessId, + devices: &[BoardDevice], + ) -> Result<(), KernelError> { + let delegate = PermissionSet::empty().with(Permission::Delegate); + let control = delegate.with(Permission::Execute); + let mmio = delegate.with(Permission::Read).with(Permission::Write); + for device in devices { + let region = self + .device_region_at(device.base) + .filter(|id| self.regions.get(*id).is_some_and(|r| r.owner() == init)) + .ok_or(KernelError::NoSuchObject)?; + self.grant_cap(init, Resource::Memory(region), mmio)?; + if let Some(irq) = device.irq { + self.grant_cap(init, Resource::Interrupt(irq), control)?; + } + } + self.grant_cap(init, Resource::SystemControl, control)?; + Ok(()) + } + + /// The device region that starts at `base`. + pub fn device_region_at(&self, base: PhysicalAddress) -> Option { + self.regions + .iter() + .find(|(_, r)| r.backing() == domain::memory::RegionBacking::Device && r.base() == base) + .map(|(id, _)| id) + } +} diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index 21fc436..e4426c5 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -15,6 +15,7 @@ )] pub mod abi; +mod board; mod captree; pub mod interrupt; mod memory; @@ -22,6 +23,7 @@ mod memory; pub mod proofs; pub mod syscall; +pub use board::BoardDevice; pub use syscall::{SyscallOutcome, SyscallResult}; #[cfg(test)] @@ -388,7 +390,9 @@ impl Kernel

{ Resource::Memory(id) => self.regions.generation(*id), Resource::Notification(id) => self.notifications.generation(*id), Resource::Process(pid) => self.processes.generation(*pid), - Resource::Interrupt(_) | Resource::ProcessCreate => Some(Generation::initial()), + Resource::Interrupt(_) | Resource::ProcessCreate | Resource::SystemControl => { + Some(Generation::initial()) + } } } diff --git a/crates/kernel/src/memory.rs b/crates/kernel/src/memory.rs index 0fb32ae..110baed 100644 --- a/crates/kernel/src/memory.rs +++ b/crates/kernel/src/memory.rs @@ -325,11 +325,23 @@ impl Kernel

{ } // Device memory must never alias frames the kernel hands out. let len = pages.value() as usize * PAGE_SIZE; + let start = base.raw(); + let end = start.checked_add(len).ok_or(KernelError::InvalidAddress)?; 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 { + if start < pool_end && end > pool_start { return Err(KernelError::InvalidAddress); } + // Nor another device's window: each device has one region, so + // revoking or delegating it means the same thing everywhere. + let overlaps = self.regions.iter().any(|(_, r)| { + let r_start = r.base().raw(); + let r_end = r_start.saturating_add(r.pages().value() as usize * PAGE_SIZE); + r.backing() == RegionBacking::Device && start < r_end && end > r_start + }); + if overlaps { + return Err(KernelError::AddressInUse); + } self.regions .insert(Region::new(owner, RegionBacking::Device, base, pages)) .ok_or(KernelError::NoFreeSlot) @@ -339,6 +351,10 @@ impl Kernel

{ self.regions.get(id).map(Region::base) } + pub fn region_pages(&self, id: RegionId) -> Option { + self.regions.get(id).map(Region::pages) + } + /// Maps all of region `id` at `va` in `pid`, all or nothing. /// Maps region `id` for boot code; the mapping is not tied to any /// capability and lasts until the region or the process dies. diff --git a/crates/kernel/src/syscall/memory.rs b/crates/kernel/src/syscall/memory.rs index 8e8b737..fdbe299 100644 --- a/crates/kernel/src/syscall/memory.rs +++ b/crates/kernel/src/syscall/memory.rs @@ -1,6 +1,9 @@ use platform::Platform; use types::{ - address::VirtualAddress, bounded::CapSlot, bounded::PageCount, id::ProcessId, + address::{PhysicalAddress, VirtualAddress}, + bounded::CapSlot, + bounded::PageCount, + id::ProcessId, mapping::PagePerms, }; @@ -115,4 +118,18 @@ impl Kernel

{ let via = self.cap_ref(pid, slot); self.map_region_via(id, pid, va, perms, via) } + + /// The physical base of the region behind `slot`. Needs Read on the + /// capability; pool regions are physically contiguous, so the base is + /// enough to program a device. + pub(crate) fn region_address_cap( + &self, + pid: ProcessId, + slot: CapSlot, + ) -> Result { + let Resource::Memory(id) = *self.check_cap(pid, slot, Permission::Read)?.resource() else { + return Err(KernelError::WrongObject); + }; + self.region_base(id).ok_or(KernelError::InvalidRegion) + } } diff --git a/crates/kernel/src/syscall/mod.rs b/crates/kernel/src/syscall/mod.rs index f1067fa..6950ebf 100644 --- a/crates/kernel/src/syscall/mod.rs +++ b/crates/kernel/src/syscall/mod.rs @@ -92,6 +92,10 @@ impl Kernel

{ self.map_region_cap(pid, region, va, perms) .map(|()| SyscallResult::Ok), ), + Syscall::RegionAddress { region } => complete( + self.region_address_cap(pid, region) + .map(|pa| SyscallResult::Value(pa.raw())), + ), Syscall::Delegate { slot, to_endpoint, @@ -136,7 +140,7 @@ impl Kernel

{ Syscall::AcknowledgeInterrupt { irq } => { complete(self.ack_interrupt(pid, irq).map(|()| SyscallResult::Ok)) } - Syscall::Halt => self.handle_halt(pid), + Syscall::Halt { control } => self.handle_halt(pid, control), Syscall::CreateEndpoint => self.handle_create_endpoint(pid), Syscall::DebugPutchar { byte } => self.handle_debug_putchar(byte), Syscall::MapPage { diff --git a/crates/kernel/src/syscall/simple.rs b/crates/kernel/src/syscall/simple.rs index efd6b36..9dac421 100644 --- a/crates/kernel/src/syscall/simple.rs +++ b/crates/kernel/src/syscall/simple.rs @@ -1,4 +1,6 @@ +use domain::{error::KernelError, resource::Resource}; use platform::Platform; +use types::{bounded::CapSlot, id::ProcessId, permission::Permission}; use super::{SyscallOutcome, SyscallResult}; use crate::Kernel; @@ -87,12 +89,13 @@ impl Kernel

{ SyscallOutcome::Complete(SyscallResult::Ok) } - pub(crate) fn handle_halt(&mut self, pid: types::id::ProcessId) -> SyscallOutcome { - if pid.raw() != 0 { - return SyscallOutcome::Complete(SyscallResult::Error( - domain::error::KernelError::ProcessCreateDenied, - )); + /// Halts the machine for a holder of a SystemControl capability with + /// Execute; anyone else gets an error and nothing happens. + pub(crate) fn handle_halt(&mut self, pid: ProcessId, control: CapSlot) -> SyscallOutcome { + match self.check_cap(pid, control, Permission::Execute) { + Ok(cap) if *cap.resource() == Resource::SystemControl => self.platform.halt(), + Ok(_) => SyscallOutcome::Complete(SyscallResult::Error(KernelError::WrongObject)), + Err(e) => SyscallOutcome::Complete(SyscallResult::Error(e)), } - self.platform.halt(); } } diff --git a/crates/kernel/src/tests/abi.rs b/crates/kernel/src/tests/abi.rs index 9e6b215..86e1168 100644 --- a/crates/kernel/src/tests/abi.rs +++ b/crates/kernel/src/tests/abi.rs @@ -41,6 +41,7 @@ fn every_defined_syscall_decodes() { SYS_SET_IRQ_NOTIFICATION, SYS_CREATE_REGION, SYS_MAP_REGION, + SYS_REGION_ADDRESS, ]; // slot 1, slot 2, small values, no cap let args = [1, 2, 1, 0, 0, NONE]; @@ -51,7 +52,7 @@ fn every_defined_syscall_decodes() { #[test] fn retired_and_unknown_numbers_do_not_decode() { - for nr in [3, 5, 35, 1000, usize::MAX] { + for nr in [3, 5, 36, 1000, usize::MAX] { assert!(decode(nr, &[0; 6]).is_none(), "{nr} decoded"); } } diff --git a/crates/kernel/src/tests/board.rs b/crates/kernel/src/tests/board.rs new file mode 100644 index 0000000..bf3af66 --- /dev/null +++ b/crates/kernel/src/tests/board.rs @@ -0,0 +1,323 @@ +use super::*; +use crate::{BoardDevice, SyscallOutcome, SyscallResult}; +use domain::{error::KernelError, resource::Resource, syscall::Syscall}; +use types::{ + address::{PhysicalAddress, VirtualAddress}, + badge::Badge, + bounded::{CapSlot, PageCount, Priority}, + id::IrqId, + mapping::PagePerms, + permission::{Permission, PermissionSet}, + time::TimeSlice, +}; + +const UART: usize = 0x1000_0000; +const VIRTIO: usize = 0x1000_1000; + +fn device(base: usize, pages: u16, irq: Option) -> BoardDevice { + BoardDevice { + base: PhysicalAddress::from_raw(base), + pages: PageCount::new(pages).unwrap(), + irq: irq.map(IrqId::from_raw), + } +} + +fn board() -> [BoardDevice; 2] { + [device(UART, 1, Some(10)), device(VIRTIO, 1, Some(1))] +} + +/// Init after boot with the board registered and its authority granted. +fn booted() -> (Kernel, ProcessId) { + let (mut k, init) = kernel_with_process(); + k.register_board(init, &board()).unwrap(); + k.grant_boot_authority(init, &board()).unwrap(); + (k, init) +} + +fn slot_holding(k: &Kernel, pid: ProcessId, resource: Resource) -> CapSlot { + k.processes + .get(pid) + .unwrap() + .capabilities() + .iter() + .find(|(_, c)| *c.resource() == resource) + .map(|(s, _)| s) + .expect("capability present") +} + +fn perms_of(k: &Kernel, pid: ProcessId, slot: CapSlot) -> PermissionSet { + *k.processes + .get(pid) + .unwrap() + .capabilities() + .get(slot) + .unwrap() + .permissions() +} + +fn error_of(outcome: SyscallOutcome) -> KernelError { + match outcome { + SyscallOutcome::Complete(SyscallResult::Error(e)) => e, + _ => panic!("expected an error"), + } +} + +// ---- halt ---- + +#[test] +fn init_cannot_halt_without_a_system_control_cap() { + let (mut k, _) = kernel_with_process(); + let e = error_of(k.handle_syscall(Syscall::Halt { + control: CapSlot::new(0).unwrap(), + })); + assert!( + matches!(e, KernelError::WrongObject), + "slot 0 is ProcessCreate" + ); + let e = error_of(k.handle_syscall(Syscall::Halt { + control: CapSlot::new(9).unwrap(), + })); + assert!(matches!(e, KernelError::InvalidSlot(_))); +} + +#[test] +#[should_panic(expected = "halt")] +fn system_control_cap_halts() { + let (mut k, init) = booted(); + let control = slot_holding(&k, init, Resource::SystemControl); + k.handle_syscall(Syscall::Halt { control }); +} + +#[test] +fn halt_needs_execute_on_the_cap() { + let (mut k, init) = booted(); + let control = slot_holding(&k, init, Resource::SystemControl); + let weak = k + .mint( + init, + control, + Badge::new(0), + PermissionSet::empty().with(Permission::Delegate), + ) + .unwrap(); + let e = error_of(k.handle_syscall(Syscall::Halt { control: weak })); + assert!(matches!(e, KernelError::PermissionDenied { .. })); +} + +#[test] +#[should_panic(expected = "halt")] +fn any_holder_of_the_cap_may_halt_not_just_init() { + let (mut k, init) = booted(); + let other = k.create_process_internal().unwrap(); + let tid = k.spawn_thread_for_test(other, Priority::DEFAULT, TimeSlice::DEFAULT); + k.grant_cap_for_test( + other, + Resource::SystemControl, + PermissionSet::empty().with(Permission::Execute), + ); + let _ = init; + k.switch_to_thread_for_test(tid); + let control = slot_holding(&k, other, Resource::SystemControl); + k.handle_syscall(Syscall::Halt { control }); +} + +// ---- boot authority ---- + +#[test] +fn init_gets_mmio_irq_and_control_caps_it_can_delegate() { + let (k, init) = booted(); + let delegate = Permission::Delegate; + for d in board() { + let region = k.device_region_at(d.base).unwrap(); + let mem = slot_holding(&k, init, Resource::Memory(region)); + let p = perms_of(&k, init, mem); + assert!(p.allows(Permission::Read) && p.allows(Permission::Write) && p.allows(delegate)); + assert!(!p.allows(Permission::Execute), "MMIO is never executable"); + let irq = slot_holding(&k, init, Resource::Interrupt(d.irq.unwrap())); + let p = perms_of(&k, init, irq); + assert!(p.allows(Permission::Execute) && p.allows(delegate)); + } + let control = slot_holding(&k, init, Resource::SystemControl); + let p = perms_of(&k, init, control); + assert!(p.allows(Permission::Execute) && p.allows(delegate)); +} + +#[test] +fn boot_authority_keeps_existing_slots() { + let (mut k, init) = kernel_with_process(); + let before = k.cap_slots_for_test(init); + k.register_board(init, &board()).unwrap(); + k.grant_boot_authority(init, &board()).unwrap(); + let after = k.cap_slots_for_test(init); + for (b, a) in before.iter().zip(after.iter()) { + assert!(!b || *a, "an existing slot was disturbed"); + } + assert_eq!(after.iter().filter(|s| **s).count(), 1 + 2 + 2 + 1); +} + +#[test] +fn init_can_map_a_device_through_its_cap() { + let (mut k, init) = booted(); + let region = slot_holding( + &k, + init, + Resource::Memory(k.device_region_at(PhysicalAddress::from_raw(UART)).unwrap()), + ); + let outcome = k.handle_syscall(Syscall::MapRegion { + region, + va: VirtualAddress::from_raw(0x10_0000), + perms: PagePerms::ReadWrite, + }); + assert!(matches!( + outcome, + SyscallOutcome::Complete(SyscallResult::Ok) + )); + let e = error_of(k.handle_syscall(Syscall::MapRegion { + region, + va: VirtualAddress::from_raw(0x20_0000), + perms: PagePerms::ReadExecute, + })); + assert!(matches!(e, KernelError::PermissionDenied { .. })); +} + +#[test] +fn init_can_route_a_board_irq() { + let (mut k, init) = booted(); + let irq = slot_holding(&k, init, Resource::Interrupt(IrqId::from_raw(1))); + let notification = k.create_notification_cap(init).unwrap(); + let outcome = k.handle_syscall(Syscall::SetIrqNotification { irq, notification }); + assert!(matches!( + outcome, + SyscallOutcome::Complete(SyscallResult::Ok) + )); +} + +#[test] +fn granting_needs_a_registered_board() { + let (mut k, init) = kernel_with_process(); + assert!(matches!( + k.grant_boot_authority(init, &board()), + Err(KernelError::NoSuchObject) + )); +} + +#[test] +fn granting_needs_the_regions_to_be_inits() { + let (mut k, init) = kernel_with_process(); + let other = k.create_process_internal().unwrap(); + k.register_board(other, &board()).unwrap(); + assert!(matches!( + k.grant_boot_authority(init, &board()), + Err(KernelError::NoSuchObject) + )); +} + +#[test] +fn board_devices_may_not_overlap() { + let (mut k, init) = kernel_with_process(); + let overlapping = [device(UART, 2, None), device(UART + 0x1000, 1, None)]; + assert!(matches!( + k.register_board(init, &overlapping), + Err(KernelError::AddressInUse) + )); +} + +#[test] +fn board_devices_may_not_alias_the_frame_pool() { + let (mut k, init) = kernel_with_process(); + assert!(matches!( + k.register_board(init, &[device(0x8400_0000, 1, None)]), + Err(KernelError::InvalidAddress) + )); +} + +#[test] +fn board_devices_must_be_page_aligned_and_in_range() { + let (mut k, init) = kernel_with_process(); + assert!(matches!( + k.register_board(init, &[device(UART + 8, 1, None)]), + Err(KernelError::InvalidAddress) + )); + assert!(matches!( + k.register_board(init, &[device(usize::MAX & !0xfff, 2, None)]), + Err(KernelError::InvalidAddress) + )); +} + +#[test] +fn board_irqs_must_be_routable() { + let (mut k, init) = kernel_with_process(); + let bad = IrqId::from_raw(u16::MAX); + assert!(matches!( + k.register_board(init, &[device(UART, 1, Some(u16::MAX))]), + Err(KernelError::InvalidIrq(i)) if i == bad + )); +} + +// ---- region address ---- + +#[test] +fn region_address_reports_the_physical_base() { + let (mut k, init) = booted(); + let pool = k + .create_region_cap(init, PageCount::new(2).unwrap()) + .unwrap(); + let outcome = k.handle_syscall(Syscall::RegionAddress { region: pool }); + let SyscallOutcome::Complete(SyscallResult::Value(pa)) = outcome else { + panic!("expected an address"); + }; + let Resource::Memory(id) = *k + .processes + .get(init) + .unwrap() + .capabilities() + .get(pool) + .unwrap() + .resource() + else { + panic!("not a region cap"); + }; + assert_eq!(Some(pa), k.region_base(id).map(|b| b.raw())); + + let uart = slot_holding( + &k, + init, + Resource::Memory(k.device_region_at(PhysicalAddress::from_raw(UART)).unwrap()), + ); + let outcome = k.handle_syscall(Syscall::RegionAddress { region: uart }); + assert!(matches!( + outcome, + SyscallOutcome::Complete(SyscallResult::Value(UART)) + )); +} + +#[test] +fn region_address_needs_read() { + let (mut k, init) = booted(); + let pool = k + .create_region_cap(init, PageCount::new(1).unwrap()) + .unwrap(); + let blind = k + .mint( + init, + pool, + Badge::new(0), + PermissionSet::empty().with(Permission::Write), + ) + .unwrap(); + let e = error_of(k.handle_syscall(Syscall::RegionAddress { region: blind })); + assert!(matches!(e, KernelError::PermissionDenied { .. })); +} + +#[test] +fn region_address_rejects_other_objects() { + let (mut k, _) = booted(); + // An endpoint cap carries Read, so this fails on the object type. + let SyscallOutcome::Complete(SyscallResult::Slot(endpoint)) = + k.handle_syscall(Syscall::CreateEndpoint) + else { + panic!("expected an endpoint"); + }; + let e = error_of(k.handle_syscall(Syscall::RegionAddress { region: endpoint })); + assert!(matches!(e, KernelError::WrongObject)); +} diff --git a/crates/kernel/src/tests/mod.rs b/crates/kernel/src/tests/mod.rs index b7e2569..bd866ea 100644 --- a/crates/kernel/src/tests/mod.rs +++ b/crates/kernel/src/tests/mod.rs @@ -2,6 +2,7 @@ mod helpers; mod mock_platform; mod abi; +mod board; mod capability; mod captree_model; mod exit; diff --git a/crates/kernel/src/tests/storm.rs b/crates/kernel/src/tests/storm.rs index c507a7c..77f3087 100644 --- a/crates/kernel/src/tests/storm.rs +++ b/crates/kernel/src/tests/storm.rs @@ -310,7 +310,7 @@ fn new_round(k: &mut Kernel, init: ProcessId, init_thread: ThreadI #[test] fn the_kernel_survives_a_syscall_storm() { - let mut total = [0usize; 36]; + let mut total = [0usize; 36 + 1]; for seed in [17u64, 0xABCD_EF01, 4242, 0x1357_9BDF, 8] { let (mut k, init) = kernel_with_process(); let init_thread = k.current_thread().unwrap(); @@ -318,7 +318,7 @@ fn the_kernel_survives_a_syscall_storm() { let mut rng = Rng(seed); let mut executed = 0; let mut rounds = 0; - let mut succeeded = [0usize; 36]; + let mut succeeded = [0usize; 36 + 1]; new_round(&mut k, init, init_thread); let mut step = 0; @@ -350,9 +350,11 @@ fn the_kernel_survives_a_syscall_storm() { } let tid = runnable[rng.below(runnable.len())]; let is_init = k.thread_process(tid) == Some(init); - let nr = rng.below(36); - // Halting stops the machine and init exiting ends the test. - if nr == SYS_HALT || (is_init && (nr == SYS_EXIT || nr == SYS_EXIT_THREAD)) { + let nr = rng.below(36 + 1); + // init exiting ends the test. Halt stays in: nobody here holds + // a SystemControl cap, so a halt that got through would panic + // the mock platform and fail the test. + if is_init && (nr == SYS_EXIT || nr == SYS_EXIT_THREAD) { continue; } let args: [usize; 6] = core::array::from_fn(|_| rng.arg()); @@ -415,6 +417,7 @@ fn the_kernel_survives_a_syscall_storm() { SYS_RELEASE_PAGES, SYS_CREATE_REGION, SYS_MAP_REGION, + SYS_REGION_ADDRESS, SYS_KILL, SYS_CREATE_PROCESS, SYS_SPAWN_THREAD, diff --git a/crates/libkos/src/syscall.rs b/crates/libkos/src/syscall.rs index dc857d6..4f2e67d 100644 --- a/crates/libkos/src/syscall.rs +++ b/crates/libkos/src/syscall.rs @@ -89,8 +89,10 @@ pub fn yield_now() { let _ = call0(SYS_YIELD); } -pub fn halt() -> ! { - let _ = call0(SYS_HALT); +/// Stops the machine. Needs a SystemControl capability; without one the +/// call fails and this spins, since there is nothing sensible to return. +pub fn halt(control: CapSlot) -> ! { + let _ = raw(SYS_HALT, [control.value() as usize, 0, 0, 0, 0, 0]); loop {} } @@ -185,6 +187,12 @@ pub fn map_region(region: CapSlot, va: VirtualAddress, perms: PagePerms) -> Resu .map(|_| ()) } +/// The physical base of the region behind `region`, for programming a +/// device's DMA. Pool regions are physically contiguous. +pub fn region_address(region: CapSlot) -> Result { + raw(SYS_REGION_ADDRESS, [region.value() as usize, 0, 0, 0, 0, 0]).map(|r| r[1]) +} + // ---- capabilities ---- pub fn delegate(slot: CapSlot, to: CapSlot, permissions: PermissionSet) -> bool { diff --git a/crates/types/src/syscall_abi.rs b/crates/types/src/syscall_abi.rs index 7247cd4..629b115 100644 --- a/crates/types/src/syscall_abi.rs +++ b/crates/types/src/syscall_abi.rs @@ -40,6 +40,7 @@ pub const SYS_BIND_NOTIFICATION: usize = 31; pub const SYS_SET_IRQ_NOTIFICATION: usize = 32; pub const SYS_CREATE_REGION: usize = 33; pub const SYS_MAP_REGION: usize = 34; +pub const SYS_REGION_ADDRESS: usize = 35; /// `PagePerms` in a register. pub const PERMS_READ_ONLY: usize = 0; diff --git a/docs/ipc.md b/docs/ipc.md index ca67e60..6089154 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -48,6 +48,26 @@ as `cap`: deleting or revoking `cap` unmaps it. `perms` needs `Read`, plus | Notification | Read | `Wait`, `Poll`, `BindNotification` | | Notification | Delegate | `Mint` | | Interrupt | Execute | `SetIrqNotification`, `AckInterrupt` | +| Region | Read | `MapRegion` (read-only), `RegionAddress` | +| SystemControl| Execute | `Halt` | + +## Boot authority + +No process is privileged by its id. At boot the kernel gives init: + +- a `ProcessCreate` capability in slot 0; +- for every device in the board table, a capability to its MMIO region + (`Read`, `Write`, `Delegate`; never `Execute`) and, if it interrupts, one + to its IRQ line (`Execute`, `Delegate`); +- one `SystemControl` capability (`Execute`, `Delegate`). + +The board capabilities come after the capabilities boot code hands out +for the built-in servers, so those slots are unchanged. Device regions +never overlap each other or the frame pool, and each device has exactly +one region, so delegating or revoking it means the same thing everywhere. + +`RegionAddress(cap)` returns a region's physical base so a driver can +program a device's DMA; pool regions are physically contiguous. ## Messages