Skip to main content

godspeed/
resource.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Invoking a delegated resource capability, once, for everything that has one.
3//!
4//! `fs` mints file capabilities and `net-stack` mints socket, listener and connection capabilities.
5//! They are the same mechanism (CLAUDE.md 7.10) with different payloads, so the parts that are easy
6//! to get subtly wrong live here rather than once per module:
7//!
8//! - **the reply cap is one-shot and per-invocation**, derived from our own endpoint and consumed by
9//!   the kernel on delivery. Reclaim it ONLY where the send never delivered - `8.5` removes an
10//!   embedded cap from the sender's table, so removing it again removes whatever the kernel has since
11//!   put in that slot. That is how a file capability was once deleted out from under its holder.
12//! - **a message that is not our reply is HELD, never dropped.** A caller waits on its own ordinary
13//!   endpoint - the same one its clients send to - because a task has exactly one. Draining would
14//!   destroy real client requests; that is what `services/shell` does, and it is safe only because
15//!   the shell serves nobody.
16//! - **the kernel's refusal is READ, not inferred.** An earlier cut guessed from the rights mask and
17//!   reported a revoked capability as "could not be reached", which named the wrong fault and sent an
18//!   operator to check a service that was running perfectly.
19
20use godspeed_sdk::capability::{CapError, CapHandle};
21use godspeed_sdk::ipc::{IpcError, Message};
22use godspeed_sdk::service_context::{ReqOutcome, ServiceContext};
23
24use crate::cap::Cap;
25use crate::error::Error;
26
27/// How many messages that are NOT our reply one invocation will hold before it stops taking them.
28///
29/// Two, and small on purpose: a held message is a full 4 KiB `Message`, so the footprint stays
30/// readable from this line (26.6.1). Two covers a client or two arriving during one round trip;
31/// beyond it the operation stops rather than dropping anything.
32pub const HELD_MAX: usize = 2;
33
34/// The largest request body one invocation carries, plus its tag.
35///
36/// Sized for the biggest user, which is a filesystem chunk. A datagram is far smaller and pays the
37/// same fixed cost - the alternative is a second constant and a second buffer shape, which is a
38/// worse trade than a few unused kilobytes of stack.
39const REQ_MAX: usize = 16 + crate::fs::IO_CHUNK;
40
41/// Messages that arrived during an invocation and were not its reply.
42pub(crate) struct Held {
43    msgs: [Option<Message>; HELD_MAX],
44    n: usize,
45}
46
47impl Held {
48    pub(crate) const fn new() -> Held {
49        Held { msgs: [None, None], n: 0 }
50    }
51
52    /// Take the oldest held message, or `None`.
53    ///
54    /// FIFO: handing them back out of order would have a client's two requests answered backwards.
55    pub(crate) fn take(&mut self) -> Option<Message> {
56        if self.n == 0 {
57            return None;
58        }
59        let first = self.msgs[0].take();
60        for i in 1..HELD_MAX {
61            self.msgs[i - 1] = self.msgs[i].take();
62        }
63        self.n -= 1;
64        first
65    }
66}
67
68/// Send `[tag, body..]` through a resource capability and wait for the reply carrying `tag`.
69///
70/// Returns the reply WHOLE, tag still at byte 0 - the caller reads its own body from index 1.
71/// Stripping would mean rebuilding a 4 KiB `Message` on every call, and the one time this library
72/// pretended it had stripped, two call sites read the status byte as data.
73pub(crate) fn invoke(
74    ctx: &ServiceContext,
75    cap: CapHandle,
76    right: u8,
77    tag: u8,
78    patience: Option<u8>,
79    body: &[u8],
80    secs: i64,
81    held: &mut Held,
82) -> Result<Message, Error> {
83    // The header is built HERE rather than by each caller, so nobody has to keep a second buffer to
84    // prepend one byte to - this function already owns a `REQ_MAX` one.
85    //
86    // `patience` is how many seconds the caller is about to wait. A service that has to put this
87    // request aside while it talks to something else can then hold it for as long as it is actually
88    // worth answering, instead of against a constant that cannot know. `None` is for an owner that
89    // does no such holding (`fs`), and costs a byte nothing would read.
90    let hdr = 1 + patience.is_some() as usize;
91    if body.len() + hdr > REQ_MAX {
92        return Err(Error::InvalidInput);
93    }
94    let mut req = [0u8; REQ_MAX];
95    req[0] = tag;
96    if let Some(p) = patience {
97        req[1] = p;
98    }
99    let body_end = hdr + body.len();
100    req[hdr..body_end].copy_from_slice(body);
101
102    let self_grant = ctx.self_grant_handle().ok_or(Error::Unreachable)?;
103    let reply_cap = ctx.derive_cap(self_grant).ok_or(Error::Busy)?;
104
105    if let Err(e) = ctx.resource_invoke(cap, right, reply_cap, &Message::from_bytes(&req[..body_end])) {
106        // Refused before routing, so the reply cap was NOT consumed: reclaim the slot (8.5).
107        ctx.remove_cap(reply_cap);
108        return Err(match e {
109            IpcError::CapError(CapError::CapInsufficientRights)
110            | IpcError::CapError(CapError::CapNotGrantable)
111            | IpcError::CapError(CapError::CapWrongScope) => Error::PermissionDenied,
112            // Revoked, or the issuer died and was replaced. One meaning to a holder: this capability
113            // is finished, and the answer is to re-open rather than to retry.
114            IpcError::CapError(CapError::CapRevoked)
115            | IpcError::CapError(CapError::CapNotHeld)
116            | IpcError::CapError(CapError::EndpointDead)
117            | IpcError::EndpointDead
118            | IpcError::ReplyDead => Error::Revoked,
119            _ => Error::Unreachable,
120        });
121    }
122
123    loop {
124        match ctx.recv_abortable_deadline(secs) {
125            ReqOutcome::Reply(m) => {
126                if m.payload_bytes().first() == Some(&tag) {
127                    return Ok(m);
128                }
129                // NOT OURS. Holding it is the entire reason this is safe to hand to a service that
130                // serves other clients.
131                if held.n == HELD_MAX {
132                    // No room. Stop taking, so the rest stays queued for the caller. Whether the
133                    // operation completed is genuinely unknown - say exactly that.
134                    return Err(Error::OutcomeUnknown);
135                }
136                held.msgs[held.n] = Some(m);
137                held.n += 1;
138            }
139            // The caller's own abort. Not a fault.
140            ReqOutcome::Aborted => return Err(Error::Cancelled),
141            // The deadline passed. The request LEFT, so it may have been performed.
142            ReqOutcome::Timeout => return Err(Error::OutcomeUnknown),
143        }
144    }
145}
146
147// ── The owner's side: minting and revoking a resource of your own ──────────────────────────────
148//
149// Everything above serves a HOLDER using someone else's capability. These three are for a service
150// that ISSUES them - what `fs` does for a file and `net-stack` for a socket.
151
152/// Mint a fresh resource capability that THIS service owns (CLAUDE.md 7.10).
153///
154/// Returns the opaque `ResourceId` the kernel will badge invocations with, and a capability carrying
155/// `rights` that you can hand to a client. The kernel tracks the id and the owning endpoint and
156/// nothing else - **what the resource MEANS is yours alone**, which is what keeps filesystem logic
157/// out of the kernel (CLAUDE.md 4.4).
158///
159/// Gated: minting requires the `RESOURCE_MINT` authority, granted only to services that legitimately
160/// issue resources. Delegated minting is explicit authority, never ambient (CLAUDE.md 3.1).
161pub fn mint(ctx: &ServiceContext, rights: u8) -> Result<(u64, Cap), Error> {
162    match ctx.resource_mint(rights) {
163        Some((id, h)) => Ok((id, Cap::from_handle(h))),
164        None => Err(Error::PermissionDenied),
165    }
166}
167
168/// Revoke a resource this service owns, invalidating EVERY capability to it.
169///
170/// One generation bump, and every outstanding copy goes stale at once - there is no list of holders
171/// to walk and none can be missed (CLAUDE.md 7.5). `fs` does this on delete and on close.
172///
173/// Returns `false` if this service does not own that id. Nothing is revoked in that case, which is
174/// worth reporting rather than treating as done.
175pub fn revoke(ctx: &ServiceContext, resource_id: u64) -> bool {
176    ctx.resource_revoke(resource_id)
177}
178
179/// The resource id and right the kernel badged the message just received with.
180///
181/// `None` means the message was an ordinary name-addressed request rather than a capability
182/// invocation. The badge is set BY THE KERNEL after it validated the capability, so it cannot be
183/// forged by a client sending over its ordinary send capability - which is what makes it safe to act
184/// on. Enforce `op <= right` yourself: the kernel checked the capability, not what you do with it.
185pub fn last_badge(ctx: &ServiceContext) -> Option<(u64, u8)> {
186    ctx.last_recv_badge()
187}