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

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