feat(kernel): SystemControl cap for halt, board device table + boot caps to init, RegionAddress syscall, idle loop services irqs

This commit is contained in:
2026-10-26 23:59:57 +01:00
parent 130a82f50d
commit bafc18a5f8
21 changed files with 626 additions and 53 deletions

View File

@@ -8,3 +8,29 @@ pub const CLINT_BASE: usize = 0x02000000;
pub const USER_POOL_BASE: usize = 0x84000000; pub const USER_POOL_BASE: usize = 0x84000000;
pub const USER_POOL_PAGES: usize = 4096; pub const USER_POOL_PAGES: usize = 4096;
pub const TICK_INTERVAL: u64 = 10_000_000; 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)
}

View File

@@ -10,7 +10,9 @@ use types::mapping::PagePerms;
// Boot-time device wiring. Everything here goes through the kernel's // Boot-time device wiring. Everything here goes through the kernel's
// mapping API so the kernel knows about every page a driver can touch. // 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<crate::platform::RiscVPlatform> { fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
unsafe { &mut *crate::kernel_ptr() } 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")) .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) { pub(crate) fn map_mmio(uart: &Uart, pid: ProcessId, va: usize, pa: usize, n: usize) {
let id = kern() let id = kern()
.create_device_region(pid, PhysicalAddress::from_raw(pa), pages(uart, n)) .device_region_at(PhysicalAddress::from_raw(pa))
.unwrap_or_else(|_| crate::halt(uart, "device region failed")); .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() kern()
.map_region(id, pid, VirtualAddress::from_raw(va), PagePerms::ReadWrite) .map_region(id, pid, VirtualAddress::from_raw(va), PagePerms::ReadWrite)
.unwrap_or_else(|_| crate::halt(uart, "device map failed")); .unwrap_or_else(|_| crate::halt(uart, "device map failed"));

View File

@@ -17,6 +17,12 @@ use kernel_cell::KernelCell;
static KERNEL: KernelCell<kernel::Kernel<platform::RiscVPlatform>> = KernelCell::uninit(); static KERNEL: KernelCell<kernel::Kernel<platform::RiscVPlatform>> = KernelCell::uninit();
fn kern_mut() -> &'static mut kernel::Kernel<platform::RiscVPlatform> {
// SAFETY: single core; boot code runs before any trap can touch the
// kernel.
unsafe { &mut *kernel_ptr() }
}
pub fn kernel_ptr() -> *mut kernel::Kernel<platform::RiscVPlatform> { pub fn kernel_ptr() -> *mut kernel::Kernel<platform::RiscVPlatform> {
KERNEL.as_ptr() 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 input = boot::loader::load_server(&uart, &mut blk, b"input.elf", "input");
let virtio_base = blk.base(); 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::storage_setup::configure(&uart, init.pid, storage.pid, virtio_base);
devices::gpu_setup::configure(&uart, display.pid); devices::gpu_setup::configure(&uart, display.pid);
devices::input_setup::configure(&uart, input.pid); devices::input_setup::configure(&uart, input.pid);
boot::caps::grant_all(&uart, init.pid, storage.pid, display.pid, 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"); uart.write_str("k-os: entering userspace\n");
boot::loader::enter_userspace(init.pid, init.entry, SHARED_BUF_VA); boot::loader::enter_userspace(init.pid, init.entry, SHARED_BUF_VA);

View File

@@ -218,32 +218,66 @@ fn switch_to_next(frame: &mut TrapFrame, current_tid: types::id::ThreadId) {
save_frame_to_context(frame, ctx); save_frame_to_context(frame, ctx);
} }
if let Some(next_tid) = kern.pick_next_thread() { let next_tid = next_runnable();
let next_pid = kern.thread_process(next_tid); let kern = unsafe { &mut *crate::kernel_ptr() };
let current_pid = kern.thread_process(current_tid); 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 core = types::id::CoreId::from_raw(0);
let _ = kern.set_thread_running(next_tid, core); let _ = kern.set_thread_running(next_tid, core);
if let Some(ctx) = kern.thread_context(next_tid) { if let Some(ctx) = kern.thread_context(next_tid) {
load_context_to_frame(ctx, frame); 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 /// Claims every pending PLIC interrupt and hands each to the kernel.
&& let Some(pid) = next_pid fn dispatch_external() {
{ loop {
let pt_addr = kern.platform_mut().page_table_for(pid); let irq_num = crate::hw::plic::Plic::claim();
if pt_addr != 0 { if irq_num == 0 {
let pt = unsafe { &*(pt_addr as *const crate::mmu::PageTable) }; return;
crate::mmu::activate(pt);
}
}
} else {
loop {
unsafe { core::arch::asm!("wfi") };
} }
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 => { CAUSE_S_EXTERNAL => dispatch_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);
}
}
_ => {} _ => {}
} }
} }

View File

@@ -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 /// Routes `irq`, replacing any previous route. Fails for lines beyond
/// the table. /// the table.
pub fn set(&mut self, irq: IrqId, route: IrqRoute) -> bool { pub fn set(&mut self, irq: IrqId, route: IrqRoute) -> bool {

View File

@@ -8,4 +8,6 @@ pub enum Resource {
Process(ProcessId), Process(ProcessId),
Interrupt(IrqId), Interrupt(IrqId),
ProcessCreate, ProcessCreate,
/// Machine-wide controls such as halting; given only to init at boot.
SystemControl,
} }

View File

@@ -84,6 +84,11 @@ pub enum Syscall {
va: VirtualAddress, va: VirtualAddress,
perms: PagePerms, 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. /// Removes a capability from the caller's own table.
DropCap { DropCap {
slot: CapSlot, slot: CapSlot,
@@ -104,7 +109,10 @@ pub enum Syscall {
GetProcessId, GetProcessId,
Yield, Yield,
Exit, Exit,
Halt, /// Stops the machine; needs a SystemControl capability.
Halt {
control: CapSlot,
},
CreateEndpoint, CreateEndpoint,
DebugPutchar { DebugPutchar {
byte: u8, byte: u8,

View File

@@ -40,7 +40,9 @@ fn message(a: &[usize; 6]) -> Option<Message> {
pub fn decode(nr: usize, a: &[usize; 6]) -> Option<Syscall> { pub fn decode(nr: usize, a: &[usize; 6]) -> Option<Syscall> {
Some(match nr { Some(match nr {
SYS_EXIT => Syscall::Exit, SYS_EXIT => Syscall::Exit,
SYS_HALT => Syscall::Halt, SYS_HALT => Syscall::Halt {
control: slot(a[0])?,
},
SYS_EXIT_THREAD => Syscall::ExitThread, SYS_EXIT_THREAD => Syscall::ExitThread,
SYS_YIELD => Syscall::Yield, SYS_YIELD => Syscall::Yield,
SYS_GET_TIME => Syscall::GetTime, SYS_GET_TIME => Syscall::GetTime,
@@ -125,6 +127,9 @@ pub fn decode(nr: usize, a: &[usize; 6]) -> Option<Syscall> {
va: VirtualAddress::from_raw(a[1]), va: VirtualAddress::from_raw(a[1]),
perms: reg_perms(a[2])?, 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_DEBUG_PUTCHAR => Syscall::DebugPutchar { byte: a[0] as u8 },
SYS_MAP_PAGE => Syscall::MapPage { SYS_MAP_PAGE => Syscall::MapPage {
process: slot(a[0])?, process: slot(a[0])?,

View File

@@ -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<IrqId>,
}
impl<P: Platform> Kernel<P> {
/// 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<RegionId> {
self.regions
.iter()
.find(|(_, r)| r.backing() == domain::memory::RegionBacking::Device && r.base() == base)
.map(|(id, _)| id)
}
}

View File

@@ -15,6 +15,7 @@
)] )]
pub mod abi; pub mod abi;
mod board;
mod captree; mod captree;
pub mod interrupt; pub mod interrupt;
mod memory; mod memory;
@@ -22,6 +23,7 @@ mod memory;
pub mod proofs; pub mod proofs;
pub mod syscall; pub mod syscall;
pub use board::BoardDevice;
pub use syscall::{SyscallOutcome, SyscallResult}; pub use syscall::{SyscallOutcome, SyscallResult};
#[cfg(test)] #[cfg(test)]
@@ -388,7 +390,9 @@ impl<P: Platform> Kernel<P> {
Resource::Memory(id) => self.regions.generation(*id), Resource::Memory(id) => self.regions.generation(*id),
Resource::Notification(id) => self.notifications.generation(*id), Resource::Notification(id) => self.notifications.generation(*id),
Resource::Process(pid) => self.processes.generation(*pid), Resource::Process(pid) => self.processes.generation(*pid),
Resource::Interrupt(_) | Resource::ProcessCreate => Some(Generation::initial()), Resource::Interrupt(_) | Resource::ProcessCreate | Resource::SystemControl => {
Some(Generation::initial())
}
} }
} }

View File

@@ -325,11 +325,23 @@ impl<P: Platform> Kernel<P> {
} }
// Device memory must never alias frames the kernel hands out. // Device memory must never alias frames the kernel hands out.
let len = pages.value() as usize * PAGE_SIZE; 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_start = self.frames.base().raw();
let pool_end = pool_start + self.frames.capacity() * PAGE_SIZE; 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); 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 self.regions
.insert(Region::new(owner, RegionBacking::Device, base, pages)) .insert(Region::new(owner, RegionBacking::Device, base, pages))
.ok_or(KernelError::NoFreeSlot) .ok_or(KernelError::NoFreeSlot)
@@ -339,6 +351,10 @@ impl<P: Platform> Kernel<P> {
self.regions.get(id).map(Region::base) self.regions.get(id).map(Region::base)
} }
pub fn region_pages(&self, id: RegionId) -> Option<PageCount> {
self.regions.get(id).map(Region::pages)
}
/// Maps all of region `id` at `va` in `pid`, all or nothing. /// 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 /// Maps region `id` for boot code; the mapping is not tied to any
/// capability and lasts until the region or the process dies. /// capability and lasts until the region or the process dies.

View File

@@ -1,6 +1,9 @@
use platform::Platform; use platform::Platform;
use types::{ use types::{
address::VirtualAddress, bounded::CapSlot, bounded::PageCount, id::ProcessId, address::{PhysicalAddress, VirtualAddress},
bounded::CapSlot,
bounded::PageCount,
id::ProcessId,
mapping::PagePerms, mapping::PagePerms,
}; };
@@ -115,4 +118,18 @@ impl<P: Platform> Kernel<P> {
let via = self.cap_ref(pid, slot); let via = self.cap_ref(pid, slot);
self.map_region_via(id, pid, va, perms, via) 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<PhysicalAddress, KernelError> {
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)
}
} }

View File

@@ -92,6 +92,10 @@ impl<P: Platform> Kernel<P> {
self.map_region_cap(pid, region, va, perms) self.map_region_cap(pid, region, va, perms)
.map(|()| SyscallResult::Ok), .map(|()| SyscallResult::Ok),
), ),
Syscall::RegionAddress { region } => complete(
self.region_address_cap(pid, region)
.map(|pa| SyscallResult::Value(pa.raw())),
),
Syscall::Delegate { Syscall::Delegate {
slot, slot,
to_endpoint, to_endpoint,
@@ -136,7 +140,7 @@ impl<P: Platform> Kernel<P> {
Syscall::AcknowledgeInterrupt { irq } => { Syscall::AcknowledgeInterrupt { irq } => {
complete(self.ack_interrupt(pid, irq).map(|()| SyscallResult::Ok)) 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::CreateEndpoint => self.handle_create_endpoint(pid),
Syscall::DebugPutchar { byte } => self.handle_debug_putchar(byte), Syscall::DebugPutchar { byte } => self.handle_debug_putchar(byte),
Syscall::MapPage { Syscall::MapPage {

View File

@@ -1,4 +1,6 @@
use domain::{error::KernelError, resource::Resource};
use platform::Platform; use platform::Platform;
use types::{bounded::CapSlot, id::ProcessId, permission::Permission};
use super::{SyscallOutcome, SyscallResult}; use super::{SyscallOutcome, SyscallResult};
use crate::Kernel; use crate::Kernel;
@@ -87,12 +89,13 @@ impl<P: Platform> Kernel<P> {
SyscallOutcome::Complete(SyscallResult::Ok) SyscallOutcome::Complete(SyscallResult::Ok)
} }
pub(crate) fn handle_halt(&mut self, pid: types::id::ProcessId) -> SyscallOutcome { /// Halts the machine for a holder of a SystemControl capability with
if pid.raw() != 0 { /// Execute; anyone else gets an error and nothing happens.
return SyscallOutcome::Complete(SyscallResult::Error( pub(crate) fn handle_halt(&mut self, pid: ProcessId, control: CapSlot) -> SyscallOutcome {
domain::error::KernelError::ProcessCreateDenied, 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();
} }
} }

View File

@@ -41,6 +41,7 @@ fn every_defined_syscall_decodes() {
SYS_SET_IRQ_NOTIFICATION, SYS_SET_IRQ_NOTIFICATION,
SYS_CREATE_REGION, SYS_CREATE_REGION,
SYS_MAP_REGION, SYS_MAP_REGION,
SYS_REGION_ADDRESS,
]; ];
// slot 1, slot 2, small values, no cap // slot 1, slot 2, small values, no cap
let args = [1, 2, 1, 0, 0, NONE]; let args = [1, 2, 1, 0, 0, NONE];
@@ -51,7 +52,7 @@ fn every_defined_syscall_decodes() {
#[test] #[test]
fn retired_and_unknown_numbers_do_not_decode() { 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"); assert!(decode(nr, &[0; 6]).is_none(), "{nr} decoded");
} }
} }

View File

@@ -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<u16>) -> 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<MockPlatform>, 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<MockPlatform>, 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<MockPlatform>, 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));
}

View File

@@ -2,6 +2,7 @@ mod helpers;
mod mock_platform; mod mock_platform;
mod abi; mod abi;
mod board;
mod capability; mod capability;
mod captree_model; mod captree_model;
mod exit; mod exit;

View File

@@ -310,7 +310,7 @@ fn new_round(k: &mut Kernel<MockPlatform>, init: ProcessId, init_thread: ThreadI
#[test] #[test]
fn the_kernel_survives_a_syscall_storm() { 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] { for seed in [17u64, 0xABCD_EF01, 4242, 0x1357_9BDF, 8] {
let (mut k, init) = kernel_with_process(); let (mut k, init) = kernel_with_process();
let init_thread = k.current_thread().unwrap(); 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 rng = Rng(seed);
let mut executed = 0; let mut executed = 0;
let mut rounds = 0; let mut rounds = 0;
let mut succeeded = [0usize; 36]; let mut succeeded = [0usize; 36 + 1];
new_round(&mut k, init, init_thread); new_round(&mut k, init, init_thread);
let mut step = 0; let mut step = 0;
@@ -350,9 +350,11 @@ fn the_kernel_survives_a_syscall_storm() {
} }
let tid = runnable[rng.below(runnable.len())]; let tid = runnable[rng.below(runnable.len())];
let is_init = k.thread_process(tid) == Some(init); let is_init = k.thread_process(tid) == Some(init);
let nr = rng.below(36); let nr = rng.below(36 + 1);
// Halting stops the machine and init exiting ends the test. // init exiting ends the test. Halt stays in: nobody here holds
if nr == SYS_HALT || (is_init && (nr == SYS_EXIT || nr == SYS_EXIT_THREAD)) { // 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; continue;
} }
let args: [usize; 6] = core::array::from_fn(|_| rng.arg()); 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_RELEASE_PAGES,
SYS_CREATE_REGION, SYS_CREATE_REGION,
SYS_MAP_REGION, SYS_MAP_REGION,
SYS_REGION_ADDRESS,
SYS_KILL, SYS_KILL,
SYS_CREATE_PROCESS, SYS_CREATE_PROCESS,
SYS_SPAWN_THREAD, SYS_SPAWN_THREAD,

View File

@@ -89,8 +89,10 @@ pub fn yield_now() {
let _ = call0(SYS_YIELD); let _ = call0(SYS_YIELD);
} }
pub fn halt() -> ! { /// Stops the machine. Needs a SystemControl capability; without one the
let _ = call0(SYS_HALT); /// 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 {} loop {}
} }
@@ -185,6 +187,12 @@ pub fn map_region(region: CapSlot, va: VirtualAddress, perms: PagePerms) -> Resu
.map(|_| ()) .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<usize> {
raw(SYS_REGION_ADDRESS, [region.value() as usize, 0, 0, 0, 0, 0]).map(|r| r[1])
}
// ---- capabilities ---- // ---- capabilities ----
pub fn delegate(slot: CapSlot, to: CapSlot, permissions: PermissionSet) -> bool { pub fn delegate(slot: CapSlot, to: CapSlot, permissions: PermissionSet) -> bool {

View File

@@ -40,6 +40,7 @@ pub const SYS_BIND_NOTIFICATION: usize = 31;
pub const SYS_SET_IRQ_NOTIFICATION: usize = 32; pub const SYS_SET_IRQ_NOTIFICATION: usize = 32;
pub const SYS_CREATE_REGION: usize = 33; pub const SYS_CREATE_REGION: usize = 33;
pub const SYS_MAP_REGION: usize = 34; pub const SYS_MAP_REGION: usize = 34;
pub const SYS_REGION_ADDRESS: usize = 35;
/// `PagePerms` in a register. /// `PagePerms` in a register.
pub const PERMS_READ_ONLY: usize = 0; pub const PERMS_READ_ONLY: usize = 0;

View File

@@ -48,6 +48,26 @@ as `cap`: deleting or revoking `cap` unmaps it. `perms` needs `Read`, plus
| Notification | Read | `Wait`, `Poll`, `BindNotification` | | Notification | Read | `Wait`, `Poll`, `BindNotification` |
| Notification | Delegate | `Mint` | | Notification | Delegate | `Mint` |
| Interrupt | Execute | `SetIrqNotification`, `AckInterrupt` | | 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 ## Messages