Skip to main content

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}