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_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)
}

View File

@@ -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<crate::platform::RiscVPlatform> {
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"));

View File

@@ -17,6 +17,12 @@ use kernel_cell::KernelCell;
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> {
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);

View File

@@ -218,7 +218,8 @@ 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_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);
@@ -240,10 +241,43 @@ fn switch_to_next(frame: &mut TrapFrame, current_tid: types::id::ThreadId) {
crate::mmu::activate(pt);
}
}
} else {
}
/// 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 {
unsafe { core::arch::asm!("wfi") };
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();
}
}
}
/// 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(),
_ => {}
}
}

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
/// the table.
pub fn set(&mut self, irq: IrqId, route: IrqRoute) -> bool {

View File

@@ -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,
}

View File

@@ -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,

View File

@@ -40,7 +40,9 @@ fn message(a: &[usize; 6]) -> Option<Message> {
pub fn decode(nr: usize, a: &[usize; 6]) -> Option<Syscall> {
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<Syscall> {
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])?,

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;
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<P: Platform> Kernel<P> {
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())
}
}
}

View File

@@ -325,11 +325,23 @@ impl<P: Platform> Kernel<P> {
}
// 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<P: Platform> Kernel<P> {
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 region `id` for boot code; the mapping is not tied to any
/// capability and lasts until the region or the process dies.

View File

@@ -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<P: Platform> Kernel<P> {
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<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)
.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<P: Platform> Kernel<P> {
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 {

View File

@@ -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<P: Platform> Kernel<P> {
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();
}
}

View File

@@ -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");
}
}

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 abi;
mod board;
mod capability;
mod captree_model;
mod exit;

View File

@@ -310,7 +310,7 @@ fn new_round(k: &mut Kernel<MockPlatform>, 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,

View File

@@ -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<usize> {
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 {

View File

@@ -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;

View File

@@ -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