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}