byteflow/scheduler/capability.rs
1//! Unforgeable flow capabilities (security phase 2 — FlowCap).
2//!
3//! # Why Caps exist
4//!
5//! After authenticated sender (S1), bytecode still needed an *address* to
6//! deliver hops. Using raw [`FlowId`] / [`crate::Value::Pid`] as that address
7//! meant any module that could learn or guess a numeric id could talk to
8//! that flow — Pid was an ambient authority token.
9//!
10//! A [`CapId`] is an opaque handle minted only by the runtime. Guessing the
11//! next integer does not grant `Send` / `Ask`: resolution goes through
12//! [`CapTable`] under the same fail-closed mutex policy as the rest of the
13//! scheduler. Rights are attenuated at mint time (`SEND`, `ASK`, or both).
14//!
15//! # Split with Pid
16//!
17//! | Value | Role |
18//! |-------|------|
19//! | [`crate::Value::Cap`] | Address for bytecode `Send` / `Ask` |
20//! | [`crate::Value::Pid`] | Identity inside `Message.sender` / `msg_sender` |
21//!
22//! Reply path: stamped hops carry `Message.reply_cap` (SEND-only Cap back to
23//! the sender). Receivers answer with `msg_reply_cap`, never by treating
24//! `msg_sender` as a delivery address.
25//!
26//! See `docs/security.md` (S6) and `docs/atomic-hop.md`.
27
28use std::collections::HashMap;
29use std::sync::atomic::{AtomicU64, Ordering};
30use std::sync::Mutex;
31
32use super::error::RuntimeError;
33use super::process::FlowId;
34use super::sync_lock;
35
36/// Opaque capability identifier carried in [`crate::Value::Cap`].
37///
38/// Never equal to a [`FlowId`] by construction (independent counter). Do not
39/// compare CapIds to FlowIds; Ask correlation uses the **resolved** FlowId.
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
41pub struct CapId(pub(crate) u64);
42
43impl CapId {
44 #[inline]
45 pub fn as_u64(self) -> u64 {
46 self.0
47 }
48}
49
50impl std::fmt::Display for CapId {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 write!(f, "cap#{}", self.0)
53 }
54}
55
56/// Cap ids start at 1 so `0` can mean “no grant” on `Message.reply_cap`
57/// (host-injected hops / unauthenticated `make_msg` placeholders).
58static NEXT_CAP_ID: AtomicU64 = AtomicU64::new(1);
59
60/// Rights attached to a capability (bitflags as `u8`).
61///
62/// Attenuation is mint-time only in this revision: there is no runtime
63/// `attenuate()` that narrows an existing Cap in place. Mint a new Cap with
64/// fewer bits instead (e.g. reply grants use [`CapRights::SEND`] alone).
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub struct CapRights(u8);
67
68impl CapRights {
69 pub const SEND: CapRights = CapRights(0b01);
70 pub const ASK: CapRights = CapRights(0b10);
71 pub const SEND_ASK: CapRights = CapRights(0b11);
72
73 #[inline]
74 pub fn contains(self, other: CapRights) -> bool {
75 self.0 & other.0 == other.0
76 }
77
78 #[inline]
79 pub fn bits(self) -> u8 {
80 self.0
81 }
82}
83
84/// One live capability entry.
85#[derive(Clone, Copy, Debug)]
86pub struct CapEntry {
87 /// Flow this Cap authorizes delivery to.
88 pub flow: FlowId,
89 pub rights: CapRights,
90}
91
92/// Registry `CapId → { FlowId, CapRights }`.
93///
94/// Lives on [`super::runtime::Shared`] next to [`super::directory::Directory`]:
95/// Directory answers “where is this flow’s mailbox?”; CapTable answers
96/// “does this Cap authorize Send/Ask to some flow?”.
97///
98/// On flow exit the worker calls [`CapTable::revoke_target`] so Caps that
99/// pointed at the dead flow stop resolving (fail-closed delivery).
100pub struct CapTable {
101 inner: Mutex<HashMap<CapId, CapEntry>>,
102}
103
104impl CapTable {
105 pub fn new() -> Self {
106 Self {
107 inner: Mutex::new(HashMap::new()),
108 }
109 }
110
111 /// Mint a new capability targeting `flow` with `rights`.
112 ///
113 /// Used for: self Cap (`SelfPid`), child Cap (`Spawn`), and per-hop
114 /// `reply_cap` (SEND-only back to the sender).
115 pub fn mint(&self, flow: FlowId, rights: CapRights) -> Result<CapId, RuntimeError> {
116 let id = CapId(NEXT_CAP_ID.fetch_add(1, Ordering::Relaxed));
117 sync_lock::lock(&self.inner, "CapTable::mint")?.insert(
118 id,
119 CapEntry { flow, rights },
120 );
121 Ok(id)
122 }
123
124 /// Resolve a capability. `None` if unknown / revoked.
125 pub fn resolve(&self, id: CapId) -> Result<Option<CapEntry>, RuntimeError> {
126 Ok(sync_lock::lock(&self.inner, "CapTable::resolve")?
127 .get(&id)
128 .copied())
129 }
130
131 /// Drop every capability whose **target** is `flow` (flow exited).
132 ///
133 /// Caps *held by* other flows that pointed here become dead; Caps this
134 /// flow held to *other* targets are not swept here (they die when those
135 /// targets exit, or leak until then — acceptable for the MVP table).
136 pub fn revoke_target(&self, flow: FlowId) -> Result<(), RuntimeError> {
137 let mut g = sync_lock::lock(&self.inner, "CapTable::revoke_target")?;
138 g.retain(|_, e| e.flow != flow);
139 Ok(())
140 }
141}
142
143impl Default for CapTable {
144 fn default() -> Self {
145 Self::new()
146 }
147}