godspeed_sdk/ipc.rs
1// SPDX-License-Identifier: Apache-2.0
2//! IPC syscall wrappers - §8.2.
3//!
4//! Typed wrappers so service code cannot accidentally call the wrong syscall
5//! number or pass a malformed message size.
6
7use crate::capability::CapHandle;
8use crate::syscall::raw_syscall;
9
10pub const MAX_PAYLOAD: usize = 4096;
11
12/// An IPC message as seen by service code.
13pub struct Message {
14 pub payload: [u8; MAX_PAYLOAD],
15 pub payload_len: usize,
16 /// Capability handles embedded in this message (received side).
17 pub caps: [Option<CapHandle>; 4],
18 pub cap_count: usize,
19}
20
21impl Message {
22 pub fn from_bytes(bytes: &[u8]) -> Self {
23 let len = bytes.len().min(MAX_PAYLOAD);
24 let mut payload = [0u8; MAX_PAYLOAD];
25 payload[..len].copy_from_slice(&bytes[..len]);
26 Self {
27 payload,
28 payload_len: len,
29 caps: [None; 4],
30 cap_count: 0,
31 }
32 }
33
34 pub fn payload_bytes(&self) -> &[u8] {
35 &self.payload[..self.payload_len]
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum IpcError {
41 EndpointDead,
42 QueueFull,
43 MessageTooLarge,
44 /// A synchronous `call` was woken because the peer (the would-be replier) died before replying
45 /// (§8.6). The reply-side twin of `EndpointDead`: the caller waited on the peer's liveness, not a
46 /// timer, and so never hangs (Commandment VIII). Reacquire the peer by name and retry, exactly as
47 /// for a failed send.
48 ReplyDead,
49 CapError(crate::capability::CapError),
50}
51
52// ---------------------------------------------------------------------------
53// IPC syscall wrappers.
54// ---------------------------------------------------------------------------
55
56/// Block until a message is available on `endpoint`. (§8.2 - `recv`)
57///
58/// Passes a stack-allocated receive buffer to the kernel; the kernel copies the
59/// message payload there and returns the number of bytes written.
60pub fn recv(endpoint: CapHandle) -> Result<Message, IpcError> {
61 let mut payload = [0u8; MAX_PAYLOAD];
62 // SAFETY: raw_syscall(2) = Recv; buf is a valid stack slice within user space.
63 let ret = unsafe {
64 raw_syscall(
65 2,
66 endpoint.0 as u64,
67 payload.as_mut_ptr() as u64,
68 MAX_PAYLOAD as u64,
69 )
70 };
71 if ret < 0 {
72 Err(i64_to_ipc_error(ret))
73 } else {
74 Ok(Message::from_bytes(&payload[..ret as usize]))
75 }
76}
77
78/// Non-blocking `recv`: return `Ok(None)` immediately if the endpoint queue is empty,
79/// `Ok(Some(msg))` if a message was waiting. (syscall 34 - `try_recv`) Lets a busy-polling
80/// driver drain interrupt events (§12) without giving up its loop.
81pub fn try_recv(endpoint: CapHandle) -> Result<Option<Message>, IpcError> {
82 const TRY_RECV_EMPTY: i64 = -1000; // kernel sentinel for an empty queue
83 let mut payload = [0u8; MAX_PAYLOAD];
84 // SAFETY: raw_syscall(34) = TryRecv; buf is a valid stack slice within user space.
85 let ret = unsafe {
86 raw_syscall(
87 34,
88 endpoint.0 as u64,
89 payload.as_mut_ptr() as u64,
90 MAX_PAYLOAD as u64,
91 )
92 };
93 if ret == TRY_RECV_EMPTY {
94 Ok(None)
95 } else if ret < 0 {
96 Err(i64_to_ipc_error(ret))
97 } else {
98 Ok(Some(Message::from_bytes(&payload[..ret as usize])))
99 }
100}
101
102/// Receive into a buffer the CALLER owns, returning the byte count.
103///
104/// A `Message` carries a 4096-byte payload BY VALUE, so `recv_timeout` below costs EIGHT kilobytes of
105/// stack per call: one 4 KiB array to receive into, and another inside the `Message` it builds and
106/// returns. Every wrapper that forwards that `Message` pays 4 KiB again.
107///
108/// On `fs` that arithmetic is fatal rather than untidy. Its request path is several such calls deep,
109/// it runs on a 256 KiB stack, and adding one more level made it overflow at mount - a fault that took
110/// five wrong attempts to place because each one removed a single 4 KiB item without measuring the
111/// whole frame.
112///
113/// This is the shape the operation actually has, and the shape every real IPC API uses: the kernel
114/// copies into memory the caller already owns, and hands back a length. Nothing large is created and
115/// nothing large is moved. `Ok(None)` is the timeout, exactly as above.
116///
117/// Caps are NOT delivered here - the block and frame paths carry none, and a caller that needs one
118/// takes it with `take_pending_cap` regardless. A buffer API that silently dropped a capability would
119/// be a leak, so this one simply does not carry them.
120pub fn recv_timeout_into(
121 endpoint: CapHandle, buf: &mut [u8], timeout_cycles: u64,
122) -> Result<Option<usize>, IpcError> {
123 const RECV_TIMED_OUT: i64 = -1001;
124 let packed = ((buf.len() as u64) << 16) | (endpoint.0 as u64 & 0xFFFF);
125 let timeout_cycles = abi_timeout(timeout_cycles);
126 // SAFETY: raw_syscall(35) = RecvTimeout; `buf` is a valid caller-owned slice in user space and its
127 // true length is what is passed, so the kernel cannot write past it.
128 let ret = unsafe { raw_syscall(35, packed, buf.as_mut_ptr() as u64, timeout_cycles) };
129 if ret == RECV_TIMED_OUT {
130 Ok(None)
131 } else if ret < 0 {
132 Err(i64_to_ipc_error(ret))
133 } else {
134 Ok(Some((ret as usize).min(buf.len())))
135 }
136}
137
138/// Blocking `recv` with a timeout in counter ticks (the kernel waits at least one scheduler tick):
139/// `Ok(Some(msg))` on a message, `Ok(None)` on timeout, `Err` on a real error. `timeout_cycles == 0`
140/// blocks forever (like `recv`). (syscall 35 - `recv_timeout`) Lets a driver idle on its interrupt
141/// yet still wake on a timer (e.g. for keyboard auto-repeat while a key is held - §12).
142pub fn recv_timeout(endpoint: CapHandle, timeout_cycles: u64) -> Result<Option<Message>, IpcError> {
143 const RECV_TIMED_OUT: i64 = -1001; // kernel sentinel for "timed out, no message"
144 let mut payload = [0u8; MAX_PAYLOAD];
145 // arg0 packs the buffer length (high) and the cap slot (low) to fit the 3-arg ABI.
146 let packed = ((MAX_PAYLOAD as u64) << 16) | (endpoint.0 as u64 & 0xFFFF);
147 let timeout_cycles = abi_timeout(timeout_cycles);
148 // SAFETY: raw_syscall(35) = RecvTimeout; buf is a valid stack slice within user space.
149 let ret = unsafe { raw_syscall(35, packed, payload.as_mut_ptr() as u64, timeout_cycles) };
150 if ret == RECV_TIMED_OUT {
151 Ok(None)
152 } else if ret < 0 {
153 Err(i64_to_ipc_error(ret))
154 } else {
155 Ok(Some(Message::from_bytes(&payload[..ret as usize])))
156 }
157}
158
159/// Fit a timeout to what one syscall argument can actually carry.
160///
161/// A 32-bit ABI carries each argument in ONE register and `raw_syscall` truncates a `u64` arg to
162/// `u32`. Every OTHER argument (pointer, handle, length) genuinely fits in 32 bits, but a timeout in
163/// generic-timer ticks does NOT: at QEMU raspi2b's 62.5 MHz, `u32::MAX` ticks is only ~68 s (~71 min
164/// at the real Pi 2's measured 1 MHz, `kernel/src/arch/arm/timer.rs`). A
165/// longer finite timeout would truncate to a tiny value (premature wake) or - if it landed on a
166/// multiple of 2^32 - to **0**, which the kernel reads as "block forever". A bounded Commandment VIII
167/// deadline turning into an infinite hang is the worst possible way for this to fail, which is why it
168/// saturates: a long finite request becomes the longest REPRESENTABLE timeout, never a tiny one and
169/// never 0. A genuine 0 (block forever) stays 0. (userspace-audit Audit 4, A-U1.)
170///
171/// ASKED AS REGISTER WIDTH, NOT AS `target_arch = "arm"`, which is what both call sites said
172/// separately. arm32 was never the reason - a 32-bit argument register was, and arm32 merely happens
173/// to be the only 32-bit port that currently builds userspace. A future 32-bit port inherits the
174/// clamp instead of inheriting the hang, and that is not hypothetical: the failure mode here is
175/// SILENT on the port that lacks it. (`target_pointer_width` is a proxy for argument-register width;
176/// they coincide on every target this SDK builds for, and `kernel/src/task/scheduler.rs` already uses
177/// the same proxy for the same reason.)
178#[inline]
179fn abi_timeout(timeout_cycles: u64) -> u64 {
180 if cfg!(target_pointer_width = "32") && timeout_cycles != 0 {
181 timeout_cycles.min(u32::MAX as u64).max(1)
182 } else {
183 timeout_cycles
184 }
185}
186
187/// Send a message to `endpoint`; block if the queue is full. (§8.2 - `send`)
188pub fn send(endpoint: CapHandle, msg: &Message) -> Result<(), IpcError> {
189 let payload = msg.payload_bytes();
190 // SAFETY: raw_syscall(1) = Send; payload is a valid slice within user space.
191 let ret = unsafe {
192 raw_syscall(
193 1,
194 endpoint.0 as u64,
195 payload.as_ptr() as u64,
196 payload.len() as u64,
197 )
198 };
199 if ret == 0 { Ok(()) } else { Err(i64_to_ipc_error(ret)) }
200}
201
202/// Send without blocking; return `QueueFull` immediately. (§8.2 - `try_send`)
203pub fn try_send(endpoint: CapHandle, msg: &Message) -> Result<(), IpcError> {
204 let payload = msg.payload_bytes();
205 // SAFETY: raw_syscall(3) = TrySend; payload is a valid slice within user space.
206 let ret = unsafe {
207 raw_syscall(
208 3,
209 endpoint.0 as u64,
210 payload.as_ptr() as u64,
211 payload.len() as u64,
212 )
213 };
214 if ret == 0 { Ok(()) } else { Err(i64_to_ipc_error(ret)) }
215}
216
217/// Synchronous CALL (syscall 41): send `request` to `target` carrying `reply_grant` as a one-shot
218/// reply cap, then block on `recv` (the caller's own endpoint, where the reply lands) until the peer
219/// replies OR the peer's endpoint dies. Returns the reply, `Err(ReplyDead)` if the peer died before
220/// replying, `Err(EndpointDead)` if the peer was already dead when the request was sent, or another
221/// `IpcError` on a failed send.
222///
223/// This **waits on truth, not on time** (Commandment VIII): a peer that dies mid-request wakes the
224/// caller with `ReplyDead` (the reply-side twin of `EndpointDead`, §8.6) instead of hanging it - no
225/// timer, no fixed yield count. The buffer is in/out: the request is read from it, and the kernel
226/// writes the reply back into the same buffer.
227pub fn call(
228 target: CapHandle,
229 reply_grant: CapHandle,
230 recv: CapHandle,
231 request: &Message,
232) -> Result<Message, IpcError> {
233 let mut buf = [0u8; MAX_PAYLOAD];
234 let req = request.payload_bytes();
235 buf[..req.len()].copy_from_slice(req);
236 // Pack three 16-bit cap slots + the length into THREE 32-bit-safe args (not two): arg0 = target |
237 // reply, arg2 = recv | len. The old layout put `recv` in bits 32-47 of arg0, which the ARM 32-bit
238 // syscall ABI truncates away (each arg is one register) - recv_slot became 0 and the Call routed to
239 // the wrong endpoint (userspace-audit A-U1 class). A request length never exceeds one message (4 KiB
240 // < 0xFFFF), so `recv` rides the high half of the length arg. `handle_call` mirrors this.
241 let packed = ((reply_grant.0 as u64) << 16) | (target.0 as u64);
242 let recv_len = ((recv.0 as u64) << 16) | (req.len() as u64 & 0xFFFF);
243 // SAFETY: raw_syscall(41) = Call; `buf` is a valid in/out stack buffer in user space (request in,
244 // reply written back), `req.len() <= MAX_PAYLOAD`. The kernel validates every cap slot and the
245 // buffer pointer before use.
246 let ret = unsafe { raw_syscall(41, packed, buf.as_mut_ptr() as u64, recv_len) };
247 if ret < 0 {
248 Err(i64_to_ipc_error(ret))
249 } else {
250 Ok(Message::from_bytes(&buf[..ret as usize]))
251 }
252}
253
254/// `call`, bounded, receiving into a caller-owned buffer.
255///
256/// The reason this exists as a SYSCALL rather than the hand-rolled send + `recv_timeout_into` it
257/// replaces: `recv_timeout_into` takes whatever is next on the endpoint. A service that serves clients
258/// on the same endpoint it awaits replies on would receive an unrelated CLIENT REQUEST, fail to match
259/// it, and drop it - losing the request and leaving the real reply to arrive later as an orphan that
260/// desynced every following exchange. `Call` never had that flaw (it dequeues the reply specifically),
261/// it just could not be bounded. Now it can.
262///
263/// Three cap slots pack into `a0` at 8 bits each - a task holds at most 64 caps, so six bits suffice -
264/// because the ARM 32-bit ABI truncates each argument to one register and the plain `call` layout had
265/// no room left. Length and deadline share `a2`.
266///
267/// `Ok(None)` = the deadline passed with no reply.
268pub fn call_deadline_into(
269 target: CapHandle,
270 reply_grant: CapHandle,
271 recv: CapHandle,
272 request: &[u8],
273 buf: &mut [u8],
274 max_secs: u64,
275) -> Result<Option<usize>, IpcError> {
276 const RECV_TIMED_OUT: i64 = -1001;
277 // `buf` carries the request OUT and the reply BACK, exactly as plain `call` does with its own
278 // 4 KiB frame. So it must hold the LARGER of the two, and a caller that sized it for replies
279 // alone is a caller whose requests are about to be cut short.
280 //
281 // This said `.min(buf.len())` in its first version, which truncated in silence: `fs` sized its
282 // buffer at 2 + BLOCK = 514 for replies while a block WRITE request is 1 + 9 + BLOCK = 522, so
283 // every write went out eight bytes short and came back a failure with no reason attached. Reads
284 // are ten bytes and fit, which is why reads worked and only writes broke. A silent truncation is
285 // the one thing a message path must never do (§26.7).
286 if request.len() > MAX_PAYLOAD { return Err(IpcError::MessageTooLarge); }
287 if request.len() > buf.len() { return Err(IpcError::MessageTooLarge); }
288 let n = request.len();
289 buf[..n].copy_from_slice(&request[..n]);
290 // TELL THE KERNEL HOW BIG THE REPLY BUFFER IS. It writes the reply back into `buf`, and until
291 // now it was never told the size: `handle_call_deadline` validated MAX_MESSAGE_SIZE and its own
292 // comment said "the SDK always passes a MAX_MESSAGE_SIZE buffer". That is true of `call` (41),
293 // which allocates its own 4 KiB internally - and false here, where the buffer is the CALLER'S
294 // slice, of whatever size it chose. `console_dims` passes EIGHT BYTES. Validating 4 KiB only
295 // proves those bytes are mapped, which they are - it is the middle of a stack - so nothing stood
296 // between a larger-than-expected reply and a smashed frame.
297 //
298 // Encoded as a 4-bit power-of-two CLASS rather than a length, because the request length and the
299 // deadline already occupy the length argument, and arg0 has exactly one nibble spare below the
300 // 32-bit line that arm32's one-register-per-arg ABI truncates at. Rounded DOWN, so a 514-byte
301 // buffer declares 512 and the kernel refuses a 514-byte reply rather than writing two bytes past
302 // it. Being wrong in the conservative direction turns a silent smash into a loud refusal (§26.7).
303 // Callers should size this buffer as a POWER OF TWO. A 522-byte buffer declares 512 and its own
304 // 522-byte replies are then refused - correct, conservative, and confusing. `fs` carries the note
305 // and a compile-time assert on its side; this is the reason.
306 let cap_class = (usize::BITS - 1 - buf.len().max(1).leading_zeros()).min(12) as u64;
307 let slots = (target.0 as u64 & 0xFF)
308 | ((reply_grant.0 as u64 & 0xFF) << 8)
309 | ((recv.0 as u64 & 0xFF) << 16)
310 | (cap_class << 24);
311 let len_secs = (n as u64 & 0xFFFF) | ((max_secs.min(0xFFFF)) << 16);
312 // SAFETY: raw_syscall(50) = CallDeadline; `buf` is a caller-owned slice in user space and the
313 // kernel writes at most one message into it, which is why the request is staged there too.
314 let ret = unsafe { crate::syscall::raw_syscall(50, slots, buf.as_mut_ptr() as u64, len_secs) };
315 if ret == RECV_TIMED_OUT {
316 Ok(None)
317 } else if ret < 0 {
318 Err(i64_to_ipc_error(ret))
319 } else {
320 Ok(Some(ret as usize))
321 }
322}
323
324// ---------------------------------------------------------------------------
325// Error conversion.
326// ---------------------------------------------------------------------------
327
328pub(crate) fn i64_to_ipc_error(code: i64) -> IpcError {
329 match code {
330 -2 => IpcError::CapError(crate::capability::CapError::CapNotHeld),
331 -3 => IpcError::CapError(crate::capability::CapError::CapInsufficientRights),
332 -4 => IpcError::CapError(crate::capability::CapError::CapNotGrantable),
333 -7 => IpcError::EndpointDead,
334 -8 => IpcError::QueueFull,
335 -9 => IpcError::QueueFull, // QueueEmpty on caller side == retry
336 -10 => IpcError::MessageTooLarge,
337 -12 => IpcError::ReplyDead,
338 _ => IpcError::EndpointDead,
339 }
340}