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). Hop `reply_cap`
12//! uses [`CapTable::mint_or_reuse`] — one live SEND token per
13//! `(holder, target)` pair.
14//!
15//! See `docs/security.md` (S6 / S7) and `docs/atomic-hop.md`.
16
17use std::collections::HashMap;
18use std::sync::{Arc, Mutex};
19
20use crate::bytecode::{Cap, CapId, CapTarget, NativeMask, RevocationCell};
21
22pub use crate::bytecode::CapRights;
23
24use super::error::RuntimeError;
25use super::process::FlowId;
26use super::sync_lock;
27
28const MINT_ATTEMPTS: u32 = 32;
29
30/// One live capability entry. The [`CapId`] is the map key, not a field.
31#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct Capability {
33    /// Flow allowed to *use* this token.
34    pub holder: FlowId,
35    pub cap: Cap,
36}
37
38impl Capability {
39    /// Flow this Cap addresses, if the target is a flow.
40    pub fn target(&self) -> Option<FlowId> {
41        self.cap.target.flow_id().map(FlowId)
42    }
43
44    pub fn rights(&self) -> CapRights {
45        self.cap.rights
46    }
47}
48
49/// Why [`CapTable::resolve`] / [`CapTable::delegate`] refused.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum CapError {
52    Unknown,
53    NotHolder,
54    InsufficientRights,
55    WrongTarget,
56    /// Mutex poison — fail closed (never `into_inner`).
57    Unavailable,
58    /// A second in-flight `Ask` reused a `request_id` still pending.
59    DuplicateRequestId,
60}
61
62impl std::fmt::Display for CapError {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        match self {
65            CapError::Unknown => f.write_str("unknown or revoked capability"),
66            CapError::NotHolder => f.write_str("calling flow does not hold this capability"),
67            CapError::InsufficientRights => f.write_str("capability lacks required rights"),
68            CapError::WrongTarget => f.write_str("capability target is not a flow"),
69            CapError::Unavailable => f.write_str("capability table unavailable (poisoned lock)"),
70            CapError::DuplicateRequestId => {
71                f.write_str("duplicate in-flight request_id")
72            }
73        }
74    }
75}
76
77impl std::error::Error for CapError {}
78
79impl From<RuntimeError> for CapError {
80    fn from(_err: RuntimeError) -> Self {
81        CapError::Unavailable
82    }
83}
84
85struct CapTableInner {
86    entries: HashMap<CapId, Capability>,
87    flow_cells: HashMap<u64, Arc<RevocationCell>>,
88    /// Stable reply address: `(holder, target) → CapId`.
89    ///
90    /// Hop `reply_cap` is an *address* (SEND back to the sender), not a
91    /// per-message ticket. Correlation stays on `Message.request_id`.
92    reply_index: HashMap<(FlowId, FlowId), CapId>,
93}
94
95/// Registry `CapId → Capability`, owned by one runtime's [`super::runtime::Shared`].
96pub struct CapTable {
97    inner: Mutex<CapTableInner>,
98    native_cell: Arc<RevocationCell>,
99    scheduler_cell: Arc<RevocationCell>,
100}
101
102impl CapTable {
103    pub fn new() -> Self {
104        Self {
105            inner: Mutex::new(CapTableInner {
106                entries: HashMap::new(),
107                flow_cells: HashMap::new(),
108                reply_index: HashMap::new(),
109            }),
110            native_cell: Arc::new(RevocationCell::new()),
111            scheduler_cell: Arc::new(RevocationCell::new()),
112        }
113    }
114
115    pub fn native_cell(&self) -> Arc<RevocationCell> {
116        Arc::clone(&self.native_cell)
117    }
118
119    fn lock(&self, where_: &'static str) -> Result<std::sync::MutexGuard<'_, CapTableInner>, RuntimeError> {
120        sync_lock::lock(&self.inner, where_)
121    }
122
123    /// Get-or-create the revocation cell for `flow`.
124    pub fn bind_flow(&self, flow: FlowId) -> Result<Arc<RevocationCell>, RuntimeError> {
125        let mut g = self.lock("CapTable::bind_flow")?;
126        Ok(g.flow_cells
127            .entry(flow.as_u64())
128            .or_insert_with(|| Arc::new(RevocationCell::new()))
129            .clone())
130    }
131
132    pub fn flow_cell(&self, flow: FlowId) -> Result<Option<Arc<RevocationCell>>, RuntimeError> {
133        Ok(self.lock("CapTable::flow_cell")?.flow_cells.get(&flow.as_u64()).cloned())
134    }
135
136    pub fn scheduler_cell(&self) -> Arc<RevocationCell> {
137        Arc::clone(&self.scheduler_cell)
138    }
139
140    fn insert_fresh(
141        table: &mut HashMap<CapId, Capability>,
142        entry: Capability,
143    ) -> Result<CapId, RuntimeError> {
144        for _ in 0..MINT_ATTEMPTS {
145            let id = CapId::random().map_err(|_| RuntimeError::EntropyFailed)?;
146            if id.is_none() || table.contains_key(&id) {
147                continue;
148            }
149            table.insert(id, entry);
150            return Ok(id);
151        }
152        Err(RuntimeError::CapIdCollision)
153    }
154
155    fn insert(&self, holder: FlowId, cap: Cap) -> Result<CapId, RuntimeError> {
156        let mut table = self.lock("CapTable::insert")?;
157        Self::insert_fresh(
158            &mut table.entries,
159            Capability { holder, cap },
160        )
161    }
162
163    /// Trusted root mint: always a **new** token.
164    ///
165    /// Used for self Cap (`SelfPid`) and child addressing (`Spawn`). Hop
166    /// `reply_cap` must go through [`Self::mint_or_reuse`] so the table
167    /// stays O(pairs), not O(hops).
168    pub fn mint(
169        &self,
170        holder: FlowId,
171        target: FlowId,
172        rights: CapRights,
173    ) -> Result<CapId, RuntimeError> {
174        let cell = self.bind_flow(target)?;
175        let cap = Cap::root(CapTarget::Flow(target.as_u64()), rights, None, cell.as_ref());
176        self.grant(holder, cap)
177    }
178
179    /// Reply-address mint: reuse the live SEND Cap for `(holder, target)`.
180    ///
181    /// [`FlowId`] is never reused (`next_flow_id` is monotonic), so a cached
182    /// pair cannot alias a later incarnation. Stale index entries are dropped
183    /// after an epoch / holder / target check — never trusted blindly.
184    ///
185    /// One lock for lookup + insert so reuse is atomic on the existing
186    /// global mutex (no CAS / retry).
187    pub fn mint_or_reuse(
188        &self,
189        holder: FlowId,
190        target: FlowId,
191        rights: CapRights,
192    ) -> Result<CapId, RuntimeError> {
193        let mut g = self.lock("CapTable::mint_or_reuse")?;
194        if let Some(&id) = g.reply_index.get(&(holder, target)) {
195            if Self::reply_cap_is_live(&g, id, holder, target, rights) {
196                return Ok(id);
197            }
198            g.reply_index.remove(&(holder, target));
199        }
200        let cell = g
201            .flow_cells
202            .entry(target.as_u64())
203            .or_insert_with(|| Arc::new(RevocationCell::new()))
204            .clone();
205        let cap = Cap::root(CapTarget::Flow(target.as_u64()), rights, None, cell.as_ref());
206        let id = Self::insert_fresh(&mut g.entries, Capability { holder, cap })?;
207        g.reply_index.insert((holder, target), id);
208        Ok(id)
209    }
210
211    fn reply_cap_is_live(
212        table: &CapTableInner,
213        id: CapId,
214        holder: FlowId,
215        target: FlowId,
216        rights: CapRights,
217    ) -> bool {
218        let Some(entry) = table.entries.get(&id) else {
219            return false;
220        };
221        if entry.holder != holder {
222            return false;
223        }
224        if entry.cap.target != CapTarget::Flow(target.as_u64()) {
225            return false;
226        }
227        if !entry.cap.rights.contains(rights) {
228            return false;
229        }
230        match table.flow_cells.get(&target.as_u64()) {
231            Some(cell) => entry.cap.is_valid(cell.as_ref()),
232            None => false,
233        }
234    }
235
236    /// Insert a Cap that was already produced by [`Cap::attenuate`] or
237    /// [`Cap::root`] (trusted).
238    pub fn grant(&self, holder: FlowId, cap: Cap) -> Result<CapId, RuntimeError> {
239        self.insert(holder, cap)
240    }
241
242    /// Host / registry lookup: existence only, no holder check.
243    pub fn lookup(&self, id: CapId) -> Result<Option<Capability>, RuntimeError> {
244        if id.is_none() {
245            return Ok(None);
246        }
247        Ok(self.lock("CapTable::lookup")?.entries.get(&id).cloned())
248    }
249
250    /// Bytecode path: token + holder + rights + live epoch.
251    pub fn resolve(
252        &self,
253        id: CapId,
254        holder: FlowId,
255        required: CapRights,
256    ) -> Result<Capability, CapError> {
257        if id.is_none() {
258            return Err(CapError::Unknown);
259        }
260        let table = self.lock("CapTable::resolve")?;
261        let entry = table.entries.get(&id).cloned().ok_or(CapError::Unknown)?;
262        if entry.holder != holder {
263            return Err(CapError::NotHolder);
264        }
265        if !entry.cap.rights.contains(required) {
266            return Err(CapError::InsufficientRights);
267        }
268        let valid = match entry.cap.target {
269            CapTarget::Flow(fid) => match table.flow_cells.get(&fid) {
270                Some(cell) => entry.cap.is_valid(cell.as_ref()),
271                None => false,
272            },
273            CapTarget::NativeTable => entry.cap.is_valid(self.native_cell.as_ref()),
274            CapTarget::Scheduler => entry.cap.is_valid(self.scheduler_cell.as_ref()),
275        };
276        if !valid {
277            return Err(CapError::Unknown);
278        }
279        Ok(entry)
280    }
281
282    /// New token, attenuated rights, `to_holder` becomes the holder.
283    /// Sole bytecode derivation path — calls [`Cap::attenuate`].
284    pub fn attenuate(
285        &self,
286        id: CapId,
287        from_holder: FlowId,
288        to_holder: FlowId,
289        want_rights: CapRights,
290        want_native: Option<&NativeMask>,
291    ) -> Result<CapId, CapError> {
292        let src = self.resolve(id, from_holder, CapRights::empty())?;
293        let cell = match src.cap.target {
294            CapTarget::Flow(fid) => self
295                .lock("CapTable::attenuate")?
296                .flow_cells
297                .get(&fid)
298                .cloned()
299                .ok_or(CapError::Unknown)?,
300            CapTarget::NativeTable | CapTarget::Scheduler => {
301                return Err(CapError::WrongTarget);
302            }
303        };
304        let narrowed = match super::delegate::exec_delegate(
305            &src.cap,
306            cell.as_ref(),
307            want_rights,
308            want_native,
309        ) {
310            Ok(c) => c,
311            Err(super::delegate::DelegateError::SourceRevoked) => {
312                return Err(CapError::Unknown)
313            }
314            Err(super::delegate::DelegateError::SourceLacksNative) => {
315                return Err(CapError::InsufficientRights)
316            }
317        };
318        if !narrowed.is_valid(cell.as_ref()) {
319            return Err(CapError::Unknown);
320        }
321        self.insert(to_holder, narrowed).map_err(CapError::from)
322    }
323
324    /// New token, same target/rights (attenuation with `want = src.rights`).
325    pub fn delegate(
326        &self,
327        id: CapId,
328        from_holder: FlowId,
329        to_holder: FlowId,
330    ) -> Result<CapId, CapError> {
331        let src = self.resolve(id, from_holder, CapRights::empty())?;
332        self.attenuate(id, from_holder, to_holder, src.cap.rights, src.cap.native_mask.as_ref())
333    }
334
335    /// Host spawn: re-issue a live Cap so `new_holder` can use it.
336    /// Does not require the caller to be the current holder (trusted host).
337    /// Still goes through [`Cap::attenuate`] (identity attenuation).
338    pub fn reissue_for(&self, id: CapId, new_holder: FlowId) -> Result<CapId, CapError> {
339        let cap = self.lookup(id)?.ok_or(CapError::Unknown)?;
340        let narrowed = cap.cap.attenuate(cap.cap.rights, cap.cap.native_mask.as_ref());
341        self.insert(new_holder, narrowed).map_err(CapError::from)
342    }
343
344    /// Drop every capability held by or targeting `flow` (flow exited).
345    /// Bumps the flow's epoch first so leaked tokens fail in O(1).
346    pub fn revoke_flow(&self, flow: FlowId) -> Result<usize, RuntimeError> {
347        let mut g = self.lock("CapTable::revoke_flow")?;
348        if let Some(cell) = g.flow_cells.get(&flow.as_u64()) {
349            cell.revoke();
350        }
351        g.flow_cells.remove(&flow.as_u64());
352        let before = g.entries.len();
353        let fid = flow.as_u64();
354        g.entries.retain(|_, e| {
355            e.holder != flow && e.cap.target != CapTarget::Flow(fid)
356        });
357        g.reply_index.retain(|(h, t), _| *h != flow && *t != flow);
358        Ok(before - g.entries.len())
359    }
360
361    #[cfg(test)]
362    fn entry_count(&self) -> Result<usize, RuntimeError> {
363        Ok(self.lock("CapTable::entry_count")?.entries.len())
364    }
365
366    #[cfg(test)]
367    fn reply_index_len(&self) -> Result<usize, RuntimeError> {
368        Ok(self.lock("CapTable::reply_index_len")?.reply_index.len())
369    }
370
371    #[cfg(test)]
372    fn reply_index_mentions(&self, flow: FlowId) -> Result<bool, RuntimeError> {
373        Ok(self
374            .lock("CapTable::reply_index_mentions")?
375            .reply_index
376            .keys()
377            .any(|(h, t)| *h == flow || *t == flow))
378    }
379}
380
381impl Default for CapTable {
382    fn default() -> Self {
383        Self::new()
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390    use crate::scheduler::process::next_flow_id;
391
392    #[test]
393    fn random_ids_are_not_sequential() -> Result<(), Box<dyn std::error::Error>> {
394        let table = CapTable::new();
395        let h = next_flow_id();
396        let t = next_flow_id();
397        let a = table.mint(h, t, CapRights::SEND)?;
398        let b = table.mint(h, t, CapRights::SEND)?;
399        assert_ne!(a, b);
400        assert!(!a.is_none());
401        assert_eq!(
402            table.resolve(CapId::from_raw(1), h, CapRights::SEND),
403            Err(CapError::Unknown)
404        );
405        Ok(())
406    }
407
408    #[test]
409    fn resolve_requires_holder_and_rights() -> Result<(), Box<dyn std::error::Error>> {
410        let table = CapTable::new();
411        let holder = next_flow_id();
412        let other = next_flow_id();
413        let target = next_flow_id();
414        let cap = table.mint(holder, target, CapRights::SEND)?;
415        assert_eq!(
416            table.resolve(cap, holder, CapRights::SEND)?.target(),
417            Some(target)
418        );
419        assert_eq!(
420            table.resolve(cap, other, CapRights::SEND),
421            Err(CapError::NotHolder)
422        );
423        assert_eq!(
424            table.resolve(cap, holder, CapRights::ASK),
425            Err(CapError::InsufficientRights)
426        );
427        Ok(())
428    }
429
430    #[test]
431    fn capability_isolation_is_per_table() -> Result<(), Box<dyn std::error::Error>> {
432        let a = CapTable::new();
433        let b = CapTable::new();
434        let holder = next_flow_id();
435        let target = next_flow_id();
436        let cap = a.mint(holder, target, CapRights::SEND)?;
437        assert_eq!(
438            b.resolve(cap, holder, CapRights::SEND),
439            Err(CapError::Unknown)
440        );
441        Ok(())
442    }
443
444    #[test]
445    fn finalizing_flow_revokes_held_and_targeted_caps() -> Result<(), Box<dyn std::error::Error>> {
446        let table = CapTable::new();
447        let dead = next_flow_id();
448        let alive = next_flow_id();
449        let target = next_flow_id();
450
451        let held_by_dead = table.mint(dead, target, CapRights::SEND)?;
452        let targeting_dead = table.mint(alive, dead, CapRights::SEND)?;
453        let unrelated = table.mint(alive, target, CapRights::SEND)?;
454
455        let removed = table.revoke_flow(dead)?;
456        assert_eq!(removed, 2);
457        assert_eq!(
458            table.resolve(held_by_dead, dead, CapRights::SEND),
459            Err(CapError::Unknown)
460        );
461        assert_eq!(
462            table.resolve(targeting_dead, alive, CapRights::SEND),
463            Err(CapError::Unknown)
464        );
465        assert!(table.resolve(unrelated, alive, CapRights::SEND).is_ok());
466        Ok(())
467    }
468
469    #[test]
470    fn delegate_issues_a_new_id_for_the_child() -> Result<(), Box<dyn std::error::Error>> {
471        let table = CapTable::new();
472        let parent = next_flow_id();
473        let child = next_flow_id();
474        let target = next_flow_id();
475        let original = table.mint(parent, target, CapRights::SEND_ASK)?;
476        let granted = table.delegate(original, parent, child)?;
477        assert_ne!(granted, original);
478        assert_eq!(
479            table.resolve(granted, child, CapRights::SEND)?.target(),
480            Some(target)
481        );
482        assert_eq!(
483            table.resolve(original, child, CapRights::SEND),
484            Err(CapError::NotHolder)
485        );
486        assert!(table.resolve(original, parent, CapRights::ASK).is_ok());
487        Ok(())
488    }
489
490    #[test]
491    fn attenuate_cannot_escalate() -> Result<(), Box<dyn std::error::Error>> {
492        let table = CapTable::new();
493        let parent = next_flow_id();
494        let child = next_flow_id();
495        let target = next_flow_id();
496        let original = table.mint(parent, target, CapRights::SEND)?;
497        let granted = table.attenuate(
498            original,
499            parent,
500            child,
501            CapRights::SEND.union(CapRights::ADMIN),
502            None,
503        )?;
504        let got = table.resolve(granted, child, CapRights::SEND)?;
505        assert!(!got.rights().contains(CapRights::ADMIN));
506        assert!(got.rights().contains(CapRights::SEND));
507        Ok(())
508    }
509
510    #[test]
511    fn mint_or_reuse_is_stable_per_pair() -> Result<(), Box<dyn std::error::Error>> {
512        let table = CapTable::new();
513        let holder = next_flow_id();
514        let target = next_flow_id();
515        let first = table.mint_or_reuse(holder, target, CapRights::SEND)?;
516        for _ in 0..1_000 {
517            let again = table.mint_or_reuse(holder, target, CapRights::SEND)?;
518            assert_eq!(again, first);
519        }
520        assert_eq!(table.entry_count()?, 1);
521        assert_eq!(table.reply_index_len()?, 1);
522        assert!(table.resolve(first, holder, CapRights::SEND).is_ok());
523        Ok(())
524    }
525
526    #[test]
527    fn mint_or_reuse_distinct_pairs_are_independent() -> Result<(), Box<dyn std::error::Error>> {
528        let table = CapTable::new();
529        let a = next_flow_id();
530        let b = next_flow_id();
531        let ab = table.mint_or_reuse(b, a, CapRights::SEND)?;
532        let ba = table.mint_or_reuse(a, b, CapRights::SEND)?;
533        assert_ne!(ab, ba);
534        assert_eq!(table.entry_count()?, 2);
535        assert_eq!(table.reply_index_len()?, 2);
536        Ok(())
537    }
538
539    #[test]
540    fn revoke_flow_clears_reply_index_for_holder_and_target() -> Result<(), Box<dyn std::error::Error>> {
541        let table = CapTable::new();
542        let a = next_flow_id();
543        let b = next_flow_id();
544        let alive = next_flow_id();
545        let ab = table.mint_or_reuse(b, a, CapRights::SEND)?;
546        let ba = table.mint_or_reuse(a, b, CapRights::SEND)?;
547        let kept = table.mint_or_reuse(alive, b, CapRights::SEND)?;
548
549        table.revoke_flow(a)?;
550        assert!(!table.reply_index_mentions(a)?);
551        assert_eq!(
552            table.resolve(ab, b, CapRights::SEND),
553            Err(CapError::Unknown)
554        );
555        assert_eq!(
556            table.resolve(ba, a, CapRights::SEND),
557            Err(CapError::Unknown)
558        );
559        assert!(table.resolve(kept, alive, CapRights::SEND).is_ok());
560        assert!(!table.reply_index_mentions(a)?);
561        assert!(table.reply_index_mentions(b)?);
562
563        table.revoke_flow(b)?;
564        assert!(!table.reply_index_mentions(a)?);
565        assert!(!table.reply_index_mentions(b)?);
566        Ok(())
567    }
568
569    #[test]
570    fn mint_or_reuse_after_revoke_issues_a_fresh_id() -> Result<(), Box<dyn std::error::Error>> {
571        let table = CapTable::new();
572        let holder = next_flow_id();
573        let target = next_flow_id();
574        let old = table.mint_or_reuse(holder, target, CapRights::SEND)?;
575        table.revoke_flow(target)?;
576        assert!(!table.reply_index_mentions(target)?);
577        let fresh = table.mint_or_reuse(holder, target, CapRights::SEND)?;
578        assert_ne!(fresh, old);
579        assert_eq!(
580            table.resolve(old, holder, CapRights::SEND),
581            Err(CapError::Unknown)
582        );
583        assert!(table.resolve(fresh, holder, CapRights::SEND).is_ok());
584        Ok(())
585    }
586}