Skip to main content

byteflow/scheduler/
capability.rs

1//! Runtime-local capability table (Phase 3 — FlowCap + attenuate).
2//!
3//! A [`CapId`] is an opaque 128-bit token, **not** an authorization.
4//! Authorization lives in [`Cap`], owned by one [`crate::Runtime`]:
5//!
6//! ```text
7//! CapId  →  { holder, Cap { target, rights, native_mask, epoch } }
8//! ```
9//!
10//! Every derived grant goes through [`Cap::attenuate`]. `mint` is the
11//! trusted-runtime root path (self Cap, spawn addressing, reply_cap).
12//!
13//! See `docs/security.md` (S6 / S7) and `docs/atomic-hop.md`.
14
15use std::collections::HashMap;
16use std::sync::{Arc, Mutex};
17
18use crate::bytecode::{Cap, CapId, CapTarget, NativeMask, RevocationCell};
19
20pub use crate::bytecode::CapRights;
21
22use super::error::RuntimeError;
23use super::process::FlowId;
24use super::sync_lock;
25
26const MINT_ATTEMPTS: u32 = 32;
27
28/// One live capability entry. The [`CapId`] is the map key, not a field.
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct Capability {
31    /// Flow allowed to *use* this token.
32    pub holder: FlowId,
33    pub cap: Cap,
34}
35
36impl Capability {
37    /// Flow this Cap addresses, if the target is a flow.
38    pub fn target(&self) -> Option<FlowId> {
39        self.cap.target.flow_id().map(FlowId)
40    }
41
42    pub fn rights(&self) -> CapRights {
43        self.cap.rights
44    }
45}
46
47/// Why [`CapTable::resolve`] / [`CapTable::delegate`] refused.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum CapError {
50    Unknown,
51    NotHolder,
52    InsufficientRights,
53    WrongTarget,
54    /// Mutex poison — fail closed (never `into_inner`).
55    Unavailable,
56}
57
58impl std::fmt::Display for CapError {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        match self {
61            CapError::Unknown => f.write_str("unknown or revoked capability"),
62            CapError::NotHolder => f.write_str("calling flow does not hold this capability"),
63            CapError::InsufficientRights => f.write_str("capability lacks required rights"),
64            CapError::WrongTarget => f.write_str("capability target is not a flow"),
65            CapError::Unavailable => f.write_str("capability table unavailable (poisoned lock)"),
66        }
67    }
68}
69
70impl std::error::Error for CapError {}
71
72impl From<RuntimeError> for CapError {
73    fn from(_err: RuntimeError) -> Self {
74        CapError::Unavailable
75    }
76}
77
78struct CapTableInner {
79    entries: HashMap<CapId, Capability>,
80    flow_cells: HashMap<u64, Arc<RevocationCell>>,
81}
82
83/// Registry `CapId → Capability`, owned by one runtime's [`super::runtime::Shared`].
84pub struct CapTable {
85    inner: Mutex<CapTableInner>,
86    native_cell: Arc<RevocationCell>,
87    scheduler_cell: Arc<RevocationCell>,
88}
89
90impl CapTable {
91    pub fn new() -> Self {
92        Self {
93            inner: Mutex::new(CapTableInner {
94                entries: HashMap::new(),
95                flow_cells: HashMap::new(),
96            }),
97            native_cell: Arc::new(RevocationCell::new()),
98            scheduler_cell: Arc::new(RevocationCell::new()),
99        }
100    }
101
102    pub fn native_cell(&self) -> Arc<RevocationCell> {
103        Arc::clone(&self.native_cell)
104    }
105
106    fn lock(&self, where_: &'static str) -> Result<std::sync::MutexGuard<'_, CapTableInner>, RuntimeError> {
107        sync_lock::lock(&self.inner, where_)
108    }
109
110    /// Get-or-create the revocation cell for `flow`.
111    pub fn bind_flow(&self, flow: FlowId) -> Result<Arc<RevocationCell>, RuntimeError> {
112        let mut g = self.lock("CapTable::bind_flow")?;
113        Ok(g.flow_cells
114            .entry(flow.as_u64())
115            .or_insert_with(|| Arc::new(RevocationCell::new()))
116            .clone())
117    }
118
119    pub fn flow_cell(&self, flow: FlowId) -> Result<Option<Arc<RevocationCell>>, RuntimeError> {
120        Ok(self.lock("CapTable::flow_cell")?.flow_cells.get(&flow.as_u64()).cloned())
121    }
122
123    pub fn scheduler_cell(&self) -> Arc<RevocationCell> {
124        Arc::clone(&self.scheduler_cell)
125    }
126
127    fn insert_fresh(
128        table: &mut HashMap<CapId, Capability>,
129        entry: Capability,
130    ) -> Result<CapId, RuntimeError> {
131        for _ in 0..MINT_ATTEMPTS {
132            let id = CapId::random().map_err(|_| RuntimeError::EntropyFailed)?;
133            if id.is_none() || table.contains_key(&id) {
134                continue;
135            }
136            table.insert(id, entry);
137            return Ok(id);
138        }
139        Err(RuntimeError::CapIdCollision)
140    }
141
142    fn insert(&self, holder: FlowId, cap: Cap) -> Result<CapId, RuntimeError> {
143        let mut table = self.lock("CapTable::insert")?;
144        Self::insert_fresh(
145            &mut table.entries,
146            Capability { holder, cap },
147        )
148    }
149
150    /// Trusted root mint: `holder` may address `target` with `rights`.
151    ///
152    /// Used for: self Cap (`SelfPid`), child addressing Cap (`Spawn`),
153    /// and per-hop `reply_cap` (holder = recipient, SEND-only back to sender).
154    pub fn mint(
155        &self,
156        holder: FlowId,
157        target: FlowId,
158        rights: CapRights,
159    ) -> Result<CapId, RuntimeError> {
160        let cell = self.bind_flow(target)?;
161        let cap = Cap::root(CapTarget::Flow(target.as_u64()), rights, None, cell.as_ref());
162        self.grant(holder, cap)
163    }
164
165    /// Insert a Cap that was already produced by [`Cap::attenuate`] or
166    /// [`Cap::root`] (trusted).
167    pub fn grant(&self, holder: FlowId, cap: Cap) -> Result<CapId, RuntimeError> {
168        self.insert(holder, cap)
169    }
170
171    /// Host / registry lookup: existence only, no holder check.
172    pub fn lookup(&self, id: CapId) -> Result<Option<Capability>, RuntimeError> {
173        if id.is_none() {
174            return Ok(None);
175        }
176        Ok(self.lock("CapTable::lookup")?.entries.get(&id).cloned())
177    }
178
179    /// Bytecode path: token + holder + rights + live epoch.
180    pub fn resolve(
181        &self,
182        id: CapId,
183        holder: FlowId,
184        required: CapRights,
185    ) -> Result<Capability, CapError> {
186        if id.is_none() {
187            return Err(CapError::Unknown);
188        }
189        let table = self.lock("CapTable::resolve")?;
190        let entry = table.entries.get(&id).cloned().ok_or(CapError::Unknown)?;
191        if entry.holder != holder {
192            return Err(CapError::NotHolder);
193        }
194        if !entry.cap.rights.contains(required) {
195            return Err(CapError::InsufficientRights);
196        }
197        let valid = match entry.cap.target {
198            CapTarget::Flow(fid) => match table.flow_cells.get(&fid) {
199                Some(cell) => entry.cap.is_valid(cell.as_ref()),
200                None => false,
201            },
202            CapTarget::NativeTable => entry.cap.is_valid(self.native_cell.as_ref()),
203            CapTarget::Scheduler => entry.cap.is_valid(self.scheduler_cell.as_ref()),
204        };
205        if !valid {
206            return Err(CapError::Unknown);
207        }
208        Ok(entry)
209    }
210
211    /// New token, attenuated rights, `to_holder` becomes the holder.
212    /// Sole bytecode derivation path — calls [`Cap::attenuate`].
213    pub fn attenuate(
214        &self,
215        id: CapId,
216        from_holder: FlowId,
217        to_holder: FlowId,
218        want_rights: CapRights,
219        want_native: Option<&NativeMask>,
220    ) -> Result<CapId, CapError> {
221        let src = self.resolve(id, from_holder, CapRights::empty())?;
222        let cell = match src.cap.target {
223            CapTarget::Flow(fid) => self
224                .lock("CapTable::attenuate")?
225                .flow_cells
226                .get(&fid)
227                .cloned()
228                .ok_or(CapError::Unknown)?,
229            CapTarget::NativeTable | CapTarget::Scheduler => {
230                return Err(CapError::WrongTarget);
231            }
232        };
233        let narrowed = match super::delegate::exec_delegate(
234            &src.cap,
235            cell.as_ref(),
236            want_rights,
237            want_native,
238        ) {
239            Ok(c) => c,
240            Err(super::delegate::DelegateError::SourceRevoked) => {
241                return Err(CapError::Unknown)
242            }
243            Err(super::delegate::DelegateError::SourceLacksNative) => {
244                return Err(CapError::InsufficientRights)
245            }
246        };
247        if !narrowed.is_valid(cell.as_ref()) {
248            return Err(CapError::Unknown);
249        }
250        self.insert(to_holder, narrowed).map_err(CapError::from)
251    }
252
253    /// New token, same target/rights (attenuation with `want = src.rights`).
254    pub fn delegate(
255        &self,
256        id: CapId,
257        from_holder: FlowId,
258        to_holder: FlowId,
259    ) -> Result<CapId, CapError> {
260        let src = self.resolve(id, from_holder, CapRights::empty())?;
261        self.attenuate(id, from_holder, to_holder, src.cap.rights, src.cap.native_mask.as_ref())
262    }
263
264    /// Host spawn: re-issue a live Cap so `new_holder` can use it.
265    /// Does not require the caller to be the current holder (trusted host).
266    /// Still goes through [`Cap::attenuate`] (identity attenuation).
267    pub fn reissue_for(&self, id: CapId, new_holder: FlowId) -> Result<CapId, CapError> {
268        let cap = self.lookup(id)?.ok_or(CapError::Unknown)?;
269        let narrowed = cap.cap.attenuate(cap.cap.rights, cap.cap.native_mask.as_ref());
270        self.insert(new_holder, narrowed).map_err(CapError::from)
271    }
272
273    /// Drop every capability held by or targeting `flow` (flow exited).
274    /// Bumps the flow's epoch first so leaked tokens fail in O(1).
275    pub fn revoke_flow(&self, flow: FlowId) -> Result<usize, RuntimeError> {
276        let mut g = self.lock("CapTable::revoke_flow")?;
277        if let Some(cell) = g.flow_cells.get(&flow.as_u64()) {
278            cell.revoke();
279        }
280        g.flow_cells.remove(&flow.as_u64());
281        let before = g.entries.len();
282        let fid = flow.as_u64();
283        g.entries.retain(|_, e| {
284            e.holder != flow && e.cap.target != CapTarget::Flow(fid)
285        });
286        Ok(before - g.entries.len())
287    }
288}
289
290impl Default for CapTable {
291    fn default() -> Self {
292        Self::new()
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use crate::scheduler::process::next_flow_id;
300
301    #[test]
302    fn random_ids_are_not_sequential() -> Result<(), Box<dyn std::error::Error>> {
303        let table = CapTable::new();
304        let h = next_flow_id();
305        let t = next_flow_id();
306        let a = table.mint(h, t, CapRights::SEND)?;
307        let b = table.mint(h, t, CapRights::SEND)?;
308        assert_ne!(a, b);
309        assert!(!a.is_none());
310        assert_eq!(
311            table.resolve(CapId::from_raw(1), h, CapRights::SEND),
312            Err(CapError::Unknown)
313        );
314        Ok(())
315    }
316
317    #[test]
318    fn resolve_requires_holder_and_rights() -> Result<(), Box<dyn std::error::Error>> {
319        let table = CapTable::new();
320        let holder = next_flow_id();
321        let other = next_flow_id();
322        let target = next_flow_id();
323        let cap = table.mint(holder, target, CapRights::SEND)?;
324        assert_eq!(
325            table.resolve(cap, holder, CapRights::SEND)?.target(),
326            Some(target)
327        );
328        assert_eq!(
329            table.resolve(cap, other, CapRights::SEND),
330            Err(CapError::NotHolder)
331        );
332        assert_eq!(
333            table.resolve(cap, holder, CapRights::ASK),
334            Err(CapError::InsufficientRights)
335        );
336        Ok(())
337    }
338
339    #[test]
340    fn capability_isolation_is_per_table() -> Result<(), Box<dyn std::error::Error>> {
341        let a = CapTable::new();
342        let b = CapTable::new();
343        let holder = next_flow_id();
344        let target = next_flow_id();
345        let cap = a.mint(holder, target, CapRights::SEND)?;
346        assert_eq!(
347            b.resolve(cap, holder, CapRights::SEND),
348            Err(CapError::Unknown)
349        );
350        Ok(())
351    }
352
353    #[test]
354    fn finalizing_flow_revokes_held_and_targeted_caps() -> Result<(), Box<dyn std::error::Error>> {
355        let table = CapTable::new();
356        let dead = next_flow_id();
357        let alive = next_flow_id();
358        let target = next_flow_id();
359
360        let held_by_dead = table.mint(dead, target, CapRights::SEND)?;
361        let targeting_dead = table.mint(alive, dead, CapRights::SEND)?;
362        let unrelated = table.mint(alive, target, CapRights::SEND)?;
363
364        let removed = table.revoke_flow(dead)?;
365        assert_eq!(removed, 2);
366        assert_eq!(
367            table.resolve(held_by_dead, dead, CapRights::SEND),
368            Err(CapError::Unknown)
369        );
370        assert_eq!(
371            table.resolve(targeting_dead, alive, CapRights::SEND),
372            Err(CapError::Unknown)
373        );
374        assert!(table.resolve(unrelated, alive, CapRights::SEND).is_ok());
375        Ok(())
376    }
377
378    #[test]
379    fn delegate_issues_a_new_id_for_the_child() -> Result<(), Box<dyn std::error::Error>> {
380        let table = CapTable::new();
381        let parent = next_flow_id();
382        let child = next_flow_id();
383        let target = next_flow_id();
384        let original = table.mint(parent, target, CapRights::SEND_ASK)?;
385        let granted = table.delegate(original, parent, child)?;
386        assert_ne!(granted, original);
387        assert_eq!(
388            table.resolve(granted, child, CapRights::SEND)?.target(),
389            Some(target)
390        );
391        assert_eq!(
392            table.resolve(original, child, CapRights::SEND),
393            Err(CapError::NotHolder)
394        );
395        assert!(table.resolve(original, parent, CapRights::ASK).is_ok());
396        Ok(())
397    }
398
399    #[test]
400    fn attenuate_cannot_escalate() -> Result<(), Box<dyn std::error::Error>> {
401        let table = CapTable::new();
402        let parent = next_flow_id();
403        let child = next_flow_id();
404        let target = next_flow_id();
405        let original = table.mint(parent, target, CapRights::SEND)?;
406        let granted = table.attenuate(
407            original,
408            parent,
409            child,
410            CapRights::SEND.union(CapRights::ADMIN),
411            None,
412        )?;
413        let got = table.resolve(granted, child, CapRights::SEND)?;
414        assert!(!got.rights().contains(CapRights::ADMIN));
415        assert!(got.rights().contains(CapRights::SEND));
416        Ok(())
417    }
418}