feat(kernel): derivation-tree revocation, region caps
- caps record parent (mint/delegate/ipc), CapRef w/ pid+slot gens - Revoke = delete subtree, keep self; drop/death re-parents children - CreateRegion/MapRegion; mapping lives only as long as its cap - drop Permission::Revoke + object-wide revoke - mint any cap (badge only ep/ntfn), process caps delegable - kani: descendants == subtree; model-based fuzz vs spec
This commit is contained in:
@@ -241,6 +241,19 @@ fn decode_syscall(frame: &TrapFrame) -> Option<domain::syscall::Syscall> {
|
||||
stack: StackPointer::from_raw(a(2)),
|
||||
},
|
||||
SYS_CREATE_ENDPOINT => Syscall::CreateEndpoint,
|
||||
SYS_CREATE_REGION => Syscall::CreateRegion {
|
||||
pages: PageCount::new(u16::try_from(a(0)).ok()?)?,
|
||||
},
|
||||
SYS_MAP_REGION => Syscall::MapRegion {
|
||||
region: slot(0)?,
|
||||
va: VirtualAddress::from_raw(a(1)),
|
||||
perms: match a(2) {
|
||||
PERMS_READ_ONLY => types::mapping::PagePerms::ReadOnly,
|
||||
PERMS_READ_WRITE => types::mapping::PagePerms::ReadWrite,
|
||||
PERMS_READ_EXECUTE => types::mapping::PagePerms::ReadExecute,
|
||||
_ => return None,
|
||||
},
|
||||
},
|
||||
SYS_DEBUG_PUTCHAR => Syscall::DebugPutchar { byte: a(0) as u8 },
|
||||
SYS_MAP_PAGE => Syscall::MapPage {
|
||||
process: slot(0)?,
|
||||
|
||||
83
crates/domain/src/capability/derivation.rs
Normal file
83
crates/domain/src/capability/derivation.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
//! The capability derivation relation, reduced to its essence: every
|
||||
//! capability is a node with at most one parent. Kept free of kernel
|
||||
//! types so the revocation rule can be checked on its own.
|
||||
|
||||
/// `result[i]` is true iff `root` is `i` or an ancestor of `i`.
|
||||
///
|
||||
/// `parents` must be acyclic, which holds because a capability's parent
|
||||
/// always exists before it does.
|
||||
pub fn descendants<const N: usize>(parents: &[Option<u16>; N], root: usize) -> [bool; N] {
|
||||
let mut marked = [false; N];
|
||||
if root >= N {
|
||||
return marked;
|
||||
}
|
||||
marked[root] = true;
|
||||
// Each pass extends the marked set by at least one generation of
|
||||
// children; a chain in an N-node forest is at most N - 1 long.
|
||||
for _ in 0..N {
|
||||
let mut changed = false;
|
||||
for i in 0..N {
|
||||
if !marked[i]
|
||||
&& let Some(p) = parents[i].map(usize::from)
|
||||
&& p < N
|
||||
&& marked[p]
|
||||
{
|
||||
marked[i] = true;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
break;
|
||||
}
|
||||
}
|
||||
marked
|
||||
}
|
||||
|
||||
#[cfg(kani)]
|
||||
mod proofs {
|
||||
use super::descendants;
|
||||
|
||||
const N: usize = 4;
|
||||
|
||||
fn is_ancestor(parents: &[Option<u16>; N], root: usize, mut i: usize) -> bool {
|
||||
for _ in 0..=N {
|
||||
if i == root {
|
||||
return true;
|
||||
}
|
||||
match parents[i].map(usize::from) {
|
||||
Some(p) if p < N => i = p,
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// On every acyclic forest of `N` capabilities, `descendants` marks
|
||||
/// exactly the nodes `root` is an ancestor of: revocation removes the
|
||||
/// whole subtree and nothing else.
|
||||
#[kani::proof]
|
||||
#[kani::unwind(6)]
|
||||
fn marks_exactly_the_subtree() {
|
||||
let mut parents = [None; N];
|
||||
for slot in parents.iter_mut() {
|
||||
if kani::any() {
|
||||
*slot = Some(kani::any::<u16>() % N as u16);
|
||||
}
|
||||
}
|
||||
// Only forests: following parents never revisits a node.
|
||||
for start in 0..N {
|
||||
let mut i = start;
|
||||
let mut steps = 0;
|
||||
while let Some(p) = parents[i] {
|
||||
steps += 1;
|
||||
kani::assume(steps <= N - 1);
|
||||
i = p as usize;
|
||||
}
|
||||
}
|
||||
let root = kani::any::<usize>() % N;
|
||||
let marked = descendants(&parents, root);
|
||||
for i in 0..N {
|
||||
assert!(marked[i] == is_ancestor(&parents, root, i));
|
||||
}
|
||||
}
|
||||
}
|
||||
40
crates/domain/src/capability/derivation_tests.rs
Normal file
40
crates/domain/src/capability/derivation_tests.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use super::derivation::descendants;
|
||||
|
||||
#[test]
|
||||
fn a_root_with_no_children_marks_only_itself() {
|
||||
let parents = [None, None, None];
|
||||
assert_eq!(descendants(&parents, 1), [false, true, false]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marks_children_and_grandchildren() {
|
||||
// 0 -> 1 -> 2, 0 -> 3, 4 unrelated
|
||||
let parents = [None, Some(0), Some(1), Some(0), None];
|
||||
assert_eq!(descendants(&parents, 0), [true, true, true, true, false]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoking_a_middle_node_spares_its_ancestors_and_siblings() {
|
||||
// 0 -> 1 -> 2, 0 -> 3
|
||||
let parents = [None, Some(0), Some(1), Some(0)];
|
||||
assert_eq!(descendants(&parents, 1), [false, true, true, false]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_chains_are_followed_to_the_end() {
|
||||
let parents = [None, Some(0), Some(1), Some(2), Some(3), Some(4)];
|
||||
assert_eq!(descendants(&parents, 0), [true; 6]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chains_listed_out_of_order_are_still_found() {
|
||||
// 3 -> 2 -> 1 -> 0 (children appear before their parents)
|
||||
let parents = [Some(1), Some(2), Some(3), None];
|
||||
assert_eq!(descendants(&parents, 3), [true, true, true, true]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_out_of_range_root_marks_nothing() {
|
||||
let parents = [None, Some(0)];
|
||||
assert_eq!(descendants(&parents, 7), [false, false]);
|
||||
}
|
||||
@@ -1,8 +1,16 @@
|
||||
use types::{badge::Badge, bounded::CapSlot, generation::Generation, permission::PermissionSet};
|
||||
use types::{
|
||||
badge::Badge, bounded::CapSlot, generation::Generation, id::ProcessId,
|
||||
permission::PermissionSet,
|
||||
};
|
||||
|
||||
pub mod authority;
|
||||
pub mod derivation;
|
||||
|
||||
use crate::{error::KernelError, resource::Resource};
|
||||
#[cfg(test)]
|
||||
#[path = "derivation_tests.rs"]
|
||||
mod derivation_tests;
|
||||
|
||||
use crate::{error::KernelError, generations::SlotGenerations, resource::Resource};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests.rs"]
|
||||
@@ -11,12 +19,26 @@ mod tests;
|
||||
// Sized so that every representable `CapSlot` indexes a real slot.
|
||||
pub const MAX_CAPS_PER_PROCESS: usize = CapSlot::MAX as usize + 1;
|
||||
|
||||
/// Names one capability slot at one point in time. Both generations must
|
||||
/// still match for the reference to resolve, so a reused pid or slot is
|
||||
/// never mistaken for the capability that used to live there.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct CapRef {
|
||||
pub process: ProcessId,
|
||||
pub process_generation: Generation,
|
||||
pub slot: CapSlot,
|
||||
pub slot_generation: Generation,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Capability {
|
||||
resource: Resource,
|
||||
permissions: PermissionSet,
|
||||
generation: Generation,
|
||||
badge: Badge,
|
||||
/// The capability this one was derived from (Mint, Delegate, IPC
|
||||
/// transfer); `None` for capabilities the kernel created directly.
|
||||
parent: Option<CapRef>,
|
||||
}
|
||||
|
||||
impl Capability {
|
||||
@@ -26,9 +48,23 @@ impl Capability {
|
||||
permissions,
|
||||
generation,
|
||||
badge: Badge::NONE,
|
||||
parent: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_parent(mut self, parent: Option<CapRef>) -> Self {
|
||||
self.parent = parent;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn parent(&self) -> Option<CapRef> {
|
||||
self.parent
|
||||
}
|
||||
|
||||
pub fn set_parent(&mut self, parent: Option<CapRef>) {
|
||||
self.parent = parent;
|
||||
}
|
||||
|
||||
/// The same capability with `badge` stamped on it.
|
||||
pub fn with_badge(mut self, badge: Badge) -> Self {
|
||||
self.badge = badge;
|
||||
@@ -54,15 +90,36 @@ impl Capability {
|
||||
|
||||
pub struct CapabilityTable {
|
||||
slots: [Option<Capability>; MAX_CAPS_PER_PROCESS],
|
||||
generations: SlotGenerations<MAX_CAPS_PER_PROCESS>,
|
||||
}
|
||||
|
||||
impl CapabilityTable {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
slots: [const { None }; MAX_CAPS_PER_PROCESS],
|
||||
generations: SlotGenerations::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generation of an occupied slot.
|
||||
pub fn slot_generation(&self, slot: CapSlot) -> Option<Generation> {
|
||||
self.slots[slot.value() as usize].as_ref()?;
|
||||
self.generations.current(slot.value() as usize)
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, slot: CapSlot) -> Result<&mut Capability, KernelError> {
|
||||
self.slots[slot.value() as usize]
|
||||
.as_mut()
|
||||
.ok_or(KernelError::InvalidSlot(slot))
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (CapSlot, &Capability)> {
|
||||
self.slots.iter().enumerate().filter_map(|(i, c)| {
|
||||
let cap = c.as_ref()?;
|
||||
Some((CapSlot::new(i as u8)?, cap))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get(&self, slot: CapSlot) -> Result<&Capability, KernelError> {
|
||||
let idx = slot.value() as usize;
|
||||
self.slots[idx]
|
||||
@@ -71,10 +128,8 @@ impl CapabilityTable {
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, cap: Capability) -> Result<CapSlot, KernelError> {
|
||||
let idx = self
|
||||
.slots
|
||||
.iter()
|
||||
.position(|s| s.is_none())
|
||||
let idx = (0..MAX_CAPS_PER_PROCESS)
|
||||
.find(|&i| self.slots[i].is_none() && self.generations.is_usable(i))
|
||||
.ok_or(KernelError::NoFreeSlot)?;
|
||||
|
||||
self.slots[idx] = Some(cap);
|
||||
@@ -87,6 +142,7 @@ impl CapabilityTable {
|
||||
return Err(KernelError::InvalidSlot(slot));
|
||||
}
|
||||
self.slots[idx] = None;
|
||||
self.generations.advance(idx);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,6 @@ fn restrict_to_cannot_add_permissions() {
|
||||
Permission::Write,
|
||||
Permission::Execute,
|
||||
Permission::Delegate,
|
||||
Permission::Revoke,
|
||||
]);
|
||||
|
||||
let result = read_only.restrict_to(&wants_everything);
|
||||
@@ -118,7 +117,6 @@ fn restrict_to_cannot_add_permissions() {
|
||||
assert!(!result.allows(Permission::Write));
|
||||
assert!(!result.allows(Permission::Execute));
|
||||
assert!(!result.allows(Permission::Delegate));
|
||||
assert!(!result.allows(Permission::Revoke));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -138,7 +136,6 @@ fn empty_permission_set_allows_nothing() {
|
||||
assert!(!empty.allows(Permission::Write));
|
||||
assert!(!empty.allows(Permission::Execute));
|
||||
assert!(!empty.allows(Permission::Delegate));
|
||||
assert!(!empty.allows(Permission::Revoke));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -150,3 +147,45 @@ fn capability_preserves_resource_and_permissions() {
|
||||
assert_eq!(cap.permissions(), &expected_permissions);
|
||||
assert_eq!(cap.generation(), &Generation::initial());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removing_a_cap_changes_its_slot_generation() {
|
||||
let mut table = CapabilityTable::new();
|
||||
let slot = table.insert(endpoint_cap(1, &[Permission::Read])).unwrap();
|
||||
let before = table.slot_generation(slot).unwrap();
|
||||
table.remove(slot).unwrap();
|
||||
assert_eq!(
|
||||
table.slot_generation(slot),
|
||||
None,
|
||||
"empty slots have no generation"
|
||||
);
|
||||
let again = table.insert(endpoint_cap(2, &[Permission::Read])).unwrap();
|
||||
assert_eq!(again, slot);
|
||||
assert!(!before.is_valid_against(&table.slot_generation(again).unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iter_lists_occupied_slots() {
|
||||
let mut table = CapabilityTable::new();
|
||||
let a = table.insert(endpoint_cap(1, &[])).unwrap();
|
||||
let b = table.insert(endpoint_cap(2, &[])).unwrap();
|
||||
table.remove(a).unwrap();
|
||||
let slots: [Option<types::bounded::CapSlot>; 2] = {
|
||||
let mut it = table.iter().map(|(s, _)| s);
|
||||
[it.next(), it.next()]
|
||||
};
|
||||
assert_eq!(slots, [Some(b), None]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caps_record_their_parent() {
|
||||
let parent = CapRef {
|
||||
process: types::id::ProcessId::from_raw(3),
|
||||
process_generation: Generation::initial(),
|
||||
slot: types::bounded::CapSlot::new(4).unwrap(),
|
||||
slot_generation: Generation::initial(),
|
||||
};
|
||||
let cap = endpoint_cap(1, &[]).with_parent(Some(parent));
|
||||
assert_eq!(cap.parent(), Some(parent));
|
||||
assert_eq!(endpoint_cap(1, &[]).parent(), None);
|
||||
}
|
||||
|
||||
@@ -111,13 +111,6 @@ impl EndpointTable {
|
||||
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()
|
||||
|
||||
@@ -102,18 +102,6 @@ fn senders_and_receivers_are_independent() {
|
||||
assert_eq!(ep.dequeue_receiver(), Some(thread(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoke_bumps_generation_but_keeps_the_endpoint() {
|
||||
let mut table = EndpointTable::new();
|
||||
let id = table.create(owner()).unwrap();
|
||||
let gen_before = table.generation(id).unwrap();
|
||||
table.revoke(id);
|
||||
let gen_after = table.generation(id).unwrap();
|
||||
|
||||
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();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::generations::SlotGenerations;
|
||||
use crate::{capability::CapRef, generations::SlotGenerations};
|
||||
use types::{
|
||||
address::{PhysicalAddress, VirtualAddress},
|
||||
bounded::PageCount,
|
||||
@@ -35,6 +35,9 @@ pub struct RegionMapping {
|
||||
pub process: ProcessId,
|
||||
pub va: VirtualAddress,
|
||||
pub perms: PagePerms,
|
||||
/// The capability the mapping was made through. Deleting that
|
||||
/// capability removes the mapping; `None` for kernel-made mappings.
|
||||
pub via: Option<CapRef>,
|
||||
}
|
||||
|
||||
/// Physically contiguous memory that may be mapped into several address
|
||||
@@ -132,13 +135,6 @@ impl RegionTable {
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ fn mapping(process: u16, addr: usize) -> RegionMapping {
|
||||
process: pid(process),
|
||||
va: va(addr),
|
||||
perms: PagePerms::ReadWrite,
|
||||
via: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -152,12 +152,6 @@ impl NotificationTable {
|
||||
self.generations.current(id.raw() as usize)
|
||||
}
|
||||
|
||||
pub fn revoke(&mut self, id: NotificationId) {
|
||||
if self.get(id).is_some() {
|
||||
self.generations.advance(id.raw() as usize);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn destroy(&mut self, id: NotificationId) -> Option<Notification> {
|
||||
let n = self.slots.get_mut(id.raw() as usize)?.take()?;
|
||||
self.generations.advance(id.raw() as usize);
|
||||
|
||||
@@ -58,9 +58,6 @@ fn delegation_cannot_add_permissions_not_in_source() {
|
||||
if !source.allows(Permission::Delegate) {
|
||||
assert!(!result.allows(Permission::Delegate));
|
||||
}
|
||||
if !source.allows(Permission::Revoke) {
|
||||
assert!(!result.allows(Permission::Revoke));
|
||||
}
|
||||
}
|
||||
|
||||
#[kani::proof]
|
||||
|
||||
@@ -2,6 +2,7 @@ use types::{
|
||||
address::{DataRef, EntryPoint, StackPointer, VirtualAddress},
|
||||
badge::Badge,
|
||||
bounded::{CapSlot, PageCount},
|
||||
mapping::PagePerms,
|
||||
permission::PermissionSet,
|
||||
};
|
||||
|
||||
@@ -68,9 +69,21 @@ pub enum Syscall {
|
||||
Kill {
|
||||
process: CapSlot,
|
||||
},
|
||||
/// Deletes every capability derived from `slot`, keeping `slot`.
|
||||
Revoke {
|
||||
slot: CapSlot,
|
||||
},
|
||||
/// Allocates a shared-memory region charged to the caller and returns
|
||||
/// a capability to it.
|
||||
CreateRegion {
|
||||
pages: PageCount,
|
||||
},
|
||||
/// Maps the whole region named by `region` at `va` in the caller.
|
||||
MapRegion {
|
||||
region: CapSlot,
|
||||
va: VirtualAddress,
|
||||
perms: PagePerms,
|
||||
},
|
||||
/// Removes a capability from the caller's own table.
|
||||
DropCap {
|
||||
slot: CapSlot,
|
||||
|
||||
168
crates/kernel/src/captree.rs
Normal file
168
crates/kernel/src/captree.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
//! The capability derivation tree. Every derived capability (Mint,
|
||||
//! Delegate, IPC transfer) records its parent; `Revoke` deletes a subtree
|
||||
//! and deleting a capability re-parents its children, so the tree can only
|
||||
//! shrink by explicit deletion, never by a middleman stepping out.
|
||||
|
||||
use domain::{
|
||||
capability::{CapRef, Capability, MAX_CAPS_PER_PROCESS, derivation::descendants},
|
||||
error::KernelError,
|
||||
process::MAX_PROCESSES,
|
||||
};
|
||||
use platform::Platform;
|
||||
use types::{
|
||||
bounded::CapSlot,
|
||||
id::{ProcessId, RegionId},
|
||||
};
|
||||
|
||||
use crate::Kernel;
|
||||
|
||||
/// Every capability slot in the system, flattened.
|
||||
const ALL_CAPS: usize = MAX_PROCESSES * MAX_CAPS_PER_PROCESS;
|
||||
|
||||
fn flat(pid: ProcessId, slot: CapSlot) -> usize {
|
||||
pid.raw() as usize * MAX_CAPS_PER_PROCESS + slot.value() as usize
|
||||
}
|
||||
|
||||
fn unflat(i: usize) -> Option<(ProcessId, CapSlot)> {
|
||||
let pid = ProcessId::from_raw((i / MAX_CAPS_PER_PROCESS) as u16);
|
||||
let slot = CapSlot::new((i % MAX_CAPS_PER_PROCESS) as u8)?;
|
||||
Some((pid, slot))
|
||||
}
|
||||
|
||||
impl<P: Platform> Kernel<P> {
|
||||
/// A reference to the capability currently in `slot` of `pid`.
|
||||
pub(crate) fn cap_ref(&self, pid: ProcessId, slot: CapSlot) -> Option<CapRef> {
|
||||
let process = self.processes.get(pid).ok()?;
|
||||
Some(CapRef {
|
||||
process: pid,
|
||||
process_generation: self.processes.generation(pid)?,
|
||||
slot,
|
||||
slot_generation: process.capabilities().slot_generation(slot)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether `r` still names a live capability.
|
||||
pub(crate) fn resolves(&self, r: CapRef) -> bool {
|
||||
self.cap_ref(r.process, r.slot) == Some(r)
|
||||
}
|
||||
|
||||
/// Stores `cap` in `to`'s table as a child of `parent`.
|
||||
pub(crate) fn insert_derived(
|
||||
&mut self,
|
||||
to: ProcessId,
|
||||
cap: Capability,
|
||||
parent: Option<CapRef>,
|
||||
) -> Result<CapSlot, KernelError> {
|
||||
self.processes
|
||||
.get_mut(to)?
|
||||
.capabilities_mut()
|
||||
.insert(cap.with_parent(parent))
|
||||
}
|
||||
|
||||
/// Calls `f` with every live capability's flat index and reference.
|
||||
fn for_each_cap(&self, mut f: impl FnMut(usize, CapRef, &Capability)) {
|
||||
for p in 0..MAX_PROCESSES {
|
||||
let pid = ProcessId::from_raw(p as u16);
|
||||
let Ok(process) = self.processes.get(pid) else {
|
||||
continue;
|
||||
};
|
||||
for (slot, cap) in process.capabilities().iter() {
|
||||
if let Some(r) = self.cap_ref(pid, slot) {
|
||||
f(flat(pid, slot), r, cap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes every region mapping made through `via`.
|
||||
fn unmap_mappings_via(&mut self, via: CapRef) {
|
||||
let mut doomed: [Option<RegionId>; domain::memory::MAX_REGIONS] =
|
||||
[None; domain::memory::MAX_REGIONS];
|
||||
for (slot, (id, _)) in doomed.iter_mut().zip(
|
||||
self.regions
|
||||
.iter()
|
||||
.filter(|(_, r)| r.mappings().any(|m| m.via == Some(via))),
|
||||
) {
|
||||
*slot = Some(id);
|
||||
}
|
||||
for id in doomed.into_iter().flatten() {
|
||||
self.unmap_region_from(id, via.process);
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes one capability: its mappings go, its children are adopted
|
||||
/// by its parent.
|
||||
pub(crate) fn delete_cap(&mut self, pid: ProcessId, slot: CapSlot) -> Result<(), KernelError> {
|
||||
let me = self
|
||||
.cap_ref(pid, slot)
|
||||
.ok_or(KernelError::InvalidSlot(slot))?;
|
||||
let grandparent = self.processes.get(pid)?.capabilities().get(slot)?.parent();
|
||||
self.unmap_mappings_via(me);
|
||||
|
||||
let mut children = [false; ALL_CAPS];
|
||||
self.for_each_cap(|i, _, cap| {
|
||||
if cap.parent() == Some(me) {
|
||||
children[i] = true;
|
||||
}
|
||||
});
|
||||
for (i, _) in children.iter().enumerate().filter(|(_, c)| **c) {
|
||||
if let Some((cpid, cslot)) = unflat(i)
|
||||
&& let Ok(p) = self.processes.get_mut(cpid)
|
||||
&& let Ok(child) = p.capabilities_mut().get_mut(cslot)
|
||||
{
|
||||
child.set_parent(grandparent);
|
||||
}
|
||||
}
|
||||
self.processes.get_mut(pid)?.capabilities_mut().remove(slot)
|
||||
}
|
||||
|
||||
/// Deletes every capability derived from `slot` of `pid`, transitively,
|
||||
/// and keeps `slot` itself.
|
||||
pub(crate) fn revoke_subtree(
|
||||
&mut self,
|
||||
pid: ProcessId,
|
||||
slot: CapSlot,
|
||||
) -> Result<(), KernelError> {
|
||||
if self.cap_ref(pid, slot).is_none() {
|
||||
return Err(KernelError::InvalidSlot(slot));
|
||||
}
|
||||
// Snapshot the tree as parent indices. A parent that no longer
|
||||
// resolves cannot exist (deletion re-parents), but is treated as
|
||||
// a root rather than trusted.
|
||||
let mut parents: [Option<u16>; ALL_CAPS] = [None; ALL_CAPS];
|
||||
self.for_each_cap(|i, _, cap| {
|
||||
parents[i] = cap
|
||||
.parent()
|
||||
.filter(|r| self.resolves(*r))
|
||||
.map(|r| flat(r.process, r.slot) as u16);
|
||||
});
|
||||
let root = flat(pid, slot);
|
||||
let marked = descendants(&parents, root);
|
||||
for (i, _) in marked.iter().enumerate().filter(|(i, m)| **m && *i != root) {
|
||||
if let Some((dpid, dslot)) = unflat(i)
|
||||
&& let Some(r) = self.cap_ref(dpid, dslot)
|
||||
{
|
||||
self.unmap_mappings_via(r);
|
||||
let _ = self
|
||||
.processes
|
||||
.get_mut(dpid)
|
||||
.map(|p| p.capabilities_mut().remove(dslot));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes all of a dying process's capabilities, re-parenting their
|
||||
/// children so revocation still reaches them.
|
||||
pub(crate) fn delete_all_caps(&mut self, pid: ProcessId) {
|
||||
let mut slots = [None; MAX_CAPS_PER_PROCESS];
|
||||
if let Ok(p) = self.processes.get(pid) {
|
||||
for (out, (slot, _)) in slots.iter_mut().zip(p.capabilities().iter()) {
|
||||
*out = Some(slot);
|
||||
}
|
||||
}
|
||||
for slot in slots.into_iter().flatten() {
|
||||
let _ = self.delete_cap(pid, slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#![no_std]
|
||||
|
||||
mod captree;
|
||||
pub mod interrupt;
|
||||
mod memory;
|
||||
#[cfg(kani)]
|
||||
@@ -278,6 +279,7 @@ impl<P: Platform> Kernel<P> {
|
||||
};
|
||||
|
||||
self.cleanup_threads(&thread_ids);
|
||||
self.delete_all_caps(pid);
|
||||
self.destroy_endpoints_owned_by(pid);
|
||||
self.destroy_notifications_owned_by(pid);
|
||||
self.teardown_memory(pid);
|
||||
@@ -373,15 +375,6 @@ impl<P: Platform> Kernel<P> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn revoke_resource(&mut self, resource: &Resource) {
|
||||
match resource {
|
||||
Resource::Endpoint(id) => self.endpoints.revoke(*id),
|
||||
Resource::Memory(id) => self.regions.revoke(*id),
|
||||
Resource::Notification(id) => self.notifications.revoke(*id),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn grant_cap(
|
||||
&mut self,
|
||||
to: ProcessId,
|
||||
|
||||
@@ -334,12 +334,28 @@ impl<P: Platform> Kernel<P> {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn map_region(
|
||||
&mut self,
|
||||
id: RegionId,
|
||||
pid: ProcessId,
|
||||
va: VirtualAddress,
|
||||
perms: PagePerms,
|
||||
) -> Result<(), KernelError> {
|
||||
self.map_region_via(id, pid, va, perms, None)
|
||||
}
|
||||
|
||||
/// Maps all of region `id` at `va` in `pid`, all or nothing. A mapping
|
||||
/// made through a capability (`via`) is removed when that capability
|
||||
/// is deleted.
|
||||
pub(crate) fn map_region_via(
|
||||
&mut self,
|
||||
id: RegionId,
|
||||
pid: ProcessId,
|
||||
va: VirtualAddress,
|
||||
perms: PagePerms,
|
||||
via: Option<domain::capability::CapRef>,
|
||||
) -> Result<(), KernelError> {
|
||||
self.processes.get(pid)?;
|
||||
let region = self.regions.get(id).ok_or(KernelError::InvalidRegion)?;
|
||||
@@ -366,6 +382,7 @@ impl<P: Platform> Kernel<P> {
|
||||
process: pid,
|
||||
va,
|
||||
perms,
|
||||
via,
|
||||
})
|
||||
});
|
||||
if !recorded {
|
||||
@@ -377,6 +394,19 @@ impl<P: Platform> Kernel<P> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes `pid`'s mapping of region `id`, if it has one.
|
||||
pub(crate) fn unmap_region_from(&mut self, id: RegionId, pid: ProcessId) {
|
||||
let Some(region) = self.regions.get_mut(id) else {
|
||||
return;
|
||||
};
|
||||
let pages = region.pages().value() as usize;
|
||||
if let Some(m) = region.remove_mapping(pid) {
|
||||
for i in 0..pages {
|
||||
self.platform.unmap_page(pid, page_at(m.va, i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Frees regions owned by `pid` (unmapping them from every other
|
||||
/// process) and forgets `pid`'s mappings of other regions.
|
||||
fn teardown_regions(&mut self, pid: ProcessId) {
|
||||
|
||||
@@ -45,9 +45,6 @@ fn kernel_delegate_path_never_escalates() {
|
||||
if !original_perms.allows(Permission::Delegate) {
|
||||
assert!(!result.permissions().allows(Permission::Delegate));
|
||||
}
|
||||
if !original_perms.allows(Permission::Revoke) {
|
||||
assert!(!result.permissions().allows(Permission::Revoke));
|
||||
}
|
||||
}
|
||||
|
||||
#[kani::proof]
|
||||
|
||||
@@ -66,5 +66,4 @@ fn check_cap_faithfully_reflects_stored_permissions() {
|
||||
assert!(
|
||||
retrieved.permissions().allows(Permission::Delegate) == perms.allows(Permission::Delegate)
|
||||
);
|
||||
assert!(retrieved.permissions().allows(Permission::Revoke) == perms.allows(Permission::Revoke));
|
||||
}
|
||||
|
||||
@@ -42,10 +42,8 @@ impl<P: Platform> Kernel<P> {
|
||||
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
|
||||
};
|
||||
|
||||
let inserted = self
|
||||
.processes
|
||||
.get_mut(target_pid)
|
||||
.and_then(|p| p.capabilities_mut().insert(copy));
|
||||
let parent = self.cap_ref(caller_pid, slot);
|
||||
let inserted = self.insert_derived(target_pid, copy, parent);
|
||||
if let Err(e) = inserted {
|
||||
return SyscallOutcome::Complete(SyscallResult::Error(e));
|
||||
}
|
||||
@@ -58,14 +56,10 @@ impl<P: Platform> Kernel<P> {
|
||||
caller_pid: types::id::ProcessId,
|
||||
slot: types::bounded::CapSlot,
|
||||
) -> SyscallOutcome {
|
||||
let resource = match self.check_cap(caller_pid, slot, types::permission::Permission::Revoke)
|
||||
{
|
||||
Ok(cap) => cap.resource().clone(),
|
||||
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
|
||||
};
|
||||
|
||||
self.revoke_resource(&resource);
|
||||
SyscallOutcome::Complete(SyscallResult::Ok)
|
||||
SyscallOutcome::Complete(match self.revoke_subtree(caller_pid, slot) {
|
||||
Ok(()) => SyscallResult::Ok,
|
||||
Err(e) => SyscallResult::Error(e),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn handle_drop_cap(
|
||||
@@ -73,11 +67,7 @@ impl<P: Platform> Kernel<P> {
|
||||
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 {
|
||||
SyscallOutcome::Complete(match self.delete_cap(caller_pid, slot) {
|
||||
Ok(()) => SyscallResult::Ok,
|
||||
Err(e) => SyscallResult::Error(e),
|
||||
})
|
||||
|
||||
@@ -150,12 +150,8 @@ impl<P: Platform> Kernel<P> {
|
||||
.check_cap(from, slot, Permission::Delegate)
|
||||
.ok()?
|
||||
.clone();
|
||||
self.processes
|
||||
.get_mut(to)
|
||||
.ok()?
|
||||
.capabilities_mut()
|
||||
.insert(copy)
|
||||
.ok()
|
||||
let parent = self.cap_ref(from, slot);
|
||||
self.insert_derived(to, copy, parent).ok()
|
||||
}
|
||||
|
||||
/// Hands `out` from `sender` to `receiver`. For a call, the receiver's
|
||||
@@ -361,13 +357,13 @@ impl<P: Platform> Kernel<P> {
|
||||
permissions: PermissionSet,
|
||||
) -> Result<CapSlot, KernelError> {
|
||||
let source = self.check_cap(pid, slot, Permission::Delegate)?;
|
||||
if !matches!(
|
||||
let badgeable = matches!(
|
||||
source.resource(),
|
||||
Resource::Endpoint(_) | Resource::Notification(_)
|
||||
) {
|
||||
return Err(KernelError::WrongObject);
|
||||
}
|
||||
if !source.badge().is_none() {
|
||||
);
|
||||
// Any capability can be copied with narrower rights; only those a
|
||||
// receiver can observe the badge of take one, and only once.
|
||||
if (!badgeable && !badge.is_none()) || !source.badge().is_none() {
|
||||
return Err(KernelError::InvalidArgument);
|
||||
}
|
||||
let minted = Capability::new(
|
||||
@@ -376,10 +372,8 @@ impl<P: Platform> Kernel<P> {
|
||||
*source.generation(),
|
||||
)
|
||||
.with_badge(badge);
|
||||
self.processes
|
||||
.get_mut(pid)?
|
||||
.capabilities_mut()
|
||||
.insert(minted)
|
||||
let parent = self.cap_ref(pid, slot);
|
||||
self.insert_derived(pid, minted, parent)
|
||||
}
|
||||
|
||||
pub(crate) fn handle_create_endpoint(&mut self, pid: ProcessId) -> SyscallOutcome {
|
||||
|
||||
@@ -65,9 +65,12 @@ impl<P: Platform> Kernel<P> {
|
||||
match self.grant_cap(
|
||||
caller_pid,
|
||||
domain::resource::Resource::Process(new_pid),
|
||||
// The creator has full authority over what it creates,
|
||||
// including handing that authority on.
|
||||
types::permission::PermissionSet::empty()
|
||||
.with(types::permission::Permission::Execute)
|
||||
.with(types::permission::Permission::Read),
|
||||
.with(types::permission::Permission::Read)
|
||||
.with(types::permission::Permission::Delegate),
|
||||
) {
|
||||
Ok(slot) => SyscallOutcome::Complete(SyscallResult::Slot(slot)),
|
||||
Err(e) => {
|
||||
|
||||
@@ -75,4 +75,44 @@ impl<P: Platform> Kernel<P> {
|
||||
.map(|()| SyscallResult::Ok),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn create_region_cap(
|
||||
&mut self,
|
||||
pid: ProcessId,
|
||||
pages: PageCount,
|
||||
) -> Result<CapSlot, KernelError> {
|
||||
let id = self.create_pool_region(pid, pages)?;
|
||||
let all = types::permission::PermissionSet::empty()
|
||||
.with(Permission::Read)
|
||||
.with(Permission::Write)
|
||||
.with(Permission::Execute)
|
||||
.with(Permission::Delegate);
|
||||
self.grant_cap(pid, Resource::Memory(id), all)
|
||||
}
|
||||
|
||||
/// Maps the region behind `slot` into the caller. The capability must
|
||||
/// allow every access `perms` grants; the mapping lasts as long as
|
||||
/// the capability.
|
||||
pub(crate) fn map_region_cap(
|
||||
&mut self,
|
||||
pid: ProcessId,
|
||||
slot: CapSlot,
|
||||
va: VirtualAddress,
|
||||
perms: PagePerms,
|
||||
) -> Result<(), KernelError> {
|
||||
let cap = self.check_cap(pid, slot, Permission::Read)?;
|
||||
let needed = match perms {
|
||||
PagePerms::ReadOnly => Permission::Read,
|
||||
PagePerms::ReadWrite => Permission::Write,
|
||||
PagePerms::ReadExecute => Permission::Execute,
|
||||
};
|
||||
if !cap.permissions().allows(needed) {
|
||||
return Err(KernelError::PermissionDenied { slot, needed });
|
||||
}
|
||||
let Resource::Memory(id) = *cap.resource() else {
|
||||
return Err(KernelError::WrongObject);
|
||||
};
|
||||
let via = self.cap_ref(pid, slot);
|
||||
self.map_region_via(id, pid, va, perms, via)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,13 @@ impl<P: Platform> Kernel<P> {
|
||||
Syscall::Kill { process } => self.handle_kill(pid, process),
|
||||
Syscall::Revoke { slot } => self.handle_revoke(pid, slot),
|
||||
Syscall::DropCap { slot } => self.handle_drop_cap(pid, slot),
|
||||
Syscall::CreateRegion { pages } => {
|
||||
complete(self.create_region_cap(pid, pages).map(SyscallResult::Slot))
|
||||
}
|
||||
Syscall::MapRegion { region, va, perms } => complete(
|
||||
self.map_region_cap(pid, region, va, perms)
|
||||
.map(|()| SyscallResult::Ok),
|
||||
),
|
||||
Syscall::Delegate {
|
||||
slot,
|
||||
to_endpoint,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use super::*;
|
||||
use crate::{SyscallOutcome, SyscallResult};
|
||||
use domain::resource::Resource;
|
||||
use domain::syscall::Syscall;
|
||||
use types::address::DataRef;
|
||||
use types::bounded::CapSlot;
|
||||
use types::permission::{Permission, PermissionSet};
|
||||
use types::permission::PermissionSet;
|
||||
|
||||
#[test]
|
||||
fn delegate_without_delegate_permission_fails() {
|
||||
@@ -57,147 +56,6 @@ fn delegate_with_valid_cap_succeeds() {
|
||||
|
||||
// --- Generation-based revocation ---
|
||||
|
||||
#[test]
|
||||
fn revoke_resource_invalidates_all_caps() {
|
||||
let (mut k, pid) = kernel_with_process();
|
||||
let ep_id = k.create_endpoint_for_test();
|
||||
|
||||
let cap_slot = k.grant_cap_for_test(
|
||||
pid,
|
||||
Resource::Endpoint(ep_id),
|
||||
PermissionSet::empty().with(Permission::Read),
|
||||
);
|
||||
|
||||
// Revoke the endpoint
|
||||
k.revoke_resource(&Resource::Endpoint(ep_id));
|
||||
|
||||
// Cap is now invalid — check_cap should reject with RevokedCapability
|
||||
let outcome = k.handle_syscall(Syscall::Receive { endpoint: cap_slot });
|
||||
assert!(matches!(
|
||||
outcome,
|
||||
SyscallOutcome::Complete(SyscallResult::Error(
|
||||
domain::error::KernelError::RevokedCapability(_)
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_cap_after_revoke_uses_current_generation() {
|
||||
let (mut k, pid) = kernel_with_process();
|
||||
let ep_id = k.create_endpoint_for_test();
|
||||
|
||||
// Grant, revoke, grant again
|
||||
let old_slot = k.grant_cap_for_test(
|
||||
pid,
|
||||
Resource::Endpoint(ep_id),
|
||||
PermissionSet::empty().with(Permission::Read),
|
||||
);
|
||||
|
||||
k.revoke_resource(&Resource::Endpoint(ep_id));
|
||||
|
||||
// Old cap is dead
|
||||
let outcome = k.handle_syscall(Syscall::Receive { endpoint: old_slot });
|
||||
assert!(matches!(
|
||||
outcome,
|
||||
SyscallOutcome::Complete(SyscallResult::Error(
|
||||
domain::error::KernelError::RevokedCapability(_)
|
||||
))
|
||||
));
|
||||
|
||||
// New cap with current generation works
|
||||
let new_slot = k.grant_cap_for_test(
|
||||
pid,
|
||||
Resource::Endpoint(ep_id),
|
||||
PermissionSet::empty().with(Permission::Read),
|
||||
);
|
||||
|
||||
let outcome = k.handle_syscall(Syscall::Receive { endpoint: new_slot });
|
||||
// Should block (no sender), not error — proving the cap is valid
|
||||
assert!(matches!(outcome, SyscallOutcome::BlockCaller(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoke_does_not_affect_other_resources() {
|
||||
let (mut k, pid) = kernel_with_process();
|
||||
let ep1 = k.create_endpoint_for_test();
|
||||
let ep2 = k.create_endpoint_for_test();
|
||||
|
||||
let slot1 = k.grant_cap_for_test(
|
||||
pid,
|
||||
Resource::Endpoint(ep1),
|
||||
PermissionSet::empty().with(Permission::Read),
|
||||
);
|
||||
let slot2 = k.grant_cap_for_test(
|
||||
pid,
|
||||
Resource::Endpoint(ep2),
|
||||
PermissionSet::empty().with(Permission::Read),
|
||||
);
|
||||
|
||||
// Revoke only ep1
|
||||
k.revoke_resource(&Resource::Endpoint(ep1));
|
||||
|
||||
// ep1 cap is revoked
|
||||
let outcome = k.handle_syscall(Syscall::Receive { endpoint: slot1 });
|
||||
assert!(matches!(
|
||||
outcome,
|
||||
SyscallOutcome::Complete(SyscallResult::Error(
|
||||
domain::error::KernelError::RevokedCapability(_)
|
||||
))
|
||||
));
|
||||
|
||||
// ep2 cap still works
|
||||
let outcome = k.handle_syscall(Syscall::Receive { endpoint: slot2 });
|
||||
assert!(matches!(outcome, SyscallOutcome::BlockCaller(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoke_syscall_invalidates_via_generation() {
|
||||
let (mut k, pid) = kernel_with_process();
|
||||
let ep_id = k.create_endpoint_for_test();
|
||||
|
||||
let cap_slot = k.grant_cap_for_test(
|
||||
pid,
|
||||
Resource::Endpoint(ep_id),
|
||||
PermissionSet::empty()
|
||||
.with(Permission::Read)
|
||||
.with(Permission::Revoke),
|
||||
);
|
||||
|
||||
// Revoke via syscall
|
||||
let outcome = k.handle_syscall(Syscall::Revoke { slot: cap_slot });
|
||||
assert!(matches!(
|
||||
outcome,
|
||||
SyscallOutcome::Complete(SyscallResult::Ok)
|
||||
));
|
||||
|
||||
// The cap itself is now revoked (generation mismatch)
|
||||
let outcome = k.handle_syscall(Syscall::Receive { endpoint: cap_slot });
|
||||
assert!(matches!(
|
||||
outcome,
|
||||
SyscallOutcome::Complete(SyscallResult::Error(
|
||||
domain::error::KernelError::RevokedCapability(_)
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoke_syscall_without_revoke_permission_fails() {
|
||||
let (mut k, pid) = kernel_with_process();
|
||||
let ep_id = k.create_endpoint_for_test();
|
||||
|
||||
let cap_slot = k.grant_cap_for_test(
|
||||
pid,
|
||||
Resource::Endpoint(ep_id),
|
||||
PermissionSet::empty().with(Permission::Read),
|
||||
);
|
||||
|
||||
let outcome = k.handle_syscall(Syscall::Revoke { slot: cap_slot });
|
||||
assert!(matches!(
|
||||
outcome,
|
||||
SyscallOutcome::Complete(SyscallResult::Error(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_representable_slot_is_rejected_not_panicking() {
|
||||
let (mut k, _) = kernel_with_process();
|
||||
|
||||
326
crates/kernel/src/tests/captree_model.rs
Normal file
326
crates/kernel/src/tests/captree_model.rs
Normal file
@@ -0,0 +1,326 @@
|
||||
//! Randomized check of the derivation tree against an independent model.
|
||||
//! The model applies the rules of docs/ipc.md (derive records a parent,
|
||||
//! delete re-parents, revoke removes the subtree, death deletes everything
|
||||
//! a process holds) to its own data; after every step the kernel must hold
|
||||
//! exactly the capabilities the model says are alive.
|
||||
|
||||
extern crate std;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::vec::Vec;
|
||||
|
||||
use super::*;
|
||||
use crate::{SyscallOutcome, SyscallResult};
|
||||
use domain::syscall::Syscall;
|
||||
use types::address::{DataRef, VirtualAddress};
|
||||
use types::badge::Badge;
|
||||
use types::bounded::{CapSlot, PageCount};
|
||||
use types::id::ThreadId;
|
||||
use types::mapping::PagePerms;
|
||||
use types::permission::{Permission, PermissionSet};
|
||||
|
||||
const STEPS: usize = 1500;
|
||||
|
||||
struct Rng(u64);
|
||||
|
||||
impl Rng {
|
||||
fn next(&mut self) -> u64 {
|
||||
self.0 ^= self.0 << 13;
|
||||
self.0 ^= self.0 >> 7;
|
||||
self.0 ^= self.0 << 17;
|
||||
self.0
|
||||
}
|
||||
|
||||
fn below(&mut self, n: usize) -> usize {
|
||||
(self.next() % n as u64) as usize
|
||||
}
|
||||
|
||||
fn pick<T: Copy>(&mut self, xs: &[T]) -> Option<T> {
|
||||
(!xs.is_empty()).then(|| xs[self.below(xs.len())])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Node {
|
||||
parent: Option<usize>,
|
||||
is_region: bool,
|
||||
}
|
||||
|
||||
/// The reference model: logical capabilities and where each one lives.
|
||||
#[derive(Default)]
|
||||
struct Model {
|
||||
nodes: Vec<Node>,
|
||||
alive: HashMap<usize, (ProcessId, CapSlot)>,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
fn add(&mut self, at: (ProcessId, CapSlot), parent: Option<usize>, is_region: bool) -> usize {
|
||||
self.nodes.push(Node { parent, is_region });
|
||||
let id = self.nodes.len() - 1;
|
||||
self.alive.insert(id, at);
|
||||
id
|
||||
}
|
||||
|
||||
fn delete(&mut self, id: usize) {
|
||||
let grandparent = self.nodes[id].parent;
|
||||
for n in 0..self.nodes.len() {
|
||||
if self.alive.contains_key(&n) && self.nodes[n].parent == Some(id) {
|
||||
self.nodes[n].parent = grandparent;
|
||||
}
|
||||
}
|
||||
self.alive.remove(&id);
|
||||
}
|
||||
|
||||
fn is_descendant(&self, mut n: usize, root: usize) -> bool {
|
||||
while let Some(p) = self.nodes[n].parent {
|
||||
if p == root {
|
||||
return true;
|
||||
}
|
||||
n = p;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn revoke(&mut self, root: usize) {
|
||||
let doomed: Vec<usize> = self
|
||||
.alive
|
||||
.keys()
|
||||
.copied()
|
||||
.filter(|&n| self.is_descendant(n, root))
|
||||
.collect();
|
||||
for n in doomed {
|
||||
self.alive.remove(&n);
|
||||
}
|
||||
}
|
||||
|
||||
fn held_by(&self, pid: ProcessId) -> Vec<(usize, CapSlot)> {
|
||||
self.alive
|
||||
.iter()
|
||||
.filter(|(_, (p, _))| *p == pid)
|
||||
.map(|(id, (_, s))| (*id, *s))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn all() -> PermissionSet {
|
||||
PermissionSet::empty()
|
||||
.with(Permission::Read)
|
||||
.with(Permission::Write)
|
||||
.with(Permission::Execute)
|
||||
.with(Permission::Delegate)
|
||||
}
|
||||
|
||||
struct World {
|
||||
k: Kernel<MockPlatform>,
|
||||
m: Model,
|
||||
pids: [ProcessId; 3],
|
||||
tids: [ThreadId; 3],
|
||||
}
|
||||
|
||||
impl World {
|
||||
fn new() -> Self {
|
||||
let (k, a) = kernel_with_process();
|
||||
let ta = k.current_thread().unwrap();
|
||||
let mut w = Self {
|
||||
k,
|
||||
m: Model::default(),
|
||||
pids: [a; 3],
|
||||
tids: [ta; 3],
|
||||
};
|
||||
// init's own boot capability (ProcessCreate) is a kernel root.
|
||||
let create = CapSlot::new(0).unwrap();
|
||||
w.m.add((a, create), None, false);
|
||||
for i in 1..3 {
|
||||
w.spawn(i);
|
||||
}
|
||||
for call in [Syscall::CreateEndpoint, Syscall::CreateNotification] {
|
||||
let s = w.slot(0, call);
|
||||
w.m.add((a, s), None, false);
|
||||
}
|
||||
let r = w.slot(
|
||||
0,
|
||||
Syscall::CreateRegion {
|
||||
pages: PageCount::new(1).unwrap(),
|
||||
},
|
||||
);
|
||||
w.m.add((a, r), None, true);
|
||||
w
|
||||
}
|
||||
|
||||
fn slot(&mut self, who: usize, call: Syscall) -> CapSlot {
|
||||
self.k.switch_to_thread_for_test(self.tids[who]);
|
||||
match self.k.handle_syscall(call) {
|
||||
SyscallOutcome::Complete(SyscallResult::Slot(s)) => s,
|
||||
_ => panic!("expected a slot"),
|
||||
}
|
||||
}
|
||||
|
||||
/// (Re)creates process `i` from init; init gets a root process cap.
|
||||
fn spawn(&mut self, i: usize) {
|
||||
let cap = self.slot(
|
||||
0,
|
||||
Syscall::CreateProcess {
|
||||
create_cap: CapSlot::new(0).unwrap(),
|
||||
image: DataRef::empty(),
|
||||
},
|
||||
);
|
||||
self.pids[i] = self.k.process_of_cap_for_test(cap);
|
||||
self.tids[i] =
|
||||
self.k
|
||||
.spawn_thread_for_test(self.pids[i], Priority::DEFAULT, TimeSlice::DEFAULT);
|
||||
self.m.add((self.pids[0], cap), None, false);
|
||||
}
|
||||
|
||||
fn run(&mut self, who: usize, call: Syscall) -> bool {
|
||||
self.k.switch_to_thread_for_test(self.tids[who]);
|
||||
!matches!(
|
||||
self.k.handle_syscall(call),
|
||||
SyscallOutcome::Complete(SyscallResult::Error(_))
|
||||
)
|
||||
}
|
||||
|
||||
fn step(&mut self, rng: &mut Rng) {
|
||||
let who = rng.below(3);
|
||||
let pid = self.pids[who];
|
||||
let held = self.m.held_by(pid);
|
||||
let Some((id, slot)) = rng.pick(&held) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match rng.below(10) {
|
||||
0..=2 => {
|
||||
// Badges only go on endpoints and notifications; the kernel
|
||||
// must refuse the rest, so try both.
|
||||
let badge = Badge::new(rng.below(2));
|
||||
let before = self.k.cap_slots_for_test(pid);
|
||||
let ok = self.run(
|
||||
who,
|
||||
Syscall::Mint {
|
||||
slot,
|
||||
badge,
|
||||
permissions: all(),
|
||||
},
|
||||
);
|
||||
if ok {
|
||||
let new = newly_used(before, self.k.cap_slots_for_test(pid));
|
||||
let is_region = self.m.nodes[id].is_region;
|
||||
self.m.add((pid, new), Some(id), is_region);
|
||||
}
|
||||
}
|
||||
3..=4 => {
|
||||
// Delegate through any process cap the caller holds.
|
||||
let proc_caps: Vec<(CapSlot, ProcessId)> = held
|
||||
.iter()
|
||||
.filter_map(|(_, s)| {
|
||||
self.k.process_cap_target_for_test(pid, *s).map(|t| (*s, t))
|
||||
})
|
||||
.collect();
|
||||
if let Some((to, target)) = rng.pick(&proc_caps) {
|
||||
let before = self.k.cap_slots_for_test(target);
|
||||
let ok = self.run(
|
||||
who,
|
||||
Syscall::Delegate {
|
||||
slot,
|
||||
to_endpoint: to,
|
||||
permissions: all(),
|
||||
},
|
||||
);
|
||||
if ok {
|
||||
let new = newly_used(before, self.k.cap_slots_for_test(target));
|
||||
let is_region = self.m.nodes[id].is_region;
|
||||
self.m.add((target, new), Some(id), is_region);
|
||||
}
|
||||
}
|
||||
}
|
||||
// init keeps its ProcessCreate cap so processes can be re-made.
|
||||
5 if !(who == 0 && slot == CapSlot::new(0).unwrap()) => {
|
||||
if self.run(who, Syscall::DropCap { slot }) {
|
||||
self.m.delete(id);
|
||||
}
|
||||
}
|
||||
6..=7 => {
|
||||
assert!(
|
||||
self.run(who, Syscall::Revoke { slot }),
|
||||
"revoking a held cap failed"
|
||||
);
|
||||
self.m.revoke(id);
|
||||
}
|
||||
8 => {
|
||||
let va = VirtualAddress::from_raw(0x30000 + rng.below(16) * 0x1000);
|
||||
let _ = self.run(
|
||||
who,
|
||||
Syscall::MapRegion {
|
||||
region: slot,
|
||||
va,
|
||||
perms: PagePerms::ReadWrite,
|
||||
},
|
||||
);
|
||||
}
|
||||
_ if who == 0 && rng.below(4) == 0 => {
|
||||
let victim = 1 + rng.below(2);
|
||||
let vp = self.pids[victim];
|
||||
// init's cap to the victim may already be revoked; kill it
|
||||
// directly, as a supervisor holding a cap would.
|
||||
for (n, _) in self.m.held_by(vp) {
|
||||
self.m.delete(n);
|
||||
}
|
||||
self.k.destroy_process(vp);
|
||||
self.spawn(victim);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn check(&self) {
|
||||
for &pid in &self.pids {
|
||||
let used = self.k.cap_slots_for_test(pid);
|
||||
let mut expected = [false; 64];
|
||||
for (_, s) in self.m.held_by(pid) {
|
||||
expected[s.value() as usize] = true;
|
||||
}
|
||||
assert_eq!(used, expected, "{pid:?} holds the wrong capabilities");
|
||||
}
|
||||
// Every capability-backed mapping is backed by a live capability
|
||||
// of the mapping process.
|
||||
for (_, region) in self.k.regions.iter() {
|
||||
for m in region.mappings() {
|
||||
if let Some(via) = m.via {
|
||||
assert!(self.k.resolves(via), "mapping outlived its capability");
|
||||
assert_eq!(via.process, m.process);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn newly_used(before: [bool; 64], after: [bool; 64]) -> CapSlot {
|
||||
(0..64)
|
||||
.find(|&i| after[i] && !before[i])
|
||||
.and_then(|i| CapSlot::new(i as u8))
|
||||
.expect("a new slot")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derivation_tree_matches_the_model() {
|
||||
for seed in [5u64, 0xBADC0DE, 31337, 0x5EED, 2024] {
|
||||
let mut world = World::new();
|
||||
let mut rng = Rng(seed);
|
||||
let mut revokes = 0;
|
||||
for _ in 0..STEPS {
|
||||
let before = world.m.alive.len();
|
||||
world.step(&mut rng);
|
||||
if world.m.alive.len() < before {
|
||||
revokes += 1;
|
||||
}
|
||||
world.check();
|
||||
}
|
||||
assert!(
|
||||
revokes > 20,
|
||||
"seed {seed:#x}: too little deletion exercised"
|
||||
);
|
||||
assert!(
|
||||
world.m.nodes.len() > 100,
|
||||
"seed {seed:#x}: too little derivation"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,33 @@ impl Kernel<MockPlatform> {
|
||||
self.destroy_notifications_owned_by(owner);
|
||||
}
|
||||
|
||||
/// The process a Process cap in `pid`'s table names, if it is one.
|
||||
pub fn process_cap_target_for_test(&self, pid: ProcessId, slot: CapSlot) -> Option<ProcessId> {
|
||||
match self
|
||||
.processes
|
||||
.get(pid)
|
||||
.ok()?
|
||||
.capabilities()
|
||||
.get(slot)
|
||||
.ok()?
|
||||
.resource()
|
||||
{
|
||||
Resource::Process(target) if self.process_exists(*target) => Some(*target),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Which slots of `pid`'s capability table are occupied.
|
||||
pub fn cap_slots_for_test(&self, pid: ProcessId) -> [bool; 64] {
|
||||
let mut used = [false; 64];
|
||||
if let Ok(p) = self.processes.get(pid) {
|
||||
for (slot, _) in p.capabilities().iter() {
|
||||
used[slot.value() as usize] = true;
|
||||
}
|
||||
}
|
||||
used
|
||||
}
|
||||
|
||||
pub fn thread_state_for_test(&self, tid: ThreadId) -> Option<ThreadState> {
|
||||
self.threads.get(tid).ok().map(|t| *t.state())
|
||||
}
|
||||
|
||||
@@ -613,7 +613,7 @@ fn transfer_needs_delegate_on_the_transferred_cap() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cap_revoked_while_the_sender_waits_is_not_delivered() {
|
||||
fn a_cap_dropped_while_the_sender_waits_is_not_delivered() {
|
||||
let mut t = Ipc::new();
|
||||
let (a, ta, tb, a_send, b_recv) = (t.a, t.ta, t.tb, t.a_send, t.b_recv);
|
||||
let other = t.k.create_endpoint(a).unwrap();
|
||||
@@ -626,7 +626,8 @@ fn a_cap_revoked_while_the_sender_waits_is_not_delivered() {
|
||||
message: msg([0; 4]).with_cap(gift),
|
||||
},
|
||||
);
|
||||
t.k.revoke_resource(&Resource::Endpoint(other));
|
||||
// A is blocked sending; delete the cap as its death or a revoke would.
|
||||
t.k.delete_cap(a, gift).unwrap();
|
||||
let d = delivered(&t.run(tb, Syscall::Receive { endpoint: b_recv })).unwrap();
|
||||
assert_eq!(d.cap, None);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ mod helpers;
|
||||
mod mock_platform;
|
||||
|
||||
mod capability;
|
||||
mod captree_model;
|
||||
mod exit;
|
||||
mod invariants;
|
||||
mod ipc;
|
||||
@@ -10,6 +11,7 @@ mod isolation;
|
||||
mod lifecycle;
|
||||
mod lifetimes;
|
||||
mod memory;
|
||||
mod revocation;
|
||||
mod simple;
|
||||
mod tick;
|
||||
|
||||
|
||||
365
crates/kernel/src/tests/revocation.rs
Normal file
365
crates/kernel/src/tests/revocation.rs
Normal file
@@ -0,0 +1,365 @@
|
||||
//! Revocation through the capability derivation tree: `Revoke(cap)`
|
||||
//! deletes everything derived from `cap`, transitively, across processes,
|
||||
//! and keeps `cap`. Mappings made through a deleted capability go with it.
|
||||
|
||||
use super::*;
|
||||
use crate::{SyscallOutcome, SyscallResult};
|
||||
use domain::message::Message;
|
||||
use domain::resource::Resource;
|
||||
use domain::syscall::Syscall;
|
||||
use types::address::{DataRef, VirtualAddress};
|
||||
use types::badge::Badge;
|
||||
use types::bounded::{CapSlot, PageCount};
|
||||
use types::id::ThreadId;
|
||||
use types::mapping::PagePerms;
|
||||
use types::permission::{Permission, PermissionSet};
|
||||
|
||||
fn perms(ps: &[Permission]) -> PermissionSet {
|
||||
ps.iter().fold(PermissionSet::empty(), |s, p| s.with(*p))
|
||||
}
|
||||
|
||||
fn all() -> PermissionSet {
|
||||
perms(&[
|
||||
Permission::Read,
|
||||
Permission::Write,
|
||||
Permission::Execute,
|
||||
Permission::Delegate,
|
||||
])
|
||||
}
|
||||
|
||||
fn ok(outcome: &SyscallOutcome) -> bool {
|
||||
!matches!(outcome, SyscallOutcome::Complete(SyscallResult::Error(_)))
|
||||
}
|
||||
|
||||
fn slot_of(outcome: SyscallOutcome) -> CapSlot {
|
||||
match outcome {
|
||||
SyscallOutcome::Complete(SyscallResult::Slot(s)) => s,
|
||||
_ => panic!("expected a slot"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Init (A) plus B and C, each with one thread; A holds process caps to
|
||||
/// both and a root capability to an endpoint it owns.
|
||||
struct Tree {
|
||||
k: Kernel<MockPlatform>,
|
||||
a: ProcessId,
|
||||
b: ProcessId,
|
||||
c: ProcessId,
|
||||
ta: ThreadId,
|
||||
tb: ThreadId,
|
||||
a_to_b: CapSlot,
|
||||
a_to_c: CapSlot,
|
||||
root: CapSlot,
|
||||
}
|
||||
|
||||
impl Tree {
|
||||
fn new() -> Self {
|
||||
let (mut k, a) = kernel_with_process();
|
||||
let ta = k.current_thread().unwrap();
|
||||
let create = || Syscall::CreateProcess {
|
||||
create_cap: CapSlot::new(0).unwrap(),
|
||||
image: DataRef::empty(),
|
||||
};
|
||||
let a_to_b = slot_of(k.handle_syscall(create()));
|
||||
let a_to_c = slot_of(k.handle_syscall(create()));
|
||||
let (b, c) = (ProcessId::from_raw(1), ProcessId::from_raw(2));
|
||||
let tb = k.spawn_thread_for_test(b, Priority::DEFAULT, TimeSlice::DEFAULT);
|
||||
k.spawn_thread_for_test(c, Priority::DEFAULT, TimeSlice::DEFAULT);
|
||||
let root = slot_of(k.handle_syscall(Syscall::CreateEndpoint));
|
||||
Self {
|
||||
k,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
ta,
|
||||
tb,
|
||||
a_to_b,
|
||||
a_to_c,
|
||||
root,
|
||||
}
|
||||
}
|
||||
|
||||
fn run(&mut self, tid: ThreadId, call: Syscall) -> SyscallOutcome {
|
||||
self.k.switch_to_thread_for_test(tid);
|
||||
self.k.handle_syscall(call)
|
||||
}
|
||||
|
||||
fn mint(&mut self, tid: ThreadId, slot: CapSlot, badge: usize) -> CapSlot {
|
||||
slot_of(self.run(
|
||||
tid,
|
||||
Syscall::Mint {
|
||||
slot,
|
||||
badge: Badge::new(badge),
|
||||
permissions: all(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// Gives `slot` (in the caller's table) to the process behind `to`;
|
||||
/// returns where it landed there.
|
||||
fn give(&mut self, tid: ThreadId, slot: CapSlot, to: CapSlot, target: ProcessId) -> CapSlot {
|
||||
let before = self.k.cap_slots_for_test(target);
|
||||
assert!(ok(&self.run(
|
||||
tid,
|
||||
Syscall::Delegate {
|
||||
slot,
|
||||
to_endpoint: to,
|
||||
permissions: all()
|
||||
}
|
||||
)));
|
||||
let after = self.k.cap_slots_for_test(target);
|
||||
(0..=CapSlot::MAX)
|
||||
.map(|n| CapSlot::new(n).unwrap())
|
||||
.find(|s| after[s.value() as usize] && !before[s.value() as usize])
|
||||
.expect("delegated cap")
|
||||
}
|
||||
|
||||
fn holds(&self, pid: ProcessId, slot: CapSlot) -> bool {
|
||||
self.k.cap_slots_for_test(pid)[slot.value() as usize]
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoke_deletes_minted_children_but_keeps_the_cap_itself() {
|
||||
let mut t = Tree::new();
|
||||
let (a, ta, root) = (t.a, t.ta, t.root);
|
||||
let m1 = t.mint(ta, root, 1);
|
||||
let m2 = t.mint(ta, root, 2);
|
||||
assert!(ok(&t.run(ta, Syscall::Revoke { slot: root })));
|
||||
assert!(!t.holds(a, m1) && !t.holds(a, m2));
|
||||
assert!(t.holds(a, root));
|
||||
// The root still works.
|
||||
assert!(ok(&t.run(
|
||||
ta,
|
||||
Syscall::Mint {
|
||||
slot: root,
|
||||
badge: Badge::new(3),
|
||||
permissions: all()
|
||||
}
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoke_reaches_grandchildren_in_other_processes() {
|
||||
let mut t = Tree::new();
|
||||
let (a, b, c, ta, tb, root, a_to_b) = (t.a, t.b, t.c, t.ta, t.tb, t.root, t.a_to_b);
|
||||
let m = t.mint(ta, root, 7);
|
||||
let in_b = t.give(ta, m, a_to_b, b);
|
||||
// B passes it on to C through a process cap A delegated to B.
|
||||
let b_to_c = t.give(ta, t.a_to_c, a_to_b, b);
|
||||
let in_c = t.give(tb, in_b, b_to_c, c);
|
||||
|
||||
assert!(ok(&t.run(ta, Syscall::Revoke { slot: m })));
|
||||
assert!(!t.holds(b, in_b), "child survived");
|
||||
assert!(!t.holds(c, in_c), "grandchild survived");
|
||||
assert!(t.holds(a, m) && t.holds(a, root));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoking_one_client_spares_another() {
|
||||
let mut t = Tree::new();
|
||||
let (b, c, ta, root, a_to_b, a_to_c) = (t.b, t.c, t.ta, t.root, t.a_to_b, t.a_to_c);
|
||||
let for_b = t.mint(ta, root, 1);
|
||||
let for_c = t.mint(ta, root, 2);
|
||||
let in_b = t.give(ta, for_b, a_to_b, b);
|
||||
let in_c = t.give(ta, for_c, a_to_c, c);
|
||||
assert!(ok(&t.run(ta, Syscall::Revoke { slot: for_b })));
|
||||
assert!(!t.holds(b, in_b));
|
||||
assert!(t.holds(c, in_c));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dropping_a_middleman_does_not_shield_the_grandchild() {
|
||||
let mut t = Tree::new();
|
||||
let (b, c, ta, tb, root, a_to_b) = (t.b, t.c, t.ta, t.tb, t.root, t.a_to_b);
|
||||
let m = t.mint(ta, root, 7);
|
||||
let in_b = t.give(ta, m, a_to_b, b);
|
||||
let b_to_c = t.give(ta, t.a_to_c, a_to_b, b);
|
||||
let in_c = t.give(tb, in_b, b_to_c, c);
|
||||
|
||||
assert!(ok(&t.run(tb, Syscall::DropCap { slot: in_b })));
|
||||
assert!(ok(&t.run(ta, Syscall::Revoke { slot: m })));
|
||||
assert!(!t.holds(c, in_c), "grandchild escaped revocation");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dying_middleman_does_not_shield_the_grandchild() {
|
||||
let mut t = Tree::new();
|
||||
let (b, c, ta, tb, root, a_to_b) = (t.b, t.c, t.ta, t.tb, t.root, t.a_to_b);
|
||||
let m = t.mint(ta, root, 7);
|
||||
let in_b = t.give(ta, m, a_to_b, b);
|
||||
let b_to_c = t.give(ta, t.a_to_c, a_to_b, b);
|
||||
let in_c = t.give(tb, in_b, b_to_c, c);
|
||||
|
||||
assert!(ok(&t.run(ta, Syscall::Kill { process: a_to_b })));
|
||||
assert!(ok(&t.run(ta, Syscall::Revoke { slot: m })));
|
||||
assert!(!t.holds(c, in_c), "grandchild escaped revocation");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_capability_sent_over_ipc_is_revocable() {
|
||||
let mut t = Tree::new();
|
||||
let (a, b, ta, tb, root, a_to_b) = (t.a, t.b, t.ta, t.tb, t.root, t.a_to_b);
|
||||
let ep = t.k.create_endpoint(a).unwrap();
|
||||
let send = t.k.grant_cap_for_test(a, Resource::Endpoint(ep), all());
|
||||
let recv = t.k.grant_cap_for_test(b, Resource::Endpoint(ep), all());
|
||||
let gift = t.mint(ta, root, 9);
|
||||
t.run(tb, Syscall::Receive { endpoint: recv });
|
||||
t.run(
|
||||
ta,
|
||||
Syscall::Send {
|
||||
endpoint: send,
|
||||
message: Message::empty().with_cap(gift),
|
||||
},
|
||||
);
|
||||
let received = match t.k.take_pending_result(tb) {
|
||||
Some(SyscallResult::Received(d)) => d.cap.expect("cap transferred"),
|
||||
_ => panic!("B got nothing"),
|
||||
};
|
||||
assert!(ok(&t.run(ta, Syscall::Revoke { slot: gift })));
|
||||
assert!(!t.holds(b, received));
|
||||
let _ = a_to_b;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_holder_can_only_revoke_its_own_subtree() {
|
||||
let mut t = Tree::new();
|
||||
let (a, b, c, ta, tb, root, a_to_b) = (t.a, t.b, t.c, t.ta, t.tb, t.root, t.a_to_b);
|
||||
let m = t.mint(ta, root, 7);
|
||||
let in_b = t.give(ta, m, a_to_b, b);
|
||||
let b_to_c = t.give(ta, t.a_to_c, a_to_b, b);
|
||||
let in_c = t.give(tb, in_b, b_to_c, c);
|
||||
let sibling = t.mint(ta, root, 8);
|
||||
|
||||
assert!(ok(&t.run(tb, Syscall::Revoke { slot: in_b })));
|
||||
assert!(!t.holds(c, in_c));
|
||||
assert!(t.holds(b, in_b));
|
||||
assert!(t.holds(a, m) && t.holds(a, sibling) && t.holds(a, root));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoking_an_empty_slot_is_an_error() {
|
||||
let mut t = Tree::new();
|
||||
let ta = t.ta;
|
||||
assert!(!ok(&t.run(
|
||||
ta,
|
||||
Syscall::Revoke {
|
||||
slot: CapSlot::new(40).unwrap()
|
||||
}
|
||||
)));
|
||||
}
|
||||
|
||||
// ---- memory ----
|
||||
|
||||
fn region(t: &mut Tree, tid: ThreadId, pages: u16) -> CapSlot {
|
||||
slot_of(t.run(
|
||||
tid,
|
||||
Syscall::CreateRegion {
|
||||
pages: PageCount::new(pages).unwrap(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn map(t: &mut Tree, tid: ThreadId, cap: CapSlot, va: usize, p: PagePerms) -> bool {
|
||||
ok(&t.run(
|
||||
tid,
|
||||
Syscall::MapRegion {
|
||||
region: cap,
|
||||
va: VirtualAddress::from_raw(va),
|
||||
perms: p,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_region_maps_every_page() {
|
||||
let mut t = Tree::new();
|
||||
let (a, ta) = (t.a, t.ta);
|
||||
let r = region(&mut t, ta, 2);
|
||||
assert!(map(&mut t, ta, r, 0x30000, PagePerms::ReadWrite));
|
||||
let p0 = t.k.platform_mut().mapping(a, 0x30000).unwrap();
|
||||
assert_eq!(t.k.platform_mut().mapping(a, 0x31000), Some(p0 + 4096));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_region_needs_the_matching_permissions() {
|
||||
let mut t = Tree::new();
|
||||
let (a, ta) = (t.a, t.ta);
|
||||
let r = region(&mut t, ta, 1);
|
||||
let read_only = slot_of(t.run(
|
||||
ta,
|
||||
Syscall::Mint {
|
||||
slot: r,
|
||||
badge: Badge::NONE,
|
||||
permissions: perms(&[Permission::Read]),
|
||||
},
|
||||
));
|
||||
assert!(!map(&mut t, ta, read_only, 0x30000, PagePerms::ReadWrite));
|
||||
assert!(!map(&mut t, ta, read_only, 0x30000, PagePerms::ReadExecute));
|
||||
assert!(map(&mut t, ta, read_only, 0x30000, PagePerms::ReadOnly));
|
||||
assert_eq!(
|
||||
t.k.platform_mut().perms(a, 0x30000),
|
||||
Some(PagePerms::ReadOnly)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoking_a_region_cap_unmaps_it_from_every_derived_holder() {
|
||||
let mut t = Tree::new();
|
||||
let (a, b, ta, tb, a_to_b) = (t.a, t.b, t.ta, t.tb, t.a_to_b);
|
||||
let r = region(&mut t, ta, 1);
|
||||
assert!(map(&mut t, ta, r, 0x30000, PagePerms::ReadWrite));
|
||||
let in_b = t.give(ta, r, a_to_b, b);
|
||||
assert!(map(&mut t, tb, in_b, 0x50000, PagePerms::ReadWrite));
|
||||
assert!(t.k.platform_mut().mapping(b, 0x50000).is_some());
|
||||
|
||||
assert!(ok(&t.run(ta, Syscall::Revoke { slot: r })));
|
||||
assert_eq!(
|
||||
t.k.platform_mut().mapping(b, 0x50000),
|
||||
None,
|
||||
"revoked mapping still live"
|
||||
);
|
||||
assert!(
|
||||
t.k.platform_mut().mapping(a, 0x30000).is_some(),
|
||||
"revoker lost its own mapping"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dropping_the_cap_a_mapping_came_through_unmaps_it() {
|
||||
let mut t = Tree::new();
|
||||
let (a, ta) = (t.a, t.ta);
|
||||
let r = region(&mut t, ta, 1);
|
||||
let copy = slot_of(t.run(
|
||||
ta,
|
||||
Syscall::Mint {
|
||||
slot: r,
|
||||
badge: Badge::NONE,
|
||||
permissions: all(),
|
||||
},
|
||||
));
|
||||
assert!(map(&mut t, ta, copy, 0x30000, PagePerms::ReadWrite));
|
||||
assert!(ok(&t.run(ta, Syscall::DropCap { slot: copy })));
|
||||
assert_eq!(t.k.platform_mut().mapping(a, 0x30000), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_region_can_be_remapped_after_its_mapping_was_revoked() {
|
||||
let mut t = Tree::new();
|
||||
let (b, ta, tb, a_to_b) = (t.b, t.ta, t.tb, t.a_to_b);
|
||||
let r = region(&mut t, ta, 1);
|
||||
let in_b = t.give(ta, r, a_to_b, b);
|
||||
assert!(map(&mut t, tb, in_b, 0x50000, PagePerms::ReadWrite));
|
||||
t.run(ta, Syscall::Revoke { slot: r });
|
||||
let again = t.give(ta, r, a_to_b, b);
|
||||
assert!(map(&mut t, tb, again, 0x50000, PagePerms::ReadWrite));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_region_is_charged_to_the_caller() {
|
||||
let mut t = Tree::new();
|
||||
let (a, ta) = (t.a, t.ta);
|
||||
let before = t.k.page_usage(a);
|
||||
region(&mut t, ta, 3);
|
||||
assert_eq!(t.k.page_usage(a), before + 3);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use types::address::{EntryPoint, StackPointer, VirtualAddress};
|
||||
use types::badge::Badge;
|
||||
use types::bounded::{CapSlot, PageCount};
|
||||
use types::id::ProcessId;
|
||||
use types::mapping::PagePerms;
|
||||
use types::permission::PermissionSet;
|
||||
use types::syscall_abi::*;
|
||||
use types::time::Ticks;
|
||||
@@ -180,6 +181,30 @@ pub fn map_page(
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Allocates `pages` of physically contiguous shared memory, charged to
|
||||
/// this process, and returns a capability to it.
|
||||
pub fn create_region(pages: PageCount) -> Result<CapSlot> {
|
||||
slot_of(raw(
|
||||
SYS_CREATE_REGION,
|
||||
[pages.value() as usize, 0, 0, 0, 0, 0],
|
||||
))
|
||||
}
|
||||
|
||||
/// Maps the region behind `region` at `va`. The mapping lasts as long as
|
||||
/// the capability: dropping or having it revoked unmaps it.
|
||||
pub fn map_region(region: CapSlot, va: VirtualAddress, perms: PagePerms) -> Result<()> {
|
||||
let perms = match perms {
|
||||
PagePerms::ReadOnly => PERMS_READ_ONLY,
|
||||
PagePerms::ReadWrite => PERMS_READ_WRITE,
|
||||
PagePerms::ReadExecute => PERMS_READ_EXECUTE,
|
||||
};
|
||||
raw(
|
||||
SYS_MAP_REGION,
|
||||
[region.value() as usize, va.raw(), perms, 0, 0, 0],
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
// ---- capabilities ----
|
||||
|
||||
pub fn delegate(slot: CapSlot, to: CapSlot, permissions: PermissionSet) -> bool {
|
||||
@@ -213,6 +238,8 @@ pub fn mint(slot: CapSlot, badge: Badge, permissions: PermissionSet) -> Result<C
|
||||
))
|
||||
}
|
||||
|
||||
/// Deletes every capability derived from `slot` (in any process) and keeps
|
||||
/// `slot` itself.
|
||||
pub fn revoke(slot: CapSlot) -> bool {
|
||||
raw(SYS_REVOKE, [slot.value() as usize, 0, 0, 0, 0, 0]).is_ok()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ pub enum Permission {
|
||||
Write,
|
||||
Execute,
|
||||
Delegate,
|
||||
Revoke,
|
||||
}
|
||||
|
||||
impl Permission {
|
||||
|
||||
@@ -34,6 +34,13 @@ pub const SYS_WAIT: usize = 29;
|
||||
pub const SYS_POLL: usize = 30;
|
||||
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;
|
||||
|
||||
/// `PagePerms` in a register.
|
||||
pub const PERMS_READ_ONLY: usize = 0;
|
||||
pub const PERMS_READ_WRITE: usize = 1;
|
||||
pub const PERMS_READ_EXECUTE: usize = 2;
|
||||
|
||||
/// "No capability" in a capability argument or result register.
|
||||
pub const NONE: usize = usize::MAX;
|
||||
|
||||
21
docs/ipc.md
21
docs/ipc.md
@@ -19,8 +19,25 @@ Both are named by capabilities whose generation must match (see P2).
|
||||
## Capabilities
|
||||
|
||||
A capability to an endpoint or notification carries a **badge** (a word;
|
||||
`0` means unbadged). `Mint` copies an unbadged capability with a badge and
|
||||
narrower permissions. A badge cannot be changed once set.
|
||||
`0` means unbadged). `Mint` copies a capability with narrower permissions
|
||||
and, for endpoints and notifications, a badge. A badge cannot be changed
|
||||
once set: a badged capability cannot be minted again.
|
||||
|
||||
## Derivation and revocation
|
||||
|
||||
Every capability made from another (by `Mint`, `Delegate` or an IPC
|
||||
transfer) records it as its **parent**; capabilities the kernel creates
|
||||
directly have none. `Revoke(cap)` deletes every capability derived from
|
||||
`cap`, transitively and across processes, and keeps `cap` itself. Any
|
||||
holder may revoke, but only its own subtree.
|
||||
|
||||
Deleting a capability (`DropCap`, or its process dying) makes its parent
|
||||
adopt its children, so stepping out of the chain never shields anyone
|
||||
from a revocation further up.
|
||||
|
||||
A region mapping made with `MapRegion(cap, va, perms)` lasts only as long
|
||||
as `cap`: deleting or revoking `cap` unmaps it. `perms` needs `Read`, plus
|
||||
`Write` for read-write or `Execute` for read-execute.
|
||||
|
||||
| Object | Permission | Allows |
|
||||
|--------------|------------|--------------------------------------------|
|
||||
|
||||
Reference in New Issue
Block a user