fix: sched round-robin, cap slot OOB panic, clippy, make ci

- RunList -> intrusive FIFO, no starvation, no silent drop
- cap table sized to CapSlot range (kani caught OOB panic)
- snake food color test uses const
- kani cfg lint, RUST_MIN_STACK for big kernel struct in tests
This commit is contained in:
2026-10-26 19:08:30 +01:00
parent 1c04ddf909
commit e931485ca7
32 changed files with 384 additions and 251 deletions

View File

@@ -7,3 +7,6 @@ edition = "2024"
types = { workspace = true }
domain = { workspace = true }
platform = { workspace = true }
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(kani)"] }

View File

@@ -196,20 +196,20 @@ impl<P: Platform> Kernel<P> {
if current == tid {
return;
}
if let Ok(t) = self.threads.get_mut(current) {
if matches!(t.state(), ThreadState::Running(_)) {
let priority = t.priority();
let _ = t.preempt();
self.scheduler.add(current, priority, core);
}
if let Ok(t) = self.threads.get_mut(current)
&& matches!(t.state(), ThreadState::Running(_))
{
let priority = t.priority();
let _ = t.preempt();
self.scheduler.add(current, priority, core);
}
}
if let Ok(t) = self.threads.get(tid) {
if matches!(t.state(), ThreadState::Ready) {
self.scheduler.next(core);
let _ = self.threads.get_mut(tid).map(|t| t.set_running(core));
}
if let Ok(t) = self.threads.get(tid)
&& matches!(t.state(), ThreadState::Ready)
{
self.scheduler.next(core);
let _ = self.threads.get_mut(tid).map(|t| t.set_running(core));
}
}
@@ -265,26 +265,19 @@ impl<P: Platform> Kernel<P> {
pub(crate) fn cleanup_threads(&mut self, thread_ids: &[Option<ThreadId>]) {
use domain::scheduler::Scheduler;
for slot in thread_ids {
if let Some(tid) = slot {
self.endpoints.remove_thread_from_all(*tid);
self.cleanup_wait_any(*tid);
self.scheduler.remove(*tid);
self.pending_messages[tid.raw() as usize] = None;
self.pending_results[tid.raw() as usize] = None;
let _ = self.threads.destroy(*tid);
}
for tid in thread_ids.iter().flatten() {
self.endpoints.remove_thread_from_all(*tid);
self.cleanup_wait_any(*tid);
self.scheduler.remove(*tid);
self.pending_messages[tid.raw() as usize] = None;
self.pending_results[tid.raw() as usize] = None;
let _ = self.threads.destroy(*tid);
}
}
pub(crate) fn cleanup_wait_any(&mut self, tid: ThreadId) {
if let Some(wait_set) = self.wait_sets[tid.raw() as usize].take() {
for ep_slot in wait_set.endpoints() {
if let Some(_slot) = ep_slot {
self.endpoints.remove_thread_from_all(tid);
break;
}
}
if self.wait_sets[tid.raw() as usize].take().is_some() {
self.endpoints.remove_thread_from_all(tid);
}
}
@@ -312,12 +305,12 @@ impl<P: Platform> Kernel<P> {
.endpoints
.get(*id)
.map(|ep| *ep.generation())
.unwrap_or_else(|| Generation::initial()),
.unwrap_or_else(Generation::initial),
Resource::Memory(id) => self
.regions
.get(*id)
.map(|r| *r.generation())
.unwrap_or_else(|| Generation::initial()),
.unwrap_or_else(Generation::initial),
_ => Generation::initial(),
}
}

View File

@@ -42,11 +42,11 @@ impl<P: Platform> Kernel<P> {
return SyscallOutcome::Complete(SyscallResult::Error(e));
}
if let domain::resource::Resource::Memory(region_id) = &resource {
if let Some(region) = self.regions.get(*region_id) {
self.platform
.map_shared(target_pid, region.base(), region.size(), restricted);
}
if let domain::resource::Resource::Memory(region_id) = &resource
&& let Some(region) = self.regions.get(*region_id)
{
self.platform
.map_shared(target_pid, region.base(), region.size(), restricted);
}
SyscallOutcome::Complete(SyscallResult::Ok)

View File

@@ -15,34 +15,31 @@ impl<P: Platform> Kernel<P> {
};
// Try to deliver fault to a fault handler endpoint (if configured)
if let Ok(process) = self.processes.get(pid) {
if let Some(handler_slot) = process.fault_handler() {
if let Ok(cap) = process.capabilities().get(handler_slot) {
if let domain::resource::Resource::Endpoint(ep_id) = cap.resource() {
let msg = domain::message::Message::from_words([
pid.raw() as usize,
fault.code(),
fault.address(),
fault_pc,
]);
if let Ok(process) = self.processes.get(pid)
&& let Some(handler_slot) = process.fault_handler()
&& let Ok(cap) = process.capabilities().get(handler_slot)
&& let domain::resource::Resource::Endpoint(ep_id) = cap.resource()
{
let msg = domain::message::Message::from_words([
pid.raw() as usize,
fault.code(),
fault.address(),
fault_pc,
]);
if let Some(ep) = self.endpoints.get_mut(*ep_id) {
if ep.has_waiting_receiver() {
if let Some(receiver_tid) = ep.dequeue_receiver() {
self.pending_results[receiver_tid.raw() as usize] =
Some(crate::SyscallResult::Message(msg));
if let Some(ep) = self.endpoints.get_mut(*ep_id)
&& ep.has_waiting_receiver()
&& let Some(receiver_tid) = ep.dequeue_receiver()
{
self.pending_results[receiver_tid.raw() as usize] =
Some(crate::SyscallResult::Message(msg));
if let Ok(receiver) = self.threads.get_mut(receiver_tid) {
let priority = receiver.priority();
let _ = receiver.unblock();
let core = self.platform.core_id();
use domain::scheduler::Scheduler;
self.scheduler.add(receiver_tid, priority, core);
}
}
}
}
}
if let Ok(receiver) = self.threads.get_mut(receiver_tid) {
let priority = receiver.priority();
let _ = receiver.unblock();
let core = self.platform.core_id();
use domain::scheduler::Scheduler;
self.scheduler.add(receiver_tid, priority, core);
}
}
}

View File

@@ -180,7 +180,7 @@ impl<P: Platform> Kernel<P> {
message.is_some(),
"sender in wait queue had no pending message"
);
let message = message.unwrap_or_else(|| domain::message::Message::empty());
let message = message.unwrap_or_else(domain::message::Message::empty);
self.pending_results[sender_tid.raw() as usize] = Some(SyscallResult::Ok);
@@ -289,54 +289,46 @@ impl<P: Platform> Kernel<P> {
));
}
for ep_slot in sources.endpoints() {
if let Some(slot) = ep_slot {
let ep_id =
match self.check_cap(caller_pid, *slot, types::permission::Permission::Read) {
Ok(cap) => match cap.resource() {
domain::resource::Resource::Endpoint(id) => *id,
_ => continue,
},
Err(_) => continue,
};
for slot in sources.endpoints().iter().flatten() {
let ep_id = match self.check_cap(caller_pid, *slot, types::permission::Permission::Read)
{
Ok(cap) => match cap.resource() {
domain::resource::Resource::Endpoint(id) => *id,
_ => continue,
},
Err(_) => continue,
};
if let Some(ep) = self.endpoints.get_mut(ep_id) {
if ep.has_waiting_sender() {
return SyscallOutcome::Complete(SyscallResult::WakeReason(
domain::syscall::WakeReason::MessageReady(*slot),
));
}
}
if let Some(ep) = self.endpoints.get_mut(ep_id)
&& ep.has_waiting_sender()
{
return SyscallOutcome::Complete(SyscallResult::WakeReason(
domain::syscall::WakeReason::MessageReady(*slot),
));
}
}
for irq_slot in sources.interrupts() {
if let Some(slot) = irq_slot {
if let Ok(cap) =
if let Some(slot) = irq_slot
&& let Ok(cap) =
self.check_cap(caller_pid, *slot, types::permission::Permission::Read)
{
if let domain::resource::Resource::Interrupt(irq_id) = cap.resource() {
if self.irqs.owner(*irq_id).is_some() {
return SyscallOutcome::Complete(SyscallResult::WakeReason(
domain::syscall::WakeReason::InterruptFired(*slot),
));
}
}
}
&& let domain::resource::Resource::Interrupt(irq_id) = cap.resource()
&& self.irqs.owner(*irq_id).is_some()
{
return SyscallOutcome::Complete(SyscallResult::WakeReason(
domain::syscall::WakeReason::InterruptFired(*slot),
));
}
}
for ep_slot in sources.endpoints() {
if let Some(slot) = ep_slot {
if let Ok(cap) =
if let Some(slot) = ep_slot
&& let Ok(cap) =
self.check_cap(caller_pid, *slot, types::permission::Permission::Read)
{
if let domain::resource::Resource::Endpoint(id) = cap.resource() {
if let Some(ep) = self.endpoints.get_mut(*id) {
let _ = ep.enqueue_receiver(caller_tid);
}
}
}
&& let domain::resource::Resource::Endpoint(id) = cap.resource()
&& let Some(ep) = self.endpoints.get_mut(*id)
{
let _ = ep.enqueue_receiver(caller_tid);
}
}

View File

@@ -1,4 +1,3 @@
use domain::process::MAX_THREADS_PER_PROCESS;
use platform::Platform;
use super::{SyscallOutcome, SyscallResult};

View File

@@ -197,3 +197,18 @@ fn revoke_syscall_without_revoke_permission_fails() {
SyscallOutcome::Complete(SyscallResult::Error(_))
));
}
#[test]
fn every_representable_slot_is_rejected_not_panicking() {
let (mut k, _) = kernel_with_process();
for raw in 1..=CapSlot::MAX {
let outcome = k.handle_syscall(Syscall::Send {
endpoint: CapSlot::new(raw).unwrap(),
message: domain::message::Message::empty(),
});
assert!(
matches!(outcome, SyscallOutcome::Complete(SyscallResult::Error(_))),
"slot {raw} should be an error"
);
}
}

View File

@@ -1,5 +1,5 @@
use super::*;
use crate::{SyscallOutcome, SyscallResult};
use crate::SyscallOutcome;
use domain::syscall::Syscall;
#[test]
@@ -21,5 +21,5 @@ fn exit_thread_last_thread_destroys_process() {
let (mut k, pid) = kernel_with_process();
k.handle_syscall(Syscall::ExitThread);
// Process should be gone since it had only one thread
assert!(k.process_exists(pid) == false);
assert!(!k.process_exists(pid));
}

View File

@@ -65,20 +65,20 @@ impl Kernel<MockPlatform> {
if current == tid {
return;
}
if let Ok(t) = self.threads.get_mut(current) {
if matches!(t.state(), ThreadState::Running(_)) {
let priority = t.priority();
let _ = t.preempt();
self.scheduler.add(current, priority, core);
}
if let Ok(t) = self.threads.get_mut(current)
&& matches!(t.state(), ThreadState::Running(_))
{
let priority = t.priority();
let _ = t.preempt();
self.scheduler.add(current, priority, core);
}
}
if let Ok(t) = self.threads.get(tid) {
if matches!(t.state(), ThreadState::Ready) {
self.scheduler.next(core);
let _ = self.threads.get_mut(tid).map(|t| t.set_running(core));
}
if let Ok(t) = self.threads.get(tid)
&& matches!(t.state(), ThreadState::Ready)
{
self.scheduler.next(core);
let _ = self.threads.get_mut(tid).map(|t| t.set_running(core));
}
}

View File

@@ -69,7 +69,7 @@ fn send_without_receiver_blocks_caller() {
#[test]
fn receive_without_sender_blocks_caller() {
let (mut k, _, pid_b, _, cap_b, tid_b) = setup_ipc_kernel();
let (mut k, _, _pid_b, _, cap_b, tid_b) = setup_ipc_kernel();
// Switch to thread B to call Receive
k.switch_to_thread_for_test(tid_b);
@@ -83,7 +83,7 @@ fn receive_without_sender_blocks_caller() {
#[test]
fn send_with_waiting_receiver_delivers_and_switches() {
let (mut k, pid_a, pid_b, cap_a, cap_b, tid_b) = setup_ipc_kernel();
let (mut k, _pid_a, _pid_b, cap_a, cap_b, tid_b) = setup_ipc_kernel();
let tid_a = ThreadId::from_raw(0);
// Thread B calls Receive first (blocks)
@@ -108,8 +108,8 @@ fn send_with_waiting_receiver_delivers_and_switches() {
#[test]
fn receive_with_waiting_sender_delivers_immediately() {
let (mut k, pid_a, pid_b, cap_a, cap_b, tid_b) = setup_ipc_kernel();
let tid_a = ThreadId::from_raw(0);
let (mut k, _pid_a, _pid_b, cap_a, cap_b, tid_b) = setup_ipc_kernel();
let _tid_a = ThreadId::from_raw(0);
// Thread A sends first (blocks)
let msg = Message::from_words([99, 0, 0, 0]);
@@ -184,7 +184,7 @@ fn ack_interrupt_returns_ok() {
#[test]
fn wait_any_with_ready_source_returns_immediately() {
let (mut k, pid_a, _, cap_a, cap_b, tid_b) = setup_ipc_kernel();
let (mut k, _pid_a, _, cap_a, cap_b, tid_b) = setup_ipc_kernel();
let tid_a = ThreadId::from_raw(0);
// Thread B sends a message (blocks since no receiver)
@@ -206,7 +206,7 @@ fn wait_any_with_ready_source_returns_immediately() {
#[test]
fn wait_any_with_no_ready_source_blocks() {
let (mut k, pid_a, _, cap_a, _, _) = setup_ipc_kernel();
let (mut k, _pid_a, _, cap_a, _, _) = setup_ipc_kernel();
let wait_set = WaitSet::new([Some(cap_a), None, None, None], [None, None, None, None]);
let outcome = k.handle_syscall(Syscall::WaitAny { sources: wait_set });
assert!(matches!(

View File

@@ -41,7 +41,7 @@ impl MockPlatform {
impl Platform for MockPlatform {
fn configure_protection(&mut self, _process: ProcessId, _regions: &[MemoryRegion]) {}
fn enable_user_mode(&mut self, _entry: EntryPoint, _stack: StackPointer) -> ! {
loop {}
panic!("enable_user_mode is not supported in tests")
}
fn set_timer(&mut self, _duration: TimerDuration) {}
fn current_ticks(&self) -> Ticks {
@@ -65,16 +65,17 @@ impl Platform for MockPlatform {
) {
}
fn halt(&self) -> ! {
loop {}
panic!("halt")
}
fn debug_putchar(&self, _byte: u8) {}
fn translate_va(&self, process: ProcessId, va: VirtualAddress) -> Option<PhysicalAddress> {
for m in &self.mappings {
if let Some(m) = m {
if m.process.raw() == process.raw() && m.va == va.raw() {
return Some(PhysicalAddress::from_raw(m.pa));
}
if let Some(m) = m
&& m.process.raw() == process.raw()
&& m.va == va.raw()
{
return Some(PhysicalAddress::from_raw(m.pa));
}
}
None

View File

@@ -12,16 +12,16 @@ fn tick_continues_when_time_remaining() {
#[test]
fn tick_preempts_when_time_expired_and_other_thread_ready() {
let mut k = test_kernel();
let _k = test_kernel();
let mock = MockPlatform::new(1);
let pool = PagePool::new(PhysicalAddress::from_raw(0x8000_0000), 1024);
let mut k = Kernel::new(mock, pool);
// Boot with quantum=2 so it expires after 2 ticks
let (pid, tid1) = k.boot(Priority::DEFAULT, TimeSlice::new(2)).unwrap();
let (pid, _tid1) = k.boot(Priority::DEFAULT, TimeSlice::new(2)).unwrap();
// Spawn a second thread so scheduler has someone to switch to
let tid2 = k.spawn_thread_for_test(pid, Priority::DEFAULT, TimeSlice::DEFAULT);
let _tid2 = k.spawn_thread_for_test(pid, Priority::DEFAULT, TimeSlice::DEFAULT);
// First tick: time remaining
let d1 = k.handle_tick(CoreId::from_raw(0));