feat(kernel): object lifetimes via slot generations
- SlotGenerations per table, never wraps, retires exhausted slots - caps to dead/reused process, endpoint, region rejected - endpoints owned, die w/ owner, waiters woken w/ error - DropCap syscall + libkos wrapper - irq table bounds-checked, routes dropped w/ thread - self-kill terminates caller, exit_thread full cleanup - sched run_now: switch to named thread, not queue head - lifetime tests, fuzz kills/recreates, kani gen proofs
This commit is contained in:
@@ -23,7 +23,7 @@ pub fn grant_all(
|
|||||||
.with(types::permission::Permission::Execute);
|
.with(types::permission::Permission::Execute);
|
||||||
|
|
||||||
let storage_ep = kern()
|
let storage_ep = kern()
|
||||||
.create_endpoint()
|
.create_endpoint(storage_pid)
|
||||||
.unwrap_or_else(|_| crate::halt(uart, "storage endpoint failed"));
|
.unwrap_or_else(|_| crate::halt(uart, "storage endpoint failed"));
|
||||||
|
|
||||||
kern()
|
kern()
|
||||||
@@ -50,7 +50,7 @@ pub fn grant_all(
|
|||||||
.unwrap_or_else(|_| crate::halt(uart, "grant failed"));
|
.unwrap_or_else(|_| crate::halt(uart, "grant failed"));
|
||||||
|
|
||||||
let display_ep = kern()
|
let display_ep = kern()
|
||||||
.create_endpoint()
|
.create_endpoint(display_pid)
|
||||||
.unwrap_or_else(|_| crate::halt(uart, "display endpoint failed"));
|
.unwrap_or_else(|_| crate::halt(uart, "display endpoint failed"));
|
||||||
|
|
||||||
kern()
|
kern()
|
||||||
@@ -77,7 +77,7 @@ pub fn grant_all(
|
|||||||
.unwrap_or_else(|_| crate::halt(uart, "grant failed"));
|
.unwrap_or_else(|_| crate::halt(uart, "grant failed"));
|
||||||
|
|
||||||
let input_ep = kern()
|
let input_ep = kern()
|
||||||
.create_endpoint()
|
.create_endpoint(input_pid)
|
||||||
.unwrap_or_else(|_| crate::halt(uart, "input endpoint failed"));
|
.unwrap_or_else(|_| crate::halt(uart, "input endpoint failed"));
|
||||||
|
|
||||||
kern()
|
kern()
|
||||||
|
|||||||
@@ -251,6 +251,10 @@ fn decode_syscall(frame: &TrapFrame) -> Option<domain::syscall::Syscall> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
SYS_CREATE_ENDPOINT => Some(Syscall::CreateEndpoint),
|
SYS_CREATE_ENDPOINT => Some(Syscall::CreateEndpoint),
|
||||||
|
SYS_DROP_CAP => {
|
||||||
|
let slot = types::bounded::CapSlot::new(a0 as u8)?;
|
||||||
|
Some(Syscall::DropCap { slot })
|
||||||
|
}
|
||||||
SYS_DEBUG_PUTCHAR => Some(Syscall::DebugPutchar { byte: a0 as u8 }),
|
SYS_DEBUG_PUTCHAR => Some(Syscall::DebugPutchar { byte: a0 as u8 }),
|
||||||
SYS_MAP_PAGE => {
|
SYS_MAP_PAGE => {
|
||||||
let slot = types::bounded::CapSlot::new(a0 as u8)?;
|
let slot = types::bounded::CapSlot::new(a0 as u8)?;
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ fn full_table_returns_no_free_slot() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn generation_mismatch_detected() {
|
fn generation_mismatch_detected() {
|
||||||
let gen_v1 = Generation::initial();
|
let gen_v1 = Generation::initial();
|
||||||
let gen_v2 = gen_v1.next();
|
let gen_v2 = gen_v1.next().unwrap();
|
||||||
|
|
||||||
assert!(gen_v1.is_valid_against(&gen_v1));
|
assert!(gen_v1.is_valid_against(&gen_v1));
|
||||||
assert!(!gen_v1.is_valid_against(&gen_v2));
|
assert!(!gen_v1.is_valid_against(&gen_v2));
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ use types::{
|
|||||||
id::{EndpointId, ProcessId, ThreadId},
|
id::{EndpointId, ProcessId, ThreadId},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{endpoint::wait_queue::WaitQueue, error::KernelError, message::Message};
|
use crate::{
|
||||||
|
endpoint::wait_queue::WaitQueue, error::KernelError, generations::SlotGenerations,
|
||||||
|
message::Message,
|
||||||
|
};
|
||||||
|
|
||||||
pub mod wait_queue;
|
pub mod wait_queue;
|
||||||
|
|
||||||
@@ -14,18 +17,19 @@ mod tests;
|
|||||||
|
|
||||||
pub struct Endpoint {
|
pub struct Endpoint {
|
||||||
id: EndpointId,
|
id: EndpointId,
|
||||||
|
/// The process that created it; the endpoint dies with its owner.
|
||||||
|
owner: ProcessId,
|
||||||
waiting_senders: WaitQueue,
|
waiting_senders: WaitQueue,
|
||||||
waiting_receivers: WaitQueue,
|
waiting_receivers: WaitQueue,
|
||||||
generation: Generation,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Endpoint {
|
impl Endpoint {
|
||||||
pub fn new(id: EndpointId) -> Self {
|
pub fn new(id: EndpointId, owner: ProcessId) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
|
owner,
|
||||||
waiting_senders: WaitQueue::new(),
|
waiting_senders: WaitQueue::new(),
|
||||||
waiting_receivers: WaitQueue::new(),
|
waiting_receivers: WaitQueue::new(),
|
||||||
generation: Generation::initial(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,8 +37,8 @@ impl Endpoint {
|
|||||||
&self.id
|
&self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generation(&self) -> &Generation {
|
pub fn owner(&self) -> ProcessId {
|
||||||
&self.generation
|
self.owner
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn enqueue_sender(&mut self, thread: ThreadId) -> Option<()> {
|
pub fn enqueue_sender(&mut self, thread: ThreadId) -> Option<()> {
|
||||||
@@ -68,32 +72,52 @@ impl Endpoint {
|
|||||||
pub fn remove_receiver(&mut self, thread: ThreadId) -> bool {
|
pub fn remove_receiver(&mut self, thread: ThreadId) -> bool {
|
||||||
self.waiting_receivers.remove_thread(thread)
|
self.waiting_receivers.remove_thread(thread)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn revoke(&mut self) {
|
|
||||||
self.generation = self.generation.next();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_ENDPOINTS: usize = 256;
|
pub const MAX_ENDPOINTS: usize = 256;
|
||||||
|
|
||||||
pub struct EndpointTable {
|
pub struct EndpointTable {
|
||||||
endpoints: [Option<Endpoint>; MAX_ENDPOINTS],
|
endpoints: [Option<Endpoint>; MAX_ENDPOINTS],
|
||||||
|
generations: SlotGenerations<MAX_ENDPOINTS>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EndpointTable {
|
impl EndpointTable {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
endpoints: [const { None }; MAX_ENDPOINTS],
|
endpoints: [const { None }; MAX_ENDPOINTS],
|
||||||
|
generations: SlotGenerations::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create(&mut self) -> Option<EndpointId> {
|
pub fn create(&mut self, owner: ProcessId) -> Option<EndpointId> {
|
||||||
let idx = self.endpoints.iter().position(|s| s.is_none())?;
|
let idx = (0..MAX_ENDPOINTS)
|
||||||
|
.find(|&i| self.endpoints[i].is_none() && self.generations.is_usable(i))?;
|
||||||
let id = EndpointId::from_raw(idx as u16);
|
let id = EndpointId::from_raw(idx as u16);
|
||||||
self.endpoints[idx] = Some(Endpoint::new(id));
|
self.endpoints[idx] = Some(Endpoint::new(id, owner));
|
||||||
Some(id)
|
Some(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generation capabilities to a live endpoint must carry.
|
||||||
|
pub fn generation(&self, id: EndpointId) -> Option<Generation> {
|
||||||
|
self.get(id)?;
|
||||||
|
self.generations.current(id.raw() as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invalidates every existing capability to `id`; the endpoint lives on.
|
||||||
|
pub fn revoke(&mut self, id: EndpointId) {
|
||||||
|
if self.get(id).is_some() {
|
||||||
|
self.generations.advance(id.raw() as usize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ids_owned_by(&self, owner: ProcessId) -> impl Iterator<Item = EndpointId> + '_ {
|
||||||
|
self.endpoints
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.filter(move |ep| ep.owner == owner)
|
||||||
|
.map(|ep| ep.id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get(&self, id: EndpointId) -> Option<&Endpoint> {
|
pub fn get(&self, id: EndpointId) -> Option<&Endpoint> {
|
||||||
self.endpoints
|
self.endpoints
|
||||||
.get(id.raw() as usize)
|
.get(id.raw() as usize)
|
||||||
@@ -106,10 +130,12 @@ impl EndpointTable {
|
|||||||
.and_then(|s| s.as_mut())
|
.and_then(|s| s.as_mut())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Removes the endpoint; capabilities to it become invalid even if the
|
||||||
|
/// slot is later reused.
|
||||||
pub fn destroy(&mut self, id: EndpointId) -> Option<Endpoint> {
|
pub fn destroy(&mut self, id: EndpointId) -> Option<Endpoint> {
|
||||||
self.endpoints
|
let ep = self.endpoints.get_mut(id.raw() as usize)?.take()?;
|
||||||
.get_mut(id.raw() as usize)
|
self.generations.advance(id.raw() as usize);
|
||||||
.and_then(|s| s.take())
|
Some(ep)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_thread_from_all(&mut self, thread: ThreadId) {
|
pub fn remove_thread_from_all(&mut self, thread: ThreadId) {
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
use crate::endpoint::wait_queue::MAX_WAITERS;
|
use crate::endpoint::wait_queue::MAX_WAITERS;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use types::id::{EndpointId, ThreadId};
|
use types::id::{EndpointId, ProcessId, ThreadId};
|
||||||
|
|
||||||
fn thread(id: u16) -> ThreadId {
|
fn thread(id: u16) -> ThreadId {
|
||||||
ThreadId::from_raw(id)
|
ThreadId::from_raw(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn owner() -> ProcessId {
|
||||||
|
ProcessId::from_raw(0)
|
||||||
|
}
|
||||||
|
|
||||||
fn test_endpoint() -> Endpoint {
|
fn test_endpoint() -> Endpoint {
|
||||||
Endpoint::new(EndpointId::from_raw(1))
|
Endpoint::new(EndpointId::from_raw(1), owner())
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- WaitQueue tests ---
|
// --- WaitQueue tests ---
|
||||||
@@ -99,13 +103,46 @@ fn senders_and_receivers_are_independent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn revoke_bumps_generation() {
|
fn revoke_bumps_generation_but_keeps_the_endpoint() {
|
||||||
let mut ep = test_endpoint();
|
let mut table = EndpointTable::new();
|
||||||
let gen_before = *ep.generation();
|
let id = table.create(owner()).unwrap();
|
||||||
ep.revoke();
|
let gen_before = table.generation(id).unwrap();
|
||||||
let gen_after = *ep.generation();
|
table.revoke(id);
|
||||||
|
let gen_after = table.generation(id).unwrap();
|
||||||
|
|
||||||
assert!(!gen_before.is_valid_against(&gen_after));
|
assert!(!gen_before.is_valid_against(&gen_after));
|
||||||
|
assert!(table.get(id).is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reused_slot_gets_a_new_generation() {
|
||||||
|
let mut table = EndpointTable::new();
|
||||||
|
let id = table.create(owner()).unwrap();
|
||||||
|
let old = table.generation(id).unwrap();
|
||||||
|
table.destroy(id);
|
||||||
|
assert_eq!(
|
||||||
|
table.generation(id),
|
||||||
|
None,
|
||||||
|
"dead endpoints have no generation"
|
||||||
|
);
|
||||||
|
let again = table.create(owner()).unwrap();
|
||||||
|
assert_eq!(again, id);
|
||||||
|
assert!(!old.is_valid_against(&table.generation(again).unwrap()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ids_owned_by_lists_only_that_owners_endpoints() {
|
||||||
|
let mut table = EndpointTable::new();
|
||||||
|
let a = table.create(owner()).unwrap();
|
||||||
|
table.create(ProcessId::from_raw(1)).unwrap();
|
||||||
|
let c = table.create(owner()).unwrap();
|
||||||
|
let mut ids = [EndpointId::from_raw(u16::MAX); 4];
|
||||||
|
let mut n = 0;
|
||||||
|
for id in table.ids_owned_by(owner()) {
|
||||||
|
ids[n] = id;
|
||||||
|
n += 1;
|
||||||
|
}
|
||||||
|
assert_eq!(&ids[..n], &[a, c]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -141,22 +178,22 @@ fn rendezvous_receiver_first() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn table_create_and_lookup() {
|
fn table_create_and_lookup() {
|
||||||
let mut table = EndpointTable::new();
|
let mut table = EndpointTable::new();
|
||||||
let id = table.create().unwrap();
|
let id = table.create(owner()).unwrap();
|
||||||
assert!(table.get(id).is_some());
|
assert!(table.get(id).is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn table_create_returns_unique_ids() {
|
fn table_create_returns_unique_ids() {
|
||||||
let mut table = EndpointTable::new();
|
let mut table = EndpointTable::new();
|
||||||
let a = table.create().unwrap();
|
let a = table.create(owner()).unwrap();
|
||||||
let b = table.create().unwrap();
|
let b = table.create(owner()).unwrap();
|
||||||
assert_ne!(a, b);
|
assert_ne!(a, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn table_destroy_then_get_fails() {
|
fn table_destroy_then_get_fails() {
|
||||||
let mut table = EndpointTable::new();
|
let mut table = EndpointTable::new();
|
||||||
let id = table.create().unwrap();
|
let id = table.create(owner()).unwrap();
|
||||||
let removed = table.destroy(id);
|
let removed = table.destroy(id);
|
||||||
assert!(removed.is_some());
|
assert!(removed.is_some());
|
||||||
assert!(table.get(id).is_none());
|
assert!(table.get(id).is_none());
|
||||||
@@ -165,7 +202,7 @@ fn table_destroy_then_get_fails() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn table_get_mut_allows_enqueue() {
|
fn table_get_mut_allows_enqueue() {
|
||||||
let mut table = EndpointTable::new();
|
let mut table = EndpointTable::new();
|
||||||
let id = table.create().unwrap();
|
let id = table.create(owner()).unwrap();
|
||||||
table
|
table
|
||||||
.get_mut(id)
|
.get_mut(id)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -177,8 +214,8 @@ fn table_get_mut_allows_enqueue() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn table_destroy_reuses_slot() {
|
fn table_destroy_reuses_slot() {
|
||||||
let mut table = EndpointTable::new();
|
let mut table = EndpointTable::new();
|
||||||
let id = table.create().unwrap();
|
let id = table.create(owner()).unwrap();
|
||||||
table.destroy(id);
|
table.destroy(id);
|
||||||
let new_id = table.create().unwrap();
|
let new_id = table.create(owner()).unwrap();
|
||||||
assert_eq!(id, new_id);
|
assert_eq!(id, new_id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,4 +31,6 @@ pub enum KernelError {
|
|||||||
/// The target virtual range already holds a mapping.
|
/// The target virtual range already holds a mapping.
|
||||||
AddressInUse,
|
AddressInUse,
|
||||||
InvalidRegion,
|
InvalidRegion,
|
||||||
|
/// The object a capability would refer to no longer exists.
|
||||||
|
NoSuchObject,
|
||||||
}
|
}
|
||||||
|
|||||||
86
crates/domain/src/generations.rs
Normal file
86
crates/domain/src/generations.rs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
//! Per-slot generation counters shared by the kernel's object tables.
|
||||||
|
|
||||||
|
use types::generation::Generation;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "generations_tests.rs"]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
/// A slot's generation changes every time the object in it is destroyed
|
||||||
|
/// or revoked, so capabilities minted earlier stop matching. A slot whose
|
||||||
|
/// counter runs out is retired for good rather than reused.
|
||||||
|
pub struct SlotGenerations<const N: usize> {
|
||||||
|
gens: [Option<Generation>; N],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> SlotGenerations<N> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
gens: [Some(Generation::initial()); N],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current generation of `idx`, `None` if retired or out of range.
|
||||||
|
pub fn current(&self, idx: usize) -> Option<Generation> {
|
||||||
|
*self.gens.get(idx)?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_usable(&self, idx: usize) -> bool {
|
||||||
|
self.current(idx).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invalidates every capability minted for the current generation.
|
||||||
|
pub fn advance(&mut self, idx: usize) {
|
||||||
|
if let Some(g) = self.gens.get_mut(idx) {
|
||||||
|
*g = g.and_then(|g| g.next());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> Default for SlotGenerations<N> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(kani)]
|
||||||
|
mod proofs {
|
||||||
|
use super::SlotGenerations;
|
||||||
|
use types::generation::Generation;
|
||||||
|
|
||||||
|
/// However many times a slot is advanced, a generation observed earlier
|
||||||
|
/// never comes back, so a capability minted for it stays dead.
|
||||||
|
#[kani::proof]
|
||||||
|
#[kani::unwind(5)]
|
||||||
|
fn an_observed_generation_never_returns() {
|
||||||
|
let mut g: SlotGenerations<1> = SlotGenerations::new();
|
||||||
|
g.gens[0] = if kani::any() {
|
||||||
|
Some(Generation::from_raw(kani::any()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let observed = g.current(0);
|
||||||
|
let steps: u8 = kani::any();
|
||||||
|
kani::assume(steps >= 1 && steps <= 4);
|
||||||
|
for _ in 0..steps {
|
||||||
|
g.advance(0);
|
||||||
|
if let (Some(old), Some(now)) = (observed, g.current(0)) {
|
||||||
|
assert!(!old.is_valid_against(&now));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A retired slot is never usable again.
|
||||||
|
#[kani::proof]
|
||||||
|
#[kani::unwind(5)]
|
||||||
|
fn retired_slots_stay_retired() {
|
||||||
|
let mut g: SlotGenerations<1> = SlotGenerations::new();
|
||||||
|
g.gens[0] = None;
|
||||||
|
let steps: u8 = kani::any();
|
||||||
|
kani::assume(steps <= 4);
|
||||||
|
for _ in 0..steps {
|
||||||
|
g.advance(0);
|
||||||
|
}
|
||||||
|
assert!(!g.is_usable(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
36
crates/domain/src/generations_tests.rs
Normal file
36
crates/domain/src/generations_tests.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use super::SlotGenerations;
|
||||||
|
use types::generation::Generation;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn slots_start_at_the_initial_generation() {
|
||||||
|
let g: SlotGenerations<2> = SlotGenerations::new();
|
||||||
|
assert_eq!(g.current(0), Some(Generation::initial()));
|
||||||
|
assert!(g.is_usable(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn advance_changes_the_generation() {
|
||||||
|
let mut g: SlotGenerations<2> = SlotGenerations::new();
|
||||||
|
let before = g.current(0);
|
||||||
|
g.advance(0);
|
||||||
|
assert_ne!(g.current(0), before);
|
||||||
|
assert_eq!(g.current(1), before, "other slots untouched");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn out_of_range_slots_are_never_usable() {
|
||||||
|
let mut g: SlotGenerations<2> = SlotGenerations::new();
|
||||||
|
assert_eq!(g.current(2), None);
|
||||||
|
g.advance(2); // no panic
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exhausted_counter_retires_the_slot() {
|
||||||
|
let mut g: SlotGenerations<1> = SlotGenerations::new();
|
||||||
|
g.gens[0] = Some(Generation::from_raw(u32::MAX));
|
||||||
|
g.advance(0);
|
||||||
|
assert_eq!(g.current(0), None);
|
||||||
|
assert!(!g.is_usable(0));
|
||||||
|
g.advance(0);
|
||||||
|
assert_eq!(g.current(0), None, "retired slots stay retired");
|
||||||
|
}
|
||||||
@@ -20,20 +20,35 @@ impl IrqRoutingTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn register(&mut self, irq: IrqId, thread: ThreadId) -> Result<(), KernelError> {
|
pub fn register(&mut self, irq: IrqId, thread: ThreadId) -> Result<(), KernelError> {
|
||||||
let idx = irq.raw() as usize;
|
let slot = self
|
||||||
if self.owners[idx].is_some() {
|
.owners
|
||||||
|
.get_mut(irq.raw() as usize)
|
||||||
|
.ok_or(KernelError::InvalidIrq(irq))?;
|
||||||
|
if slot.is_some() {
|
||||||
return Err(KernelError::IrqAlreadyOwned(irq));
|
return Err(KernelError::IrqAlreadyOwned(irq));
|
||||||
}
|
}
|
||||||
self.owners[idx] = Some(thread);
|
*slot = Some(thread);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unregister(&mut self, irq: IrqId) {
|
pub fn unregister(&mut self, irq: IrqId) {
|
||||||
self.owners[irq.raw() as usize] = None;
|
if let Some(slot) = self.owners.get_mut(irq.raw() as usize) {
|
||||||
|
*slot = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Drops every route to `thread`, e.g. when it dies.
|
||||||
|
pub fn unregister_thread(&mut self, thread: ThreadId) {
|
||||||
|
for slot in &mut self.owners {
|
||||||
|
if *slot == Some(thread) {
|
||||||
|
*slot = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The thread an interrupt is routed to; `None` for unknown lines.
|
||||||
pub fn owner(&self, irq: IrqId) -> Option<ThreadId> {
|
pub fn owner(&self, irq: IrqId) -> Option<ThreadId> {
|
||||||
self.owners[irq.raw() as usize]
|
*self.owners.get(irq.raw() as usize)?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ pub mod capability;
|
|||||||
pub mod endpoint;
|
pub mod endpoint;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod fault;
|
pub mod fault;
|
||||||
|
pub mod generations;
|
||||||
pub mod irq;
|
pub mod irq;
|
||||||
pub mod memory;
|
pub mod memory;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::generations::SlotGenerations;
|
||||||
use types::{
|
use types::{
|
||||||
address::{PhysicalAddress, VirtualAddress},
|
address::{PhysicalAddress, VirtualAddress},
|
||||||
bounded::PageCount,
|
bounded::PageCount,
|
||||||
@@ -43,7 +44,6 @@ pub struct Region {
|
|||||||
backing: RegionBacking,
|
backing: RegionBacking,
|
||||||
base: PhysicalAddress,
|
base: PhysicalAddress,
|
||||||
pages: PageCount,
|
pages: PageCount,
|
||||||
generation: Generation,
|
|
||||||
mappings: [Option<RegionMapping>; MAX_REGION_MAPPINGS],
|
mappings: [Option<RegionMapping>; MAX_REGION_MAPPINGS],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +59,6 @@ impl Region {
|
|||||||
backing,
|
backing,
|
||||||
base,
|
base,
|
||||||
pages,
|
pages,
|
||||||
generation: Generation::initial(),
|
|
||||||
mappings: [None; MAX_REGION_MAPPINGS],
|
mappings: [None; MAX_REGION_MAPPINGS],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,14 +79,6 @@ impl Region {
|
|||||||
self.pages
|
self.pages
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generation(&self) -> &Generation {
|
|
||||||
&self.generation
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn revoke(&mut self) {
|
|
||||||
self.generation = self.generation.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mappings(&self) -> impl Iterator<Item = &RegionMapping> {
|
pub fn mappings(&self) -> impl Iterator<Item = &RegionMapping> {
|
||||||
self.mappings.iter().flatten()
|
self.mappings.iter().flatten()
|
||||||
}
|
}
|
||||||
@@ -124,23 +115,42 @@ impl Region {
|
|||||||
|
|
||||||
pub struct RegionTable {
|
pub struct RegionTable {
|
||||||
regions: [Option<Region>; MAX_REGIONS],
|
regions: [Option<Region>; MAX_REGIONS],
|
||||||
|
generations: SlotGenerations<MAX_REGIONS>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegionTable {
|
impl RegionTable {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
regions: [const { None }; MAX_REGIONS],
|
regions: [const { None }; MAX_REGIONS],
|
||||||
|
generations: SlotGenerations::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generation capabilities to a live region must carry.
|
||||||
|
pub fn generation(&self, id: RegionId) -> Option<Generation> {
|
||||||
|
self.get(id)?;
|
||||||
|
self.generations.current(id.raw() as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invalidates every existing capability to `id`; the region lives on.
|
||||||
|
pub fn revoke(&mut self, id: RegionId) {
|
||||||
|
if self.get(id).is_some() {
|
||||||
|
self.generations.advance(id.raw() as usize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn free_slot(&self) -> Option<usize> {
|
||||||
|
(0..MAX_REGIONS).find(|&i| self.regions[i].is_none() && self.generations.is_usable(i))
|
||||||
|
}
|
||||||
|
|
||||||
/// The id the next `insert` will return.
|
/// The id the next `insert` will return.
|
||||||
pub fn next_free_id(&self) -> Option<RegionId> {
|
pub fn next_free_id(&self) -> Option<RegionId> {
|
||||||
let idx = self.regions.iter().position(|s| s.is_none())?;
|
let idx = self.free_slot()?;
|
||||||
Some(RegionId::from_raw(idx as u16))
|
Some(RegionId::from_raw(idx as u16))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(&mut self, region: Region) -> Option<RegionId> {
|
pub fn insert(&mut self, region: Region) -> Option<RegionId> {
|
||||||
let idx = self.regions.iter().position(|s| s.is_none())?;
|
let idx = self.free_slot()?;
|
||||||
self.regions[idx] = Some(region);
|
self.regions[idx] = Some(region);
|
||||||
Some(RegionId::from_raw(idx as u16))
|
Some(RegionId::from_raw(idx as u16))
|
||||||
}
|
}
|
||||||
@@ -153,8 +163,12 @@ impl RegionTable {
|
|||||||
self.regions.get_mut(id.raw() as usize)?.as_mut()
|
self.regions.get_mut(id.raw() as usize)?.as_mut()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Removes the region; capabilities to it become invalid even if the
|
||||||
|
/// slot is later reused.
|
||||||
pub fn remove(&mut self, id: RegionId) -> Option<Region> {
|
pub fn remove(&mut self, id: RegionId) -> Option<Region> {
|
||||||
self.regions.get_mut(id.raw() as usize)?.take()
|
let region = self.regions.get_mut(id.raw() as usize)?.take()?;
|
||||||
|
self.generations.advance(id.raw() as usize);
|
||||||
|
Some(region)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ids_owned_by(&self, owner: ProcessId) -> impl Iterator<Item = RegionId> + '_ {
|
pub fn ids_owned_by(&self, owner: ProcessId) -> impl Iterator<Item = RegionId> + '_ {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use types::{
|
use types::{
|
||||||
bounded::CapSlot,
|
bounded::CapSlot,
|
||||||
|
generation::Generation,
|
||||||
id::{ProcessId, ThreadId},
|
id::{ProcessId, ThreadId},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{capability::CapabilityTable, error::KernelError};
|
use crate::{capability::CapabilityTable, error::KernelError, generations::SlotGenerations};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "tests.rs"]
|
#[path = "tests.rs"]
|
||||||
@@ -75,24 +76,30 @@ impl Process {
|
|||||||
|
|
||||||
pub struct ProcessTable {
|
pub struct ProcessTable {
|
||||||
processes: [Option<Process>; MAX_PROCESSES],
|
processes: [Option<Process>; MAX_PROCESSES],
|
||||||
|
generations: SlotGenerations<MAX_PROCESSES>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProcessTable {
|
impl ProcessTable {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
processes: [const { None }; MAX_PROCESSES],
|
processes: [const { None }; MAX_PROCESSES],
|
||||||
|
generations: SlotGenerations::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generation capabilities to a live process must carry.
|
||||||
|
pub fn generation(&self, id: ProcessId) -> Option<Generation> {
|
||||||
|
self.get(id).ok()?;
|
||||||
|
self.generations.current(id.raw() as usize)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn create(
|
pub fn create(
|
||||||
&mut self,
|
&mut self,
|
||||||
capabilities: CapabilityTable,
|
capabilities: CapabilityTable,
|
||||||
fault_handler: Option<CapSlot>,
|
fault_handler: Option<CapSlot>,
|
||||||
) -> Result<ProcessId, KernelError> {
|
) -> Result<ProcessId, KernelError> {
|
||||||
let idx = self
|
let idx = (0..MAX_PROCESSES)
|
||||||
.processes
|
.find(|&i| self.processes[i].is_none() && self.generations.is_usable(i))
|
||||||
.iter()
|
|
||||||
.position(|s| s.is_none())
|
|
||||||
.ok_or(KernelError::TooManyProcesses)?;
|
.ok_or(KernelError::TooManyProcesses)?;
|
||||||
|
|
||||||
let id = ProcessId::from_raw(idx as u16);
|
let id = ProcessId::from_raw(idx as u16);
|
||||||
@@ -116,12 +123,17 @@ impl ProcessTable {
|
|||||||
.ok_or(KernelError::InvalidProcess(id))
|
.ok_or(KernelError::InvalidProcess(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Removes the process; capabilities to it become invalid even if the
|
||||||
|
/// pid is later reused.
|
||||||
pub fn destroy(&mut self, id: ProcessId) -> Result<Process, KernelError> {
|
pub fn destroy(&mut self, id: ProcessId) -> Result<Process, KernelError> {
|
||||||
let idx = id.raw() as usize;
|
let idx = id.raw() as usize;
|
||||||
self.processes
|
let process = self
|
||||||
|
.processes
|
||||||
.get_mut(idx)
|
.get_mut(idx)
|
||||||
.and_then(|s| s.take())
|
.and_then(|s| s.take())
|
||||||
.ok_or(KernelError::InvalidProcess(id))
|
.ok_or(KernelError::InvalidProcess(id))?;
|
||||||
|
self.generations.advance(idx);
|
||||||
|
Ok(process)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,14 @@ impl MultiCoreScheduler {
|
|||||||
self.cores[core.raw() as usize].set_current(thread);
|
self.cores[core.raw() as usize].set_current(thread);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs `thread` next on `core` regardless of its place in the queue.
|
||||||
|
/// Fails if it is not queued there.
|
||||||
|
pub fn run_now(&mut self, core: CoreId, thread: ThreadId) -> bool {
|
||||||
|
self.cores
|
||||||
|
.get_mut(core.raw() as usize)
|
||||||
|
.is_some_and(|c| c.run_now(thread))
|
||||||
|
}
|
||||||
|
|
||||||
fn thread_count_on(&self, core: CoreId) -> usize {
|
fn thread_count_on(&self, core: CoreId) -> usize {
|
||||||
self.placements
|
self.placements
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -39,6 +39,15 @@ impl PerCoreScheduler {
|
|||||||
Some(thread)
|
Some(thread)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Takes `thread` out of the run queue and makes it current.
|
||||||
|
pub fn run_now(&mut self, thread: ThreadId) -> bool {
|
||||||
|
if !self.queue.remove(thread) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.current = Some(thread);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clear_current(&mut self) {
|
pub fn clear_current(&mut self) {
|
||||||
self.current = None;
|
self.current = None;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -284,3 +284,26 @@ fn removed_thread_is_skipped_and_order_kept() {
|
|||||||
assert_eq!(q.pop_highest().unwrap().0, thread(3));
|
assert_eq!(q.pop_highest().unwrap().0, thread(3));
|
||||||
assert!(q.pop_highest().is_none());
|
assert!(q.pop_highest().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn run_now_picks_the_named_thread_not_the_queue_head() {
|
||||||
|
let mut s = MultiCoreScheduler::new(1);
|
||||||
|
s.add(thread(1), prio(10), core(0));
|
||||||
|
s.add(thread(2), prio(10), core(0));
|
||||||
|
s.add(thread(3), prio(10), core(0));
|
||||||
|
|
||||||
|
assert!(s.run_now(core(0), thread(2)));
|
||||||
|
assert_eq!(s.current(core(0)), Some(thread(2)));
|
||||||
|
// The others keep their order; thread 2 is no longer queued.
|
||||||
|
assert_eq!(s.next(core(0)), Some(thread(1)));
|
||||||
|
assert_eq!(s.next(core(0)), Some(thread(3)));
|
||||||
|
assert_eq!(s.next(core(0)), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn run_now_rejects_a_thread_that_is_not_queued() {
|
||||||
|
let mut s = MultiCoreScheduler::new(1);
|
||||||
|
s.add(thread(1), prio(10), core(0));
|
||||||
|
assert!(!s.run_now(core(0), thread(9)));
|
||||||
|
assert_eq!(s.current(core(0)), None);
|
||||||
|
}
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ pub enum Syscall {
|
|||||||
Revoke {
|
Revoke {
|
||||||
slot: CapSlot,
|
slot: CapSlot,
|
||||||
},
|
},
|
||||||
|
/// Removes a capability from the caller's own table.
|
||||||
|
DropCap {
|
||||||
|
slot: CapSlot,
|
||||||
|
},
|
||||||
SpawnThread {
|
SpawnThread {
|
||||||
entry: EntryPoint,
|
entry: EntryPoint,
|
||||||
stack: StackPointer,
|
stack: StackPointer,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub struct Thread {
|
|||||||
core_affinity: Option<CoreId>,
|
core_affinity: Option<CoreId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ThreadState {
|
pub enum ThreadState {
|
||||||
Ready,
|
Ready,
|
||||||
Running(CoreId),
|
Running(CoreId),
|
||||||
@@ -77,6 +78,7 @@ impl SavedContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum BlockReason {
|
pub enum BlockReason {
|
||||||
Sending(CapSlot),
|
Sending(CapSlot),
|
||||||
Receiving(CapSlot),
|
Receiving(CapSlot),
|
||||||
|
|||||||
@@ -205,8 +205,13 @@ impl<P: Platform> Kernel<P> {
|
|||||||
Some(tid)
|
Some(tid)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_endpoint(&mut self) -> Result<types::id::EndpointId, KernelError> {
|
/// Creates an endpoint owned by `owner`; it is destroyed with `owner`.
|
||||||
self.endpoints.create().ok_or(KernelError::NoFreeSlot)
|
pub fn create_endpoint(
|
||||||
|
&mut self,
|
||||||
|
owner: ProcessId,
|
||||||
|
) -> Result<types::id::EndpointId, KernelError> {
|
||||||
|
self.processes.get(owner)?;
|
||||||
|
self.endpoints.create(owner).ok_or(KernelError::NoFreeSlot)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn grant_capability(
|
pub fn grant_capability(
|
||||||
@@ -237,8 +242,8 @@ impl<P: Platform> Kernel<P> {
|
|||||||
|
|
||||||
if let Ok(t) = self.threads.get(tid)
|
if let Ok(t) = self.threads.get(tid)
|
||||||
&& matches!(t.state(), ThreadState::Ready)
|
&& matches!(t.state(), ThreadState::Ready)
|
||||||
|
&& self.scheduler.run_now(core, tid)
|
||||||
{
|
{
|
||||||
self.scheduler.next(core);
|
|
||||||
let _ = self.threads.get_mut(tid).map(|t| t.set_running(core));
|
let _ = self.threads.get_mut(tid).map(|t| t.set_running(core));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,16 +279,9 @@ impl<P: Platform> Kernel<P> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.cleanup_threads(&thread_ids);
|
self.cleanup_threads(&thread_ids);
|
||||||
|
self.destroy_endpoints_owned_by(pid);
|
||||||
self.teardown_memory(pid);
|
self.teardown_memory(pid);
|
||||||
|
|
||||||
// Unblock threads in other processes that were blocked waiting
|
|
||||||
// on endpoints where this process's threads were the other side.
|
|
||||||
// The cleanup_threads call above already removed this process's
|
|
||||||
// threads from all endpoint wait queues. Threads blocked waiting
|
|
||||||
// for this process will eventually time out or be rescheduled
|
|
||||||
// when the endpoint is revoked.
|
|
||||||
|
|
||||||
let _ = self.processes.destroy(pid);
|
let _ = self.processes.destroy(pid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,6 +290,7 @@ impl<P: Platform> Kernel<P> {
|
|||||||
for tid in thread_ids.iter().flatten() {
|
for tid in thread_ids.iter().flatten() {
|
||||||
self.endpoints.remove_thread_from_all(*tid);
|
self.endpoints.remove_thread_from_all(*tid);
|
||||||
self.cleanup_wait_any(*tid);
|
self.cleanup_wait_any(*tid);
|
||||||
|
self.irqs.unregister_thread(*tid);
|
||||||
self.scheduler.remove(*tid);
|
self.scheduler.remove(*tid);
|
||||||
self.pending_messages[tid.raw() as usize] = None;
|
self.pending_messages[tid.raw() as usize] = None;
|
||||||
self.pending_results[tid.raw() as usize] = None;
|
self.pending_results[tid.raw() as usize] = None;
|
||||||
@@ -299,6 +298,41 @@ impl<P: Platform> Kernel<P> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Destroys every endpoint `owner` created, waking anyone blocked on
|
||||||
|
/// them with an error instead of leaving them blocked forever.
|
||||||
|
fn destroy_endpoints_owned_by(&mut self, owner: ProcessId) {
|
||||||
|
let mut owned = [None; domain::endpoint::MAX_ENDPOINTS];
|
||||||
|
for (slot, id) in owned.iter_mut().zip(self.endpoints.ids_owned_by(owner)) {
|
||||||
|
*slot = Some(id);
|
||||||
|
}
|
||||||
|
for id in owned.into_iter().flatten() {
|
||||||
|
let Some(mut ep) = self.endpoints.destroy(id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
while let Some(tid) = ep.dequeue_sender().or_else(|| ep.dequeue_receiver()) {
|
||||||
|
self.wake_with_error(tid, KernelError::InvalidEndpoint(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes a blocked thread runnable with `error` as its syscall result.
|
||||||
|
fn wake_with_error(&mut self, tid: ThreadId, error: KernelError) {
|
||||||
|
self.cleanup_wait_any(tid);
|
||||||
|
self.pending_messages[tid.raw() as usize] = None;
|
||||||
|
let Ok(thread) = self.threads.get_mut(tid) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if !matches!(thread.state(), ThreadState::Blocked(_)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let priority = thread.priority();
|
||||||
|
if thread.unblock().is_ok() {
|
||||||
|
self.pending_results[tid.raw() as usize] = Some(SyscallResult::Error(error));
|
||||||
|
let core = self.platform.core_id();
|
||||||
|
self.scheduler.add(tid, priority, core);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn cleanup_wait_any(&mut self, tid: ThreadId) {
|
pub(crate) fn cleanup_wait_any(&mut self, tid: ThreadId) {
|
||||||
if self.wait_sets[tid.raw() as usize].take().is_some() {
|
if self.wait_sets[tid.raw() as usize].take().is_some() {
|
||||||
self.endpoints.remove_thread_from_all(tid);
|
self.endpoints.remove_thread_from_all(tid);
|
||||||
@@ -316,41 +350,27 @@ impl<P: Platform> Kernel<P> {
|
|||||||
if !cap.permissions().allows(needed) {
|
if !cap.permissions().allows(needed) {
|
||||||
return Err(KernelError::PermissionDenied { slot, needed });
|
return Err(KernelError::PermissionDenied { slot, needed });
|
||||||
}
|
}
|
||||||
let current_gen = self.resource_generation(cap.resource());
|
match self.resource_generation(cap.resource()) {
|
||||||
if !cap.generation().is_valid_against(¤t_gen) {
|
Some(current) if cap.generation().is_valid_against(¤t) => Ok(cap),
|
||||||
return Err(KernelError::RevokedCapability(slot));
|
_ => Err(KernelError::RevokedCapability(slot)),
|
||||||
}
|
}
|
||||||
Ok(cap)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resource_generation(&self, resource: &Resource) -> Generation {
|
/// The generation a capability to `resource` must carry, or `None` if
|
||||||
|
/// the object no longer exists.
|
||||||
|
fn resource_generation(&self, resource: &Resource) -> Option<Generation> {
|
||||||
match resource {
|
match resource {
|
||||||
Resource::Endpoint(id) => self
|
Resource::Endpoint(id) => self.endpoints.generation(*id),
|
||||||
.endpoints
|
Resource::Memory(id) => self.regions.generation(*id),
|
||||||
.get(*id)
|
Resource::Process(pid) => self.processes.generation(*pid),
|
||||||
.map(|ep| *ep.generation())
|
Resource::Interrupt(_) | Resource::ProcessCreate => Some(Generation::initial()),
|
||||||
.unwrap_or_else(Generation::initial),
|
|
||||||
Resource::Memory(id) => self
|
|
||||||
.regions
|
|
||||||
.get(*id)
|
|
||||||
.map(|r| *r.generation())
|
|
||||||
.unwrap_or_else(Generation::initial),
|
|
||||||
_ => Generation::initial(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn revoke_resource(&mut self, resource: &Resource) {
|
pub fn revoke_resource(&mut self, resource: &Resource) {
|
||||||
match resource {
|
match resource {
|
||||||
Resource::Endpoint(id) => {
|
Resource::Endpoint(id) => self.endpoints.revoke(*id),
|
||||||
if let Some(ep) = self.endpoints.get_mut(*id) {
|
Resource::Memory(id) => self.regions.revoke(*id),
|
||||||
ep.revoke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Resource::Memory(id) => {
|
|
||||||
if let Some(r) = self.regions.get_mut(*id) {
|
|
||||||
r.revoke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -361,7 +381,9 @@ impl<P: Platform> Kernel<P> {
|
|||||||
resource: Resource,
|
resource: Resource,
|
||||||
permissions: PermissionSet,
|
permissions: PermissionSet,
|
||||||
) -> Result<CapSlot, KernelError> {
|
) -> Result<CapSlot, KernelError> {
|
||||||
let generation = self.resource_generation(&resource);
|
let generation = self
|
||||||
|
.resource_generation(&resource)
|
||||||
|
.ok_or(KernelError::NoSuchObject)?;
|
||||||
let cap = Capability::new(resource, permissions, generation);
|
let cap = Capability::new(resource, permissions, generation);
|
||||||
self.processes.get_mut(to)?.capabilities_mut().insert(cap)
|
self.processes.get_mut(to)?.capabilities_mut().insert(cap)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use super::{arbitrary_endpoint_cap, insert_cap};
|
use super::{arbitrary_endpoint_cap, insert_cap};
|
||||||
use domain::capability::{Capability, CapabilityTable};
|
use domain::capability::{Capability, CapabilityTable};
|
||||||
use domain::endpoint::Endpoint;
|
|
||||||
use domain::resource::Resource;
|
use domain::resource::Resource;
|
||||||
use types::generation::Generation;
|
use types::generation::Generation;
|
||||||
use types::id::EndpointId;
|
use types::id::EndpointId;
|
||||||
@@ -62,89 +61,3 @@ fn grant_cap_preserves_resource_identity() {
|
|||||||
_ => panic!("resource type changed"),
|
_ => panic!("resource type changed"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[kani::proof]
|
|
||||||
fn generation_revocation_invalidates_all_caps() {
|
|
||||||
let bits: u8 = kani::any();
|
|
||||||
let ep_id: u16 = kani::any();
|
|
||||||
kani::assume(ep_id < 256);
|
|
||||||
|
|
||||||
let mut endpoint = Endpoint::new(EndpointId::from_raw(ep_id));
|
|
||||||
let original_gen = *endpoint.generation();
|
|
||||||
|
|
||||||
let cap = Capability::new(
|
|
||||||
Resource::Endpoint(EndpointId::from_raw(ep_id)),
|
|
||||||
PermissionSet::from_bits(bits),
|
|
||||||
original_gen,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut table = CapabilityTable::new();
|
|
||||||
let slot = insert_cap(&mut table, cap);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
table
|
|
||||||
.get(slot)
|
|
||||||
.unwrap()
|
|
||||||
.generation()
|
|
||||||
.is_valid_against(&original_gen)
|
|
||||||
);
|
|
||||||
|
|
||||||
endpoint.revoke();
|
|
||||||
let new_gen = *endpoint.generation();
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
!table
|
|
||||||
.get(slot)
|
|
||||||
.unwrap()
|
|
||||||
.generation()
|
|
||||||
.is_valid_against(&new_gen)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[kani::proof]
|
|
||||||
fn cap_created_after_revoke_is_valid() {
|
|
||||||
let ep_id: u16 = kani::any();
|
|
||||||
kani::assume(ep_id < 256);
|
|
||||||
let bits: u8 = kani::any();
|
|
||||||
|
|
||||||
let mut endpoint = Endpoint::new(EndpointId::from_raw(ep_id));
|
|
||||||
endpoint.revoke();
|
|
||||||
let current_gen = *endpoint.generation();
|
|
||||||
|
|
||||||
let cap = Capability::new(
|
|
||||||
Resource::Endpoint(EndpointId::from_raw(ep_id)),
|
|
||||||
PermissionSet::from_bits(bits),
|
|
||||||
current_gen,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(cap.generation().is_valid_against(¤t_gen));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[kani::proof]
|
|
||||||
fn multiple_revocations_invalidate_all_prior_caps() {
|
|
||||||
let ep_id: u16 = kani::any();
|
|
||||||
kani::assume(ep_id < 256);
|
|
||||||
|
|
||||||
let mut endpoint = Endpoint::new(EndpointId::from_raw(ep_id));
|
|
||||||
|
|
||||||
let gen0 = *endpoint.generation();
|
|
||||||
let cap0 = Capability::new(
|
|
||||||
Resource::Endpoint(EndpointId::from_raw(ep_id)),
|
|
||||||
PermissionSet::empty().with(Permission::Read),
|
|
||||||
gen0,
|
|
||||||
);
|
|
||||||
|
|
||||||
endpoint.revoke();
|
|
||||||
let gen1 = *endpoint.generation();
|
|
||||||
let cap1 = Capability::new(
|
|
||||||
Resource::Endpoint(EndpointId::from_raw(ep_id)),
|
|
||||||
PermissionSet::empty().with(Permission::Read),
|
|
||||||
gen1,
|
|
||||||
);
|
|
||||||
|
|
||||||
endpoint.revoke();
|
|
||||||
let gen2 = *endpoint.generation();
|
|
||||||
|
|
||||||
assert!(!cap0.generation().is_valid_against(&gen2));
|
|
||||||
assert!(!cap1.generation().is_valid_against(&gen2));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -59,4 +59,19 @@ impl<P: Platform> Kernel<P> {
|
|||||||
self.revoke_resource(&resource);
|
self.revoke_resource(&resource);
|
||||||
SyscallOutcome::Complete(SyscallResult::Ok)
|
SyscallOutcome::Complete(SyscallResult::Ok)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn handle_drop_cap(
|
||||||
|
&mut self,
|
||||||
|
caller_pid: types::id::ProcessId,
|
||||||
|
slot: types::bounded::CapSlot,
|
||||||
|
) -> SyscallOutcome {
|
||||||
|
let result = self
|
||||||
|
.processes
|
||||||
|
.get_mut(caller_pid)
|
||||||
|
.and_then(|p| p.capabilities_mut().remove(slot));
|
||||||
|
SyscallOutcome::Complete(match result {
|
||||||
|
Ok(()) => SyscallResult::Ok,
|
||||||
|
Err(e) => SyscallResult::Error(e),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ impl<P: Platform> Kernel<P> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
caller_pid: types::id::ProcessId,
|
caller_pid: types::id::ProcessId,
|
||||||
) -> SyscallOutcome {
|
) -> SyscallOutcome {
|
||||||
let ep_id = match self.create_endpoint() {
|
let ep_id = match self.create_endpoint(caller_pid) {
|
||||||
Ok(id) => id,
|
Ok(id) => id,
|
||||||
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
|
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -159,6 +159,10 @@ impl<P: Platform> Kernel<P> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.destroy_process(target_pid);
|
self.destroy_process(target_pid);
|
||||||
|
if target_pid == caller_pid {
|
||||||
|
// The caller no longer exists; nothing to return to.
|
||||||
|
return SyscallOutcome::TerminateCaller;
|
||||||
|
}
|
||||||
SyscallOutcome::Complete(SyscallResult::Ok)
|
SyscallOutcome::Complete(SyscallResult::Ok)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ impl<P: Platform> Kernel<P> {
|
|||||||
}
|
}
|
||||||
Syscall::Kill { process } => self.handle_kill(pid, process),
|
Syscall::Kill { process } => self.handle_kill(pid, process),
|
||||||
Syscall::Revoke { slot } => self.handle_revoke(pid, slot),
|
Syscall::Revoke { slot } => self.handle_revoke(pid, slot),
|
||||||
|
Syscall::DropCap { slot } => self.handle_drop_cap(pid, slot),
|
||||||
Syscall::Delegate {
|
Syscall::Delegate {
|
||||||
slot,
|
slot,
|
||||||
to_endpoint,
|
to_endpoint,
|
||||||
|
|||||||
@@ -70,10 +70,7 @@ impl<P: Platform> Kernel<P> {
|
|||||||
caller_tid: types::id::ThreadId,
|
caller_tid: types::id::ThreadId,
|
||||||
pid: types::id::ProcessId,
|
pid: types::id::ProcessId,
|
||||||
) -> SyscallOutcome {
|
) -> SyscallOutcome {
|
||||||
use domain::scheduler::Scheduler;
|
self.cleanup_threads(&[Some(caller_tid)]);
|
||||||
|
|
||||||
self.scheduler.remove(caller_tid);
|
|
||||||
let _ = self.threads.destroy(caller_tid);
|
|
||||||
|
|
||||||
if let Ok(process) = self.processes.get_mut(pid) {
|
if let Ok(process) = self.processes.get_mut(pid) {
|
||||||
process.remove_thread(caller_tid);
|
process.remove_thread(caller_tid);
|
||||||
|
|||||||
@@ -74,14 +74,16 @@ impl Kernel<MockPlatform> {
|
|||||||
|
|
||||||
if let Ok(t) = self.threads.get(tid)
|
if let Ok(t) = self.threads.get(tid)
|
||||||
&& matches!(t.state(), ThreadState::Ready)
|
&& matches!(t.state(), ThreadState::Ready)
|
||||||
|
&& self.scheduler.run_now(core, tid)
|
||||||
{
|
{
|
||||||
self.scheduler.next(core);
|
|
||||||
let _ = self.threads.get_mut(tid).map(|t| t.set_running(core));
|
let _ = self.threads.get_mut(tid).map(|t| t.set_running(core));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_endpoint_for_test(&mut self) -> EndpointId {
|
pub fn create_endpoint_for_test(&mut self) -> EndpointId {
|
||||||
self.endpoints.create().expect("create endpoint")
|
self.endpoints
|
||||||
|
.create(ProcessId::from_raw(0))
|
||||||
|
.expect("create endpoint")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn grant_cap_for_test(
|
pub fn grant_cap_for_test(
|
||||||
@@ -93,4 +95,40 @@ impl Kernel<MockPlatform> {
|
|||||||
self.grant_cap(pid, resource, permissions)
|
self.grant_cap(pid, resource, permissions)
|
||||||
.expect("grant cap")
|
.expect("grant cap")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn endpoint_of_cap_for_test(&self, pid: ProcessId, slot: CapSlot) -> EndpointId {
|
||||||
|
match self
|
||||||
|
.processes
|
||||||
|
.get(pid)
|
||||||
|
.unwrap()
|
||||||
|
.capabilities()
|
||||||
|
.get(slot)
|
||||||
|
.unwrap()
|
||||||
|
.resource()
|
||||||
|
{
|
||||||
|
Resource::Endpoint(id) => *id,
|
||||||
|
_ => panic!("not an endpoint cap"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The process a Process cap in init's table points at.
|
||||||
|
pub fn process_of_cap_for_test(&self, slot: CapSlot) -> ProcessId {
|
||||||
|
let init = ProcessId::from_raw(0);
|
||||||
|
match self
|
||||||
|
.processes
|
||||||
|
.get(init)
|
||||||
|
.unwrap()
|
||||||
|
.capabilities()
|
||||||
|
.get(slot)
|
||||||
|
.unwrap()
|
||||||
|
.resource()
|
||||||
|
{
|
||||||
|
Resource::Process(pid) => *pid,
|
||||||
|
_ => panic!("not a process cap"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn thread_state_for_test(&self, tid: ThreadId) -> Option<ThreadState> {
|
||||||
|
self.threads.get(tid).ok().map(|t| *t.state())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ struct World {
|
|||||||
caps: [CapSlot; PROCS],
|
caps: [CapSlot; PROCS],
|
||||||
known: [[usize; 64]; PROCS],
|
known: [[usize; 64]; PROCS],
|
||||||
known_len: [usize; PROCS],
|
known_len: [usize; PROCS],
|
||||||
|
rebirths: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl World {
|
impl World {
|
||||||
@@ -80,6 +81,7 @@ impl World {
|
|||||||
caps,
|
caps,
|
||||||
known: [[0; 64]; PROCS],
|
known: [[0; 64]; PROCS],
|
||||||
known_len: [0; PROCS],
|
known_len: [0; PROCS],
|
||||||
|
rebirths: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,14 +154,46 @@ impl World {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
8 if who == 0 && rng.below(200) == 0 => {
|
8 if who == 0 && rng.below(8) == 0 => {
|
||||||
let victim = 1 + rng.below(PROCS - 1);
|
let victim = 1 + rng.below(PROCS - 1);
|
||||||
if self.alive[victim] {
|
let stale = self.caps[victim];
|
||||||
self.k.handle_syscall(Syscall::Kill {
|
self.k.handle_syscall(Syscall::Kill { process: stale });
|
||||||
process: self.caps[victim],
|
self.rebirths += 1;
|
||||||
|
|
||||||
|
// Re-create it; the pid slot is reused.
|
||||||
|
let fresh = match self.k.handle_syscall(Syscall::CreateProcess {
|
||||||
|
create_cap: CapSlot::new(0).unwrap(),
|
||||||
|
image: DataRef::empty(),
|
||||||
|
}) {
|
||||||
|
SyscallOutcome::Complete(SyscallResult::Slot(s)) => s,
|
||||||
|
_ => panic!("re-create process"),
|
||||||
|
};
|
||||||
|
assert_eq!(self.k.process_of_cap_for_test(fresh), self.pids[victim]);
|
||||||
|
self.tids[victim] = self.k.spawn_thread_for_test(
|
||||||
|
self.pids[victim],
|
||||||
|
Priority::DEFAULT,
|
||||||
|
TimeSlice::DEFAULT,
|
||||||
|
);
|
||||||
|
self.known_len[victim] = 0;
|
||||||
|
|
||||||
|
// The old cap names the same pid but must reach nothing.
|
||||||
|
let src = rng.va(known);
|
||||||
|
let outcome = self.k.handle_syscall(Syscall::MapPage {
|
||||||
|
process: stale,
|
||||||
|
src_va: VirtualAddress::from_raw(src),
|
||||||
|
dst_va: VirtualAddress::from_raw(0x10000),
|
||||||
|
executable: false,
|
||||||
});
|
});
|
||||||
self.alive[victim] = false;
|
assert!(
|
||||||
}
|
matches!(outcome, SyscallOutcome::Complete(SyscallResult::Error(_))),
|
||||||
|
"stale process cap still works"
|
||||||
|
);
|
||||||
|
assert!(matches!(
|
||||||
|
self.k.handle_syscall(Syscall::Kill { process: stale }),
|
||||||
|
SyscallOutcome::Complete(SyscallResult::Error(_))
|
||||||
|
));
|
||||||
|
self.k.handle_syscall(Syscall::DropCap { slot: stale });
|
||||||
|
self.caps[victim] = fresh;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -196,7 +230,7 @@ impl World {
|
|||||||
frames_owned + region_pages,
|
frames_owned + region_pages,
|
||||||
"quota of {pid:?} drifted"
|
"quota of {pid:?} drifted"
|
||||||
);
|
);
|
||||||
if !self.alive[i] {
|
if !self.alive[i] && i != 0 {
|
||||||
assert_eq!(k.page_usage(pid), 0);
|
assert_eq!(k.page_usage(pid), 0);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
k.platform.mapping_count(pid),
|
k.platform.mapping_count(pid),
|
||||||
@@ -257,5 +291,9 @@ fn memory_model_holds_under_random_hostile_operations() {
|
|||||||
world.step(&mut rng);
|
world.step(&mut rng);
|
||||||
world.check();
|
world.check();
|
||||||
}
|
}
|
||||||
|
assert!(
|
||||||
|
world.rebirths >= 5,
|
||||||
|
"seed {seed:#x} barely exercised kill/re-create"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
255
crates/kernel/src/tests/lifetimes.rs
Normal file
255
crates/kernel/src/tests/lifetimes.rs
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
//! Object lifetimes: what happens to capabilities, blocked threads and
|
||||||
|
//! routing entries when the object they refer to dies and its id is reused.
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::{SyscallOutcome, SyscallResult};
|
||||||
|
use domain::message::Message;
|
||||||
|
use domain::resource::Resource;
|
||||||
|
use domain::syscall::Syscall;
|
||||||
|
use domain::thread::{BlockReason, ThreadState};
|
||||||
|
use types::address::DataRef;
|
||||||
|
use types::bounded::CapSlot;
|
||||||
|
use types::id::{IrqId, ThreadId};
|
||||||
|
use types::permission::{Permission, PermissionSet};
|
||||||
|
|
||||||
|
fn create_process(k: &mut Kernel<MockPlatform>) -> CapSlot {
|
||||||
|
match k.handle_syscall(Syscall::CreateProcess {
|
||||||
|
create_cap: CapSlot::new(0).unwrap(),
|
||||||
|
image: DataRef::empty(),
|
||||||
|
}) {
|
||||||
|
SyscallOutcome::Complete(SyscallResult::Slot(s)) => s,
|
||||||
|
_ => panic!("create process"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_error(outcome: &SyscallOutcome) -> bool {
|
||||||
|
matches!(outcome, SyscallOutcome::Complete(SyscallResult::Error(_)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ipc_perms() -> PermissionSet {
|
||||||
|
PermissionSet::empty()
|
||||||
|
.with(Permission::Read)
|
||||||
|
.with(Permission::Execute)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process B (pid 1) with one thread that has created an endpoint.
|
||||||
|
/// Returns (B's pid, B's thread, the endpoint, A's cap to B).
|
||||||
|
fn b_with_endpoint(k: &mut Kernel<MockPlatform>) -> (ProcessId, ThreadId, EndpointRef, CapSlot) {
|
||||||
|
let tid_a = k.current_thread().unwrap();
|
||||||
|
let cap_b = create_process(k);
|
||||||
|
let pid_b = ProcessId::from_raw(1);
|
||||||
|
let tid_b = k.spawn_thread_for_test(pid_b, Priority::DEFAULT, TimeSlice::DEFAULT);
|
||||||
|
k.switch_to_thread_for_test(tid_b);
|
||||||
|
let ep_slot_in_b = match k.handle_syscall(Syscall::CreateEndpoint) {
|
||||||
|
SyscallOutcome::Complete(SyscallResult::Slot(s)) => s,
|
||||||
|
_ => panic!("create endpoint"),
|
||||||
|
};
|
||||||
|
let ep = k.endpoint_of_cap_for_test(pid_b, ep_slot_in_b);
|
||||||
|
k.switch_to_thread_for_test(tid_a);
|
||||||
|
(pid_b, tid_b, ep, cap_b)
|
||||||
|
}
|
||||||
|
|
||||||
|
type EndpointRef = types::id::EndpointId;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stale_process_cap_cannot_reach_a_process_that_reused_the_pid() {
|
||||||
|
let (mut k, _) = kernel_with_process();
|
||||||
|
let old_cap = create_process(&mut k);
|
||||||
|
assert!(!is_error(
|
||||||
|
&k.handle_syscall(Syscall::Kill { process: old_cap })
|
||||||
|
));
|
||||||
|
|
||||||
|
let _new_cap = create_process(&mut k); // reuses pid 1
|
||||||
|
assert!(k.process_exists(ProcessId::from_raw(1)));
|
||||||
|
|
||||||
|
assert!(is_error(
|
||||||
|
&k.handle_syscall(Syscall::Kill { process: old_cap })
|
||||||
|
));
|
||||||
|
assert!(
|
||||||
|
k.process_exists(ProcessId::from_raw(1)),
|
||||||
|
"stale cap killed the new process"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn endpoints_die_with_their_creator() {
|
||||||
|
let (mut k, pid_a) = kernel_with_process();
|
||||||
|
let (_, _, ep, cap_b) = b_with_endpoint(&mut k);
|
||||||
|
let a_cap = k.grant_cap_for_test(pid_a, Resource::Endpoint(ep), ipc_perms());
|
||||||
|
|
||||||
|
k.handle_syscall(Syscall::Kill { process: cap_b });
|
||||||
|
|
||||||
|
let outcome = k.handle_syscall(Syscall::Send {
|
||||||
|
endpoint: a_cap,
|
||||||
|
message: Message::empty(),
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
is_error(&outcome),
|
||||||
|
"send to a dead process's endpoint must fail, not block"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stale_endpoint_cap_does_not_reach_a_reused_endpoint_slot() {
|
||||||
|
let (mut k, pid_a) = kernel_with_process();
|
||||||
|
let (_, _, ep, cap_b) = b_with_endpoint(&mut k);
|
||||||
|
let stale = k.grant_cap_for_test(pid_a, Resource::Endpoint(ep), ipc_perms());
|
||||||
|
k.handle_syscall(Syscall::Kill { process: cap_b });
|
||||||
|
|
||||||
|
// A new endpoint lands in the freed slot.
|
||||||
|
let fresh = match k.handle_syscall(Syscall::CreateEndpoint) {
|
||||||
|
SyscallOutcome::Complete(SyscallResult::Slot(s)) => s,
|
||||||
|
_ => panic!("create endpoint"),
|
||||||
|
};
|
||||||
|
assert_eq!(k.endpoint_of_cap_for_test(pid_a, fresh), ep);
|
||||||
|
|
||||||
|
let outcome = k.handle_syscall(Syscall::Send {
|
||||||
|
endpoint: stale,
|
||||||
|
message: Message::empty(),
|
||||||
|
});
|
||||||
|
assert!(is_error(&outcome));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn receiver_blocked_on_a_dying_endpoint_is_woken_with_an_error() {
|
||||||
|
let (mut k, pid_a) = kernel_with_process();
|
||||||
|
let tid_a = k.current_thread().unwrap();
|
||||||
|
let (pid_b, _, ep, _) = b_with_endpoint(&mut k);
|
||||||
|
let a_cap = k.grant_cap_for_test(pid_a, Resource::Endpoint(ep), ipc_perms());
|
||||||
|
|
||||||
|
let outcome = k.handle_syscall(Syscall::Receive { endpoint: a_cap });
|
||||||
|
assert!(matches!(outcome, SyscallOutcome::BlockCaller(_)));
|
||||||
|
|
||||||
|
k.destroy_process(pid_b);
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
k.thread_state_for_test(tid_a),
|
||||||
|
Some(ThreadState::Ready)
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
k.pending_result(tid_a),
|
||||||
|
Some(SyscallResult::Error(_))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sender_blocked_on_a_dying_endpoint_is_woken_with_an_error() {
|
||||||
|
let (mut k, pid_a) = kernel_with_process();
|
||||||
|
let tid_a = k.current_thread().unwrap();
|
||||||
|
let (pid_b, _, ep, _) = b_with_endpoint(&mut k);
|
||||||
|
let a_cap = k.grant_cap_for_test(pid_a, Resource::Endpoint(ep), ipc_perms());
|
||||||
|
|
||||||
|
let outcome = k.handle_syscall(Syscall::Send {
|
||||||
|
endpoint: a_cap,
|
||||||
|
message: Message::from_words([1, 2, 3, 4]),
|
||||||
|
});
|
||||||
|
assert!(matches!(outcome, SyscallOutcome::BlockCaller(_)));
|
||||||
|
|
||||||
|
k.destroy_process(pid_b);
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
k.thread_state_for_test(tid_a),
|
||||||
|
Some(ThreadState::Ready)
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
k.pending_result(tid_a),
|
||||||
|
Some(SyscallResult::Error(_))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn endpoint_slots_are_reclaimed_when_processes_die() {
|
||||||
|
let (mut k, _) = kernel_with_process();
|
||||||
|
let tid_a = k.current_thread().unwrap();
|
||||||
|
// Far more endpoints than the table holds, created and abandoned.
|
||||||
|
for _ in 0..600 {
|
||||||
|
let cap = create_process(&mut k);
|
||||||
|
let pid = ProcessId::from_raw(1);
|
||||||
|
let tid = k.spawn_thread_for_test(pid, Priority::DEFAULT, TimeSlice::DEFAULT);
|
||||||
|
k.switch_to_thread_for_test(tid);
|
||||||
|
assert!(
|
||||||
|
!is_error(&k.handle_syscall(Syscall::CreateEndpoint)),
|
||||||
|
"endpoint table exhausted"
|
||||||
|
);
|
||||||
|
k.switch_to_thread_for_test(tid_a);
|
||||||
|
k.handle_syscall(Syscall::Kill { process: cap });
|
||||||
|
k.handle_syscall(Syscall::DropCap { slot: cap });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn interrupt_routing_does_not_survive_the_owning_thread() {
|
||||||
|
let (mut k, pid_a) = kernel_with_process();
|
||||||
|
let tid_a = k.current_thread().unwrap();
|
||||||
|
let cap_b = create_process(&mut k);
|
||||||
|
let pid_b = ProcessId::from_raw(1);
|
||||||
|
let tid_b = k.spawn_thread_for_test(pid_b, Priority::DEFAULT, TimeSlice::DEFAULT);
|
||||||
|
let irq = IrqId::from_raw(10);
|
||||||
|
k.register_irq_for_test(irq, tid_b);
|
||||||
|
|
||||||
|
k.switch_to_thread_for_test(tid_a);
|
||||||
|
k.handle_syscall(Syscall::Kill { process: cap_b });
|
||||||
|
|
||||||
|
// A new thread reuses B's thread id and happens to be blocked.
|
||||||
|
let reused = k.spawn_thread_for_test(pid_a, Priority::DEFAULT, TimeSlice::DEFAULT);
|
||||||
|
assert_eq!(reused, tid_b);
|
||||||
|
k.block_thread_for_test(reused, BlockReason::Receiving(CapSlot::new(1).unwrap()));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
k.handle_interrupt(irq),
|
||||||
|
None,
|
||||||
|
"IRQ delivered to an unrelated thread"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn interrupt_numbers_beyond_the_table_are_ignored() {
|
||||||
|
let (mut k, _) = kernel_with_process();
|
||||||
|
// The PLIC can report sources up to 1023.
|
||||||
|
assert_eq!(k.handle_interrupt(IrqId::from_raw(1000)), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exiting_a_thread_releases_its_interrupt_route() {
|
||||||
|
let (mut k, pid_a) = kernel_with_process();
|
||||||
|
let tid_a = k.current_thread().unwrap();
|
||||||
|
let irq = IrqId::from_raw(10);
|
||||||
|
k.register_irq_for_test(irq, tid_a);
|
||||||
|
k.spawn_thread_for_test(pid_a, Priority::DEFAULT, TimeSlice::DEFAULT);
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
k.handle_syscall(Syscall::ExitThread),
|
||||||
|
SyscallOutcome::TerminateCaller
|
||||||
|
));
|
||||||
|
|
||||||
|
let reused = k.spawn_thread_for_test(pid_a, Priority::DEFAULT, TimeSlice::DEFAULT);
|
||||||
|
assert_eq!(reused, tid_a);
|
||||||
|
k.block_thread_for_test(reused, BlockReason::Receiving(CapSlot::new(1).unwrap()));
|
||||||
|
assert_eq!(k.handle_interrupt(irq), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn drop_cap_frees_the_slot() {
|
||||||
|
let (mut k, pid_a) = kernel_with_process();
|
||||||
|
let ep = k.create_endpoint_for_test();
|
||||||
|
let cap = k.grant_cap_for_test(pid_a, Resource::Endpoint(ep), ipc_perms());
|
||||||
|
assert!(!is_error(&k.handle_syscall(Syscall::DropCap { slot: cap })));
|
||||||
|
assert!(is_error(&k.handle_syscall(Syscall::Send {
|
||||||
|
endpoint: cap,
|
||||||
|
message: Message::empty(),
|
||||||
|
})));
|
||||||
|
assert!(is_error(&k.handle_syscall(Syscall::DropCap { slot: cap })));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn killing_yourself_terminates_the_caller() {
|
||||||
|
let (mut k, pid_a) = kernel_with_process();
|
||||||
|
let self_cap = k.grant_cap_for_test(
|
||||||
|
pid_a,
|
||||||
|
Resource::Process(pid_a),
|
||||||
|
PermissionSet::empty().with(Permission::Execute),
|
||||||
|
);
|
||||||
|
let outcome = k.handle_syscall(Syscall::Kill { process: self_cap });
|
||||||
|
assert!(matches!(outcome, SyscallOutcome::TerminateCaller));
|
||||||
|
assert!(!k.process_exists(pid_a));
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ mod invariants;
|
|||||||
mod ipc;
|
mod ipc;
|
||||||
mod isolation;
|
mod isolation;
|
||||||
mod lifecycle;
|
mod lifecycle;
|
||||||
|
mod lifetimes;
|
||||||
mod memory;
|
mod memory;
|
||||||
mod simple;
|
mod simple;
|
||||||
mod tick;
|
mod tick;
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ pub fn revoke(slot: CapSlot) -> bool {
|
|||||||
arch::syscall1(SYS_REVOKE, slot.value() as usize) == 0
|
arch::syscall1(SYS_REVOKE, slot.value() as usize) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Removes a capability from this process's own table, freeing the slot.
|
||||||
|
pub fn drop_cap(slot: CapSlot) -> bool {
|
||||||
|
arch::syscall1(SYS_DROP_CAP, slot.value() as usize) == 0
|
||||||
|
}
|
||||||
|
|
||||||
pub fn debug_putchar(byte: u8) {
|
pub fn debug_putchar(byte: u8) {
|
||||||
arch::syscall1(SYS_DEBUG_PUTCHAR, byte as usize);
|
arch::syscall1(SYS_DEBUG_PUTCHAR, byte as usize);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,22 @@ impl Generation {
|
|||||||
Self(0)
|
Self(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next(&self) -> Self {
|
/// The following generation, or `None` once the counter is exhausted.
|
||||||
Self(self.0.wrapping_add(1))
|
/// Never wraps: a wrapped counter would make stale capabilities valid.
|
||||||
|
pub fn next(&self) -> Option<Self> {
|
||||||
|
self.0.checked_add(1).map(Self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn raw(&self) -> u32 {
|
||||||
|
self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_valid_against(&self, current: &Generation) -> bool {
|
pub fn is_valid_against(&self, current: &Generation) -> bool {
|
||||||
self == current
|
self == current
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(kani)]
|
/// Only kernel code builds capabilities, so exposing this cannot
|
||||||
|
/// forge one; it exists for tests and proofs.
|
||||||
pub fn from_raw(value: u32) -> Self {
|
pub fn from_raw(value: u32) -> Self {
|
||||||
Self(value)
|
Self(value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
use crate::generation::Generation;
|
use crate::generation::Generation;
|
||||||
|
|
||||||
|
/// Generations only ever increase, so a capability minted for an earlier
|
||||||
|
/// generation can never match again. (The old wrapping counter returned to
|
||||||
|
/// earlier values after 2^32 bumps.)
|
||||||
#[kani::proof]
|
#[kani::proof]
|
||||||
fn generation_next_always_invalidates() {
|
fn generation_next_is_strictly_greater_or_exhausted() {
|
||||||
let value: u32 = kani::any();
|
let value: u32 = kani::any();
|
||||||
let current = Generation::from_raw(value);
|
let current = Generation::from_raw(value);
|
||||||
let next = current.next();
|
match current.next() {
|
||||||
|
Some(next) => {
|
||||||
|
assert!(next.raw() > current.raw());
|
||||||
assert!(!current.is_valid_against(&next));
|
assert!(!current.is_valid_against(&next));
|
||||||
|
}
|
||||||
|
None => assert!(value == u32::MAX),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,3 +20,4 @@ pub const SYS_CREATE_ENDPOINT: usize = 18;
|
|||||||
pub const SYS_DEBUG_PUTCHAR: usize = 19;
|
pub const SYS_DEBUG_PUTCHAR: usize = 19;
|
||||||
pub const SYS_MAP_PAGE: usize = 20;
|
pub const SYS_MAP_PAGE: usize = 20;
|
||||||
pub const SYS_SPAWN_THREAD_IN: usize = 21;
|
pub const SYS_SPAWN_THREAD_IN: usize = 21;
|
||||||
|
pub const SYS_DROP_CAP: usize = 22;
|
||||||
|
|||||||
Reference in New Issue
Block a user