godspeed_sdk/service_context.rs
1// SPDX-License-Identifier: Apache-2.0
2//! ServiceContext - entry-point type handed to every service's `service_main`.
3//!
4//! Provides safe, named access to the capabilities the service declared in its
5//! contract. Capability names match the contract field names exactly.
6//! Requesting a cap not in the contract returns `Err(CapNotHeld)`.
7
8use crate::capability::{CapError, CapHandle};
9use crate::ipc::{IpcError, Message};
10use crate::syscall::raw_syscall;
11
12/// Why a `reacquire_cap_detail` failed. The two real cases want OPPOSITE responses, which is the
13/// entire reason this type exists rather than a `bool`.
14///
15/// `NameNotRegistered` is **transient and expected**: peers are acquired before they are spawned at
16/// boot, and again while one is mid-restart. Retry; it heals. `CapTableFull` is **permanent**: this
17/// service cannot hold another capability, so it can never reacquire anything again and will not
18/// recover until it is restarted - a failed recovery, which §26.7 says must stay exactly as visible as
19/// the original failure.
20///
21/// Collapsed into one value (as `reacquire_cap` still does, for callers that only retry) a service can
22/// only print both and commit to neither. That ambiguity cost a full debugging pass on the Pi 2: a
23/// `block-driver` that was alive the whole time produced the same line as a reader out of cap slots.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum AcquireFailure {
26 /// The name does not resolve right now - no live instance is registered under it. Transient.
27 NameNotRegistered,
28 /// The name resolved, but this service's own capability table is full. Terminal until restart.
29 CapTableFull,
30 /// The request itself was refused (name empty, too long, or not a declared peer).
31 Refused,
32}
33
34impl AcquireFailure {
35 /// Decode the kernel's negative return. Codes are fixed at the syscall boundary
36 /// (`kernel/src/syscall/dispatch.rs`); an UNRECOGNISED negative maps to `Refused` rather than
37 /// being guessed at, so a future code cannot silently masquerade as one of the two named cases.
38 fn from_code(ret: i64) -> Self {
39 match ret {
40 -20 => AcquireFailure::NameNotRegistered,
41 -21 => AcquireFailure::CapTableFull,
42 _ => AcquireFailure::Refused,
43 }
44 }
45
46 /// One clause naming the cause, for a caller assembling a log line.
47 pub fn reason(self) -> &'static str {
48 match self {
49 AcquireFailure::NameNotRegistered =>
50 "the name does not resolve - no live instance is registered under it",
51 AcquireFailure::CapTableFull =>
52 "this service's capability table is FULL - it cannot hold another cap and will not recover until restarted",
53 AcquireFailure::Refused =>
54 "the kernel refused the request (name empty, too long, or not a declared peer)",
55 }
56 }
57}
58
59/// Outcome of [`ServiceContext::request_with_reply_deadline_outcome_into`] - the buffer-writing twin
60/// of [`DeadlineOutcome`].
61///
62/// It exists because the `_into` form collapsed all four outcomes into `Option<usize>`, and the two
63/// that matter are OPPOSITES. `SendFailed` means the request never left, so nothing can arrive late
64/// and retrying after a reacquire is safe. `Timeout` means it DID leave, so a late reply is still
65/// coming and re-sending desyncs the protocol - every later reply is matched to the wrong request.
66/// A caller handed `None` cannot tell those apart, so it must either never retry (and stay broken
67/// after a peer restarts) or always retry (and desync). `fs` had the first of those.
68#[derive(Debug)]
69pub enum DeadlineOutcomeInto {
70 /// A reply arrived; the payload is in the caller's buffer, this many bytes of it.
71 Reply(usize),
72 /// The send never left: the peer's cap is stale or its name does not currently resolve. Nothing
73 /// is in flight. Reacquire by name and retry ONCE - this is the only safe retry.
74 SendFailed,
75 /// The request WAS delivered, and the peer died holding it (`ReplyDead`, 8.6), so the work may
76 /// have been done. Re-send only an operation that is safe to repeat (a block read, a rewrite of
77 /// the same sector). It was folded into `SendFailed` until 2026-10-10 (`backlog/fixed/80` S3); `gs::call`
78 /// has kept the two apart all along, as `Error::PeerDied` against `Error::Unreachable`.
79 PeerDied,
80 /// The peer's queue was full. The request did not leave either, but the peer is alive and busy.
81 QueueFull,
82 /// The request was delivered and no reply came in time. Do NOT re-send.
83 Timeout,
84}
85
86/// Outcome of an abortable request/reply ([`ServiceContext::request_with_reply_abortable`]): the reply
87/// arrived, the user pressed `q`/`Q`/ESC to abort the wait, or the deadline expired with no reply.
88/// `Timeout` also covers a request that never left (no such peer, no reply cap, a failed send).
89pub enum ReqOutcome {
90 Reply(Message),
91 Aborted,
92 Timeout,
93}
94
95/// Outcome of [`ServiceContext::request_with_reply_deadline_outcome`], which distinguishes the two
96/// ways a deadline request fails - so a caller can self-heal a *restarted* peer without penalising a
97/// *silent* one. `SendFailed`: the send never left (the peer's cap is stale or its name is currently
98/// unresolvable - it was killed and respawned, generation bumped), so reacquiring it by name and
99/// retrying will reach the fresh instance. `Timeout`: the peer received the request but did not reply
100/// within the deadline (a genuinely silent/absent host) - retrying only doubles the wait.
101/// `request_with_reply_deadline` collapses both to `None`; use this when the difference matters.
102pub enum DeadlineOutcome {
103 Reply(Message),
104 SendFailed,
105 /// The peer is ALIVE and its queue is FULL. A different failure entirely, and it was folded into
106 /// `SendFailed` until x86 showed the cost: `net-stack` answered a full queue by reacquiring a
107 /// capability that was never stale, retried once and gave up - "5 of 6 REQUESTs never left the
108 /// host", DHCP backing off for 34 seconds on a link that was up the whole time.
109 ///
110 /// Congestion is transient by definition. The answer is to pace and retry, not to go looking for
111 /// a peer that never went anywhere.
112 QueueFull,
113 Timeout,
114}
115
116/// Where a wall-clock reading came from. Reported alongside the time so a displayed timestamp says what
117/// it is standing on: a local hardware clock, a network correction, or nothing (§26.4 - a fallback chain
118/// is mechanism while its choice is visible, and magic when it is not).
119#[derive(Clone, Copy, PartialEq, Eq)]
120pub enum ClockSource {
121 /// No clock: no hardware RTC and nothing has set the time. `date` says so rather than inventing one.
122 Unset,
123 /// A local hardware real-time clock reading a plausible date.
124 Rtc,
125 /// Set from the network (SNTP) this boot.
126 Ntp,
127 /// Started from the persisted floor (`/system/services/time/floor`) - no RTC on this board and no network sync yet.
128 ///
129 /// A real reading and a distinct one: it is a LOWER BOUND carried over from the last boot, advancing
130 /// correctly since, but it cannot know how long the machine was powered off. Reporting it as `Rtc`
131 /// would claim hardware this board does not have, and as `Unset` would deny a time it is displaying.
132 Floor,
133}
134
135/// The machine's one pending power action, as the kernel holds it (InspectKernel query 28, `docs/shutdown.md`).
136/// See [`ServiceContext::power_pending`].
137#[derive(Clone, Copy, PartialEq, Eq)]
138pub struct PowerPending {
139 /// Whole seconds until it falls due, rounded up.
140 pub secs: u64,
141 /// A reboot; otherwise a power-off.
142 pub reboot: bool,
143}
144
145/// Wall-clock date/time read from the hardware RTC, fully decoded (binary,
146/// 24-hour). See [`ServiceContext::datetime`].
147#[derive(Clone, Copy, PartialEq, Eq)]
148pub struct Datetime {
149 pub year: u16,
150 pub month: u8,
151 pub day: u8,
152 pub hour: u8,
153 pub minute: u8,
154 pub second: u8,
155}
156
157impl Datetime {
158 /// Decode Unix epoch seconds into calendar fields (Howard Hinnant's `civil_from_days`) - the inverse
159 /// of [`Datetime::epoch_secs`]. Used to render a stored epoch, such as the clock floor, as a date.
160 pub fn from_epoch_secs(epoch: i64) -> Datetime {
161 let days = epoch.div_euclid(86_400);
162 let rem = epoch.rem_euclid(86_400);
163 let z = days + 719_468;
164 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
165 let doe = z - era * 146_097;
166 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
167 let y = yoe + era * 400;
168 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
169 let mp = (5 * doy + 2) / 153;
170 let d = doy - (153 * mp + 2) / 5 + 1;
171 let m = if mp < 10 { mp + 3 } else { mp - 9 };
172 Datetime {
173 year: (y + (m <= 2) as i64) as u16,
174 month: m as u8,
175 day: d as u8,
176 hour: (rem / 3_600) as u8,
177 minute: ((rem % 3_600) / 60) as u8,
178 second: (rem % 60) as u8,
179 }
180 }
181
182 /// Days since the epoch (1970-01-01), proleptic Gregorian and leap-year aware
183 /// (Howard Hinnant's `days_from_civil`). The basis for both `weekday` and
184 /// `epoch_secs`.
185 fn days_since_epoch(&self) -> i64 {
186 let mut y = self.year as i64;
187 let m = self.month as i64;
188 let d = self.day as i64;
189 y -= (m <= 2) as i64;
190 let era = (if y >= 0 { y } else { y - 399 }) / 400;
191 let yoe = y - era * 400;
192 let doy = (153 * (m + if m > 2 { -3 } else { 9 }) + 2) / 5 + d - 1;
193 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
194 era * 146097 + doe - 719468
195 }
196
197 /// Day of week, 0 = Sunday .. 6 = Saturday (computed, not the RTC's
198 /// often-unreliable weekday register).
199 pub fn weekday(&self) -> u8 {
200 (self.days_since_epoch() + 4).rem_euclid(7) as u8
201 }
202
203 /// Seconds since the epoch (1970-01-01). Assumes the RTC reads UTC; if the
204 /// hardware clock is set to local time the value is offset by the timezone
205 /// (v1 has no timezone database).
206 pub fn epoch_secs(&self) -> i64 {
207 self.days_since_epoch() * 86_400
208 + self.hour as i64 * 3_600
209 + self.minute as i64 * 60
210 + self.second as i64
211 }
212}
213
214#[cfg(test)]
215mod datetime_tests {
216 use super::Datetime;
217
218 fn dt(year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> Datetime {
219 Datetime { year, month, day, hour, minute, second }
220 }
221
222 fn is_leap(y: i64) -> bool { (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 }
223 fn days_in_month(y: i64, m: i64) -> i64 {
224 match m {
225 1 => 31, 2 => if is_leap(y) { 29 } else { 28 }, 3 => 31, 4 => 30, 5 => 31, 6 => 30,
226 7 => 31, 8 => 31, 9 => 30, 10 => 31, 11 => 30, 12 => 31, _ => 0,
227 }
228 }
229
230 /// A deliberately naive, obviously-correct reference: count days from 1970 by iterating years +
231 /// months. It cannot share a bug with Hinnant's closed form - which is what makes the cross-check valid.
232 fn reference_epoch(y: i64, mo: i64, d: i64, h: i64, mi: i64, s: i64) -> i64 {
233 let mut days: i64 = 0;
234 for yy in 1970..y { days += if is_leap(yy) { 366 } else { 365 }; }
235 for mm in 1..mo { days += days_in_month(y, mm); }
236 days += d - 1;
237 days * 86_400 + h * 3_600 + mi * 60 + s
238 }
239
240 #[test]
241 fn reference_matches_known_unix_anchors() {
242 // Validate the REFERENCE itself against well-known Unix epochs first, so the cross-check is trustworthy.
243 assert_eq!(reference_epoch(1970, 1, 1, 0, 0, 0), 0);
244 assert_eq!(reference_epoch(2000, 1, 1, 0, 0, 0), 946_684_800);
245 assert_eq!(reference_epoch(2038, 1, 19, 3, 14, 8), 2_147_483_648); // Y2038 (2^31)
246 assert_eq!(reference_epoch(2000, 2, 29, 0, 0, 0), 951_782_400); // leap day
247 }
248
249 #[test]
250 fn epoch_secs_matches_known_unix_anchors() {
251 assert_eq!(dt(1970, 1, 1, 0, 0, 0).epoch_secs(), 0);
252 assert_eq!(dt(2000, 1, 1, 0, 0, 0).epoch_secs(), 946_684_800);
253 assert_eq!(dt(2038, 1, 19, 3, 14, 8).epoch_secs(), 2_147_483_648);
254 }
255
256 #[test]
257 fn epoch_secs_matches_reference_over_a_multi_century_sweep() {
258 // Cross-check Hinnant (the SDK's epoch_secs - the twin every SERVICE uses) vs the naive reference for
259 // every month of 1971..=2100: div-4 leaps, the 2100 century non-leap, the 2000 leap-400. This is the
260 // drift guard - if a future edit to the SDK's days_since_epoch diverges from the kernel's (pinned in
261 // kernel/src/clock.rs), this catches it.
262 for y in 1971..=2100i64 {
263 for mo in 1..=12i64 {
264 let last = days_in_month(y, mo);
265 for &d in &[1i64, 15, 28, last] {
266 for &(h, mi, s) in &[(0i64, 0i64, 0i64), (23, 59, 59), (12, 30, 15)] {
267 assert_eq!(
268 dt(y as u16, mo as u8, d as u8, h as u8, mi as u8, s as u8).epoch_secs(),
269 reference_epoch(y, mo, d, h, mi, s),
270 "SDK epoch_secs mismatch at {}-{:02}-{:02} {:02}:{:02}:{:02}", y, mo, d, h, mi, s);
271 }
272 }
273 }
274 }
275 }
276
277 #[test]
278 fn weekday_matches_known_anchors() {
279 // weekday() shares days_since_epoch with epoch_secs. 0=Sun..6=Sat. Known: 1970-01-01 Thursday (4),
280 // 2000-01-01 Saturday (6), 2026-06-06 Saturday (6) - the T630 `date` HW example.
281 assert_eq!(dt(1970, 1, 1, 0, 0, 0).weekday(), 4);
282 assert_eq!(dt(2000, 1, 1, 0, 0, 0).weekday(), 6);
283 assert_eq!(dt(2026, 6, 6, 0, 0, 0).weekday(), 6);
284 }
285
286 #[test]
287 fn from_epoch_secs_round_trips_over_the_sweep() {
288 // The INVERSE (civil_from_days) must undo epoch_secs exactly, over the same multi-century sweep -
289 // the drift guard its forward twin already has. Without this, the only thing pinning the decode
290 // used to render a stored epoch (the clock floor) would be that it looked right once.
291 for y in 1971..=2100i64 {
292 for mo in 1..=12i64 {
293 let last = days_in_month(y, mo);
294 for &d in &[1i64, 15, 28, last] {
295 for &(h, mi, s) in &[(0i64, 0i64, 0i64), (23, 59, 59), (12, 30, 15)] {
296 let a = dt(y as u16, mo as u8, d as u8, h as u8, mi as u8, s as u8);
297 assert!(Datetime::from_epoch_secs(a.epoch_secs()) == a,
298 "from_epoch_secs round-trip mismatch at {}-{:02}-{:02} {:02}:{:02}:{:02}",
299 y, mo, d, h, mi, s);
300 }
301 }
302 }
303 }
304 assert!(Datetime::from_epoch_secs(0) == dt(1970, 1, 1, 0, 0, 0));
305 }
306}
307
308// ---------------------------------------------------------------------------
309// ServiceContextData page layout.
310// MUST match `ServiceContextData` in `kernel/src/task/mod.rs`.
311// ---------------------------------------------------------------------------
312
313const SERVICE_CTX_ADDR: u64 = 0x3ff000;
314/// What a spawner tells the kernel about a task it wants started from an image IT supplies
315/// (`SpawnImage`, syscall 52). Layout must match the kernel's `SpawnRequest` exactly.
316///
317/// Shaped for the end state, not for what the kernel honours today: the hardware and privilege
318/// fields are here because step D needs them, and a request that sets one is REFUSED loudly rather
319/// than silently ignored (`docs/service-ownership.md`).
320#[repr(C)]
321#[derive(Clone, Copy)]
322pub struct SpawnRequest {
323 pub version: u32,
324 pub flags: u32,
325 pub image_ptr: u64,
326 pub image_len: u64,
327 pub name_ptr: u64,
328 pub name_len: u32,
329 pub core: u32,
330 pub memory_limit: u64,
331 pub privileges: u32,
332 pub hw_flags: u32,
333 pub mmio_base: u64,
334 pub mmio_len: u64,
335 pub dma_pages: u32,
336 pub bdf: u32,
337 pub irq_count: u32,
338 pub irqs: [u8; 4],
339 pub peers_ptr: u64,
340 pub peers_len: u32,
341 pub _pad: u32,
342 /// Caller-provided caps to install into the child: `[label_len][label][slot_lo][slot_hi]`
343 /// repeated. The same encoding `SpawnWithCaps` uses - one wire format, not two.
344 pub installs_ptr: u64,
345 pub installs_count: u32,
346 pub _pad2: u32,
347 /// Mode selector. Named for probes, its first user, but general: `observe` reads it to choose
348 /// one-shot / live / foreground, and takes the LIVE LOOP at 0.
349 pub probe_mode: u32,
350 pub _pad3: u32,
351}
352
353/// 2 since `installs` was added, 3 since `probe_mode` was. A spawner built against a different kernel
354/// is refused loudly.
355pub const SPAWN_REQUEST_VERSION: u32 = 3;
356pub const SPAWN_FLAG_REQ_RECV: u32 = 1 << 0;
357pub const SPAWN_FLAG_REQ_CONSOLE: u32 = 1 << 1;
358/// `core` is a STRICT placement, not a preference.
359///
360/// The two are different rules (9.2) and the kernel has always had both: an explicitly-requested core
361/// (a restart's `--core N`) is REJECTED with `PlacementInvalid` when that core is not ready, because
362/// silently placing the service elsewhere would defeat the point of asking. A catalogue row's
363/// PREFERRED core falls back to round-robin instead, so a machine with fewer cores still boots.
364///
365/// Without this bit a moved service's table preference read as an override, and every service naming
366/// a core the machine does not have failed to spawn rather than degrading - `events` and `xhci` (both
367/// core 2) simply vanished under `-smp 2`, which 11.3 requires to keep working.
368pub const SPAWN_FLAG_CORE_STRICT: u32 = 1 << 2;
369/// Mint the child's name-wired peer caps WITH `GRANT`, so it can re-delegate them.
370///
371/// AUTHORITY, not a setting: handing out a re-delegatable capability is itself a grant (§7.4), which
372/// is why the kernel used to keep this keyed by name for the one probe that needs it (§22 Test 5A).
373/// As a request bit it is checked the same way every privilege is - the spawner may ask for it only
374/// because it could transfer such a cap itself.
375pub const SPAWN_FLAG_PEERS_GRANT: u32 = 1 << 3;
376/// Report this task's death to the supervisor's death-notification endpoint and count it as a restart. The
377/// supervisor sets it for every service it manages; the kernel keeps no list of which services those are
378/// (`docs/audio.md`, "No service names in the kernel").
379pub const SPAWN_FLAG_WATCHED: u32 = 1 << 4;
380/// A live handover: start this beside the one task already running under its name, which keeps running until it
381/// ends itself; the new one takes the name. The supervisor's alone, since only `SpawnImage` honours it (CLAUDE.md 2.5).
382pub const SPAWN_FLAG_HANDOVER: u32 = 1 << 5;
383
384/// Bits for `SpawnRequest::privileges`. A spawner may only request what it HOLDS ITSELF - the kernel
385/// checks, and refuses otherwise - so this passes authority on, it never mints it (3.1, 7.3).
386pub mod privbits {
387 pub const SPAWN: u32 = 1 << 0;
388 pub const CONSOLE_PUSH: u32 = 1 << 1;
389 pub const INTROSPECT: u32 = 1 << 2;
390 pub const SERVICE_CONTROL: u32 = 1 << 3;
391 pub const FIRE_IRQ: u32 = 1 << 4;
392 pub const REBOOT: u32 = 1 << 5;
393 pub const ACQUIRE_ANY: u32 = 1 << 6;
394 pub const RESOURCE_MINT: u32 = 1 << 7;
395 /// ARM-only in practice; the bit is arch-neutral.
396 pub const GPIO: u32 = 1 << 8;
397 /// SET_CLOCK with READ (raise the clock FLOOR), not WRITE (step the clock).
398 pub const SET_CLOCK_FLOOR: u32 = 1 << 9;
399 /// SET_CLOCK with WRITE (set the wall clock). Distinct from SET_CLOCK_FLOOR, the READ right.
400 pub const SET_CLOCK: u32 = 1 << 10;
401 /// NET_DEVICE: move ethernet frames through an in-kernel network device (`NetFrame*`/`NetInfo`,
402 /// syscalls 42-44). No port has one any more - every arch answers with a stub - and no spawn
403 /// request in the tree sets this bit; the kernel still understands it.
404 pub const NET_DEVICE: u32 = 1 << 11;
405 /// Read PCI configuration space through the legacy CF8/CFC ports (step D2). READ-ONLY and held
406 /// by ONE service - see `docs/service-ownership.md` D2 for why the write side is not on offer.
407 pub const PCI_CFG: u32 = 1 << 12;
408 /// Set the Arm cores to the platform's minimum or maximum clock (`CpuClock`, syscall 55). Held by
409 /// ONE service, `power`, because the clock is one machine-wide setting (`docs/power.md`).
410 pub const CPU_CLOCK: u32 = 1 << 13;
411 /// Turn the machine off, now or at a deadline the kernel holds, or halt it where it cannot cut its own
412 /// power (`PowerOff`, syscall 56). Held by the shell, for the operator (`docs/shutdown.md`).
413 pub const POWER_OFF: u32 = 1 << 14;
414}
415
416/// Device classes a spawner can name in `SpawnRequest::hw_flags`. The kernel resolves the class to
417/// what its own bus scan found - a CLASS rather than an address, because the kernel keeps a permanent
418/// physical DMA reservation per device that a respawned driver must get back.
419pub mod hwclass {
420 /// `hw-enumerator`'s which-device question: `[BY_CLASS, class (3 bytes LE), PREFER_OWN]`, answered
421 /// `[bdf u32, class u32]`. The supervisor asks it to choose a PCI driver's device, which the kernel
422 /// then grants by that BDF; `hardware` asks it again to say which device that driver holds. One
423 /// definition, so the two cannot disagree. `PREFER_OWN`: the first device of the class that is not
424 /// a display's companion function, if there is one (the T630's HDMI audio, `docs/audio.md`).
425 pub const BY_CLASS: u8 = 3;
426 pub const PREFER_OWN: u8 = 1;
427 pub const NONE: u32 = 0;
428 pub const NIC: u32 = 2;
429 pub const XHCI: u32 = 3;
430 pub const EHCI: u32 = 4;
431 pub const DWC2: u32 = 5;
432 pub const FRAMEBUFFER: u32 = 6;
433 /// Not a device: the software-raised test interrupt (§22 IR1). A class, so that the probe which
434 /// receives it names a CLASS like any driver and the kernel states the vector.
435 pub const TEST_IRQ: u32 = 7;
436 /// An audio jack driven by PWM and fed by the SoC's DMA engine (the Pis, `docs/audio.md`). The kernel
437 /// routes the jack's pins and starts the PWM clock as part of the grant, then maps the PWM block and
438 /// the DMA engine side by side and grants a DMA arena.
439 pub const AUDIO_PWM: u32 = 8;
440 /// A WiFi radio on an SDIO host at a fixed SoC address (the Pi 4's CYW43455 behind the Arasan, the
441 /// VisionFive 2 Lite's AIC8800 behind a DesignWare `dw_mmc`). The
442 /// kernel grants the host's window - and, where it can, the radio's power control - to the service
443 /// whose request names this kind, never to a service because of its name.
444 pub const WIFI_SDIO: u32 = 9;
445
446 /// Bit 31: the value is a PCI CLASS CODE, not one of the named kinds above.
447 pub const PCI: u32 = 1 << 31;
448 /// Bit 28: place this device behind the IOMMU (§6.4).
449 pub const PCI_CONFINE: u32 = 1 << 28;
450 /// Bit 29: this driver wants an INTERRUPT. The kernel allocates a vector from its MSI pool and
451 /// programs the device with it - the caller never names a vector, because routing one is
452 /// authority (`task::hw_irqs_for`).
453 pub const PCI_IRQ: u32 = 1 << 29;
454
455 /// Describe a PCI device by what the BUS says it is: the industry-standard 24-bit
456 /// (class, subclass, prog-if) triple, plus which BAR holds its registers.
457 ///
458 /// This is what lets a driver be added with NO KERNEL CHANGE. `0x0C0330` is xHCI on every
459 /// machine ever built, so the kernel needs no name for it - it looks the code up in the table
460 /// its boot scan already filled. The BAR index is the one extra fact, because devices differ
461 /// (xHCI 0, AHCI 5), and an INDEX is not an address, so it is safe to accept from a caller.
462 pub const fn pci(class_code: u32, bar_ix: u32, confine: bool) -> u32 {
463 PCI | (if confine { PCI_CONFINE } else { 0 }) | ((bar_ix & 0x7) << 24) | (class_code & 0x00FF_FFFF)
464 }
465
466 /// BAR index meaning "the first mapped MEMORY BAR" rather than a numbered one.
467 ///
468 /// Use this when one driver covers devices that put their registers in different BARs - the
469 /// e1000 uses BAR0 where the RTL8168 puts I/O ports there and its registers in BAR2. Both are
470 /// "the first memory BAR", and neither driver should have to know which it got.
471 pub const BAR_AUTO: u32 = 7;
472
473 /// `pci`, for a driver that needs an INTERRUPT as well.
474 pub const fn pci_irq(class_code: u32, bar_ix: u32, confine: bool) -> u32 {
475 pci(class_code, bar_ix, confine) | PCI_IRQ
476 }
477}
478
479impl SpawnRequest {
480 /// A request with everything the kernel does not yet honour left at zero.
481 pub fn new(image: &[u8], name: &str) -> Self {
482 Self {
483 version: SPAWN_REQUEST_VERSION, flags: 0,
484 image_ptr: image.as_ptr() as u64, image_len: image.len() as u64,
485 name_ptr: name.as_ptr() as u64, name_len: name.len() as u32,
486 core: u32::MAX, memory_limit: 0,
487 privileges: 0, hw_flags: 0, mmio_base: 0, mmio_len: 0,
488 dma_pages: 0, bdf: 0, irq_count: 0, irqs: [0; 4],
489 peers_ptr: 0, peers_len: 0, _pad: 0,
490 installs_ptr: 0, installs_count: 0, _pad2: 0,
491 probe_mode: 0, _pad3: 0,
492 }
493 }
494
495 /// Encode `(label, cap)` pairs into `buf` and point this request at them.
496 ///
497 /// The caller must hold each cap WITH GRANT: the kernel checks it, and that check is what makes
498 /// installing them non-escalating (7.3) - a caller that can GRANT a cap could transfer it anyway.
499 pub fn set_installs(&mut self, buf: &mut [u8], pairs: &[(&str, CapHandle)]) -> bool {
500 let mut n = 0usize;
501 for (label, cap) in pairs {
502 let lb = label.as_bytes();
503 // The kernel's limit, `PEER_NAME_BYTES`: this once accepted more than the kernel did, and the kernel refused
504 // the spawn for a label it had been told was fine (`backlog/fixed/80` S5).
505 if lb.is_empty() || lb.len() > PEER_NAME_BYTES { return false; }
506 if n + 1 + lb.len() + 2 > buf.len() { return false; }
507 buf[n] = lb.len() as u8; n += 1;
508 buf[n..n + lb.len()].copy_from_slice(lb); n += lb.len();
509 buf[n] = (cap.0 & 0xFF) as u8; buf[n + 1] = ((cap.0 >> 8) & 0xFF) as u8; n += 2;
510 }
511 self.installs_ptr = if n > 0 { buf.as_ptr() as u64 } else { 0 };
512 self.installs_count = pairs.len() as u32;
513 true
514 }
515}
516
517/// How a task ended, as the kernel says it in the death notice (`name`, NUL, then one of these). The
518/// kernel's `task::death` holds the same values; `ask` asserts its copy against these.
519pub mod death {
520 /// Ended by the `kill` syscall; the name of the task that called it follows.
521 pub const KILLED: u8 = b'K';
522 /// Ended by a user-mode CPU fault; the kind below and the address (u64 LE) follow.
523 pub const FAULT: u8 = b'F';
524 /// Ended itself with `exit`; the status it gave (u32 LE) follows.
525 pub const EXITED: u8 = b'E';
526 /// A fault the port does not classify.
527 pub const F_OTHER: u8 = 0;
528 /// A load or store its page tables do not allow; the address is the data address.
529 pub const F_MEMORY: u8 = 1;
530 /// An instruction fetch its page tables do not allow; the address is the fetch address.
531 pub const F_INSTRUCTION: u8 = 2;
532 /// An instruction the CPU will not run; the address is the instruction's.
533 pub const F_ILLEGAL: u8 = 3;
534 /// x86's general protection fault; the address is the instruction's.
535 pub const F_PROTECTION: u8 = 4;
536 /// A misaligned access the CPU refuses.
537 pub const F_ALIGNMENT: u8 = 5;
538}
539
540/// The supervisor's command channel wire format.
541///
542/// ONE definition. This lived in three services at once (`supervisor`, `control`, `shell`) - three
543/// copies of a protocol, which is the second-truth problem this project rejects everywhere else
544/// (26.4, Commandment III). A drifted copy would not fail loudly; it would send a byte the supervisor
545/// reads as a different opcode.
546///
547/// The supervisor's endpoint receives two kinds of message, told apart by the first byte:
548///
549/// ```text
550/// death notice "pong" 0 how... <- kernel-generated; a name, [a-z0-9-], then how it
551/// ended (`death`, CLAUDE.md 6.2 amendment 2026-10-11)
552/// command 0x01 op <core u32 LE> name <- 0x01 cannot begin a name
553/// name may be followed by NUL-separated PEER names
554/// ```
555///
556/// Choosing an impossible first byte means a death notice needs nothing to set it apart: it still
557/// begins with the name, as it always has (what follows the NUL is `death`'s, added 2026-10-11), so
558/// the two are unambiguous without the kernel learning anything about commands.
559pub mod supcmd {
560 /// First byte of a command. Not a legal first byte of a service name.
561 pub const MARKER: u8 = 0x01;
562 /// Kill if alive, then spawn.
563 pub const RESTART: u8 = b'R';
564 /// Spawn a service that is not running.
565 pub const SPAWN: u8 = b'S';
566 /// Which devices does the supervisor drive? Read only: `[MARKER, DEVICES]`, answered with
567 /// `[OK, count, entries...]` - per entry `b'H'`, the spawn row's device word (u32 LE, `hwclass`),
568 /// the service's name (length byte, bytes); or `b'U'`, a USB device it starts a driver for (vid
569 /// u16 LE, pid u16 LE, attached 0/1, the reporting host's name, the driver's name, each a length
570 /// byte and bytes). The `hardware` utility's DRIVER column (`docs/hardware-design.md`).
571 pub const DEVICES: u8 = b'D';
572 /// Why is this driver's device handled as it is? Read only: `[MARKER, WHY, name...]`, answered
573 /// with `[OK, text...]` - the reason the supervisor keeps beside the spawn rows - or `[UNKNOWN]`
574 /// for a service with none recorded. `hardware why` (`docs/hardware-design.md` 11).
575 pub const WHY: u8 = b'W';
576 /// What has happened to the devices this supervisor drives since it started. Read only:
577 /// `[MARKER, EVENTS]`, answered with `[OK, since u32 LE, recorded u32 LE, count, entries...]` - since:
578 /// when this supervisor began its record (after its boot spawns), in seconds of uptime; recorded: every
579 /// event noted, so `recorded - count`
580 /// were overwritten; per entry the uptime second (u32 LE), an `EV_*` code, the USB vid and pid (u16
581 /// LE each, 0 for a driver event), and the driver's name (length byte, bytes). `hardware events`.
582 pub const EVENTS: u8 = b'E';
583 /// Which services does this supervisor keep running right now? Read only: `[MARKER, MANAGED]`,
584 /// answered with `[OK, count, (len, name)*]` - the `MANAGED` services this build carries and wants
585 /// now (a USB device's driver only while its device is attached). The question `ask`'s health
586 /// answer turns on: a service on this list that is not running is down when it should be up.
587 pub const MANAGED: u8 = b'M';
588 /// How did this service last end? Read only: `[MARKER, DEATH, name...]`, answered with `[OK, since
589 /// u32 LE, deaths u32 LE, secs u32 LE, how, kind, addr u64 LE, by_len, by...]` - since: when this
590 /// supervisor began its record; deaths: the notices it has read for this name since then (0 = none,
591 /// and the rest is zero); then the newest one: its uptime second, how it ended (`death::KILLED`,
592 /// `death::FAULT`), the fault kind and address, and the name of the task that called `kill`. The
593 /// kernel says how, in the death notice; the supervisor only keeps the newest per name. `ask`.
594 pub const DEATH: u8 = b'X';
595 /// Start an installed package: `[MARKER, INSTALL, "publisher/name version"]`. The supervisor checks the fetched
596 /// copy from the disk, grants the send peers its `grant` file names and its contract requests, and starts it
597 /// under the task name `publisher/name` (`services/supervisor/src/installed.rs`). Answered `[OK]`, or
598 /// `[FAILED, reason...]` - the reason is a sentence.
599 pub const INSTALL: u8 = b'I';
600 /// Stop an installed package and forget it: `[MARKER, UNINSTALL, "publisher/name"]`. `[OK]`, or `[UNKNOWN]` when
601 /// it was not installed.
602 pub const UNINSTALL: u8 = b'O';
603 /// Which packages has this supervisor started from the disk? Read only: `[MARKER, INSTALLED]`, answered with
604 /// `[OK, count, (len, name, running 0/1)*]`. `godspeed list installed`.
605 pub const INSTALLED: u8 = b'L';
606 /// Replace a running installed package with another version WITHOUT STOPPING IT: `[MARKER, HANDOVER, "publisher/name
607 /// version"]`. The supervisor checks the new version, starts it beside the running one, and tells the running one to
608 /// hand over (`gs::handover`). Where either version cannot - it is not running, or a contract does not declare
609 /// `handover` - it stops the old and starts the new, as INSTALL after UNINSTALL would. Answered `[OK, how]`, `how`
610 /// `b'H'` handed over live or `b'R'` restarted, or `[FAILED, reason...]`.
611 pub const HANDOVER: u8 = b'H';
612 /// A device's driver died and was restarted.
613 pub const EV_RESTARTED: u8 = 1;
614 /// A device's driver died and its restart failed.
615 pub const EV_RESTART_FAILED: u8 = 2;
616 /// A USB device was reported attached and its driver started.
617 pub const EV_ATTACHED: u8 = 3;
618 /// A USB device was reported gone and its driver stopped.
619 pub const EV_DETACHED: u8 = 4;
620 /// A device's driver was found dead by the sweep - its death notice never arrived - and restarted.
621 pub const EV_SWEPT: u8 = 5;
622 /// A USB device was reported attached and its driver could not be started.
623 pub const EV_START_FAILED: u8 = 6;
624
625 /// Reply status, one byte, so a caller can log the truth rather than assume success.
626 pub const OK: u8 = 0;
627 pub const FAILED: u8 = 1;
628 pub const UNKNOWN: u8 = 2;
629
630 /// Longest command payload: opcode + core + a name and its peers, each at most `NAME_MAX` bytes.
631 pub const MAX: usize = 6 + (super::MAX_SEND_PEERS + 1) * (super::NAME_MAX + 1);
632
633 /// Build `MARKER op <core> name[\0peer]*` into `buf`, returning its length.
634 pub fn encode(buf: &mut [u8; MAX], op: u8, core: u32, name: &str, peers: &[&str]) -> Option<usize> {
635 if name.is_empty() { return None; }
636 buf[0] = MARKER;
637 buf[1] = op;
638 buf[2..6].copy_from_slice(&core.to_le_bytes());
639 let mut n = 6usize;
640 let mut put = |b: &[u8], buf: &mut [u8; MAX], n: &mut usize| -> bool {
641 if *n + b.len() > MAX { return false; }
642 buf[*n..*n + b.len()].copy_from_slice(b);
643 *n += b.len();
644 true
645 };
646 if !put(name.as_bytes(), buf, &mut n) { return None; }
647 for p in peers {
648 if !put(&[0u8], buf, &mut n) || !put(p.as_bytes(), buf, &mut n) { return None; }
649 }
650 Some(n)
651 }
652}
653
654/// A USB host's report to the supervisor about a device it does not drive itself, so the supervisor can
655/// start that device's driver when it arrives and stop it when it leaves (`docs/usb-device-drivers.md`).
656///
657/// The host reports FACTS - present or not, VID:PID, how many times it has bound one - and never names a
658/// driver: which driver a device gets is the supervisor's table. Sent with `try_send` and never answered,
659/// so a host never waits on the supervisor (8.9). Each report is the host's whole state for that device,
660/// not a change, so a duplicate is harmless and a lost one is corrected by the next.
661pub mod usbdev {
662 /// The report's opcode, after `supcmd::MARKER`: `[MARKER, REPORT, present, gen(4 LE), vid(2 LE), pid(2 LE), host]`.
663 pub const REPORT: u8 = b'U';
664 /// Supervisor to host, one byte and no reply capability: send your report again. How a respawned
665 /// supervisor learns what is attached (6.2). In the host's request space beside `usbfn`'s ops (0x20 up),
666 /// clear of them and of every block and net op.
667 pub const ASK: u8 = 0x26;
668 pub const LEN: usize = 12;
669
670 /// Which host a report is from: a PC has two that bind a device for a driver of its own, `xhci` and `ehci`, and
671 /// each reports only what IT has bound - so "nothing attached" from one says nothing of the other's.
672 pub const HOST_DWC2: u8 = 0;
673 pub const HOST_XHCI: u8 = 1;
674 pub const HOST_EHCI: u8 = 2;
675 /// How many host numbers there are: a table indexed by one is this long.
676 pub const HOSTS: usize = 3;
677
678 /// The service a host number names, or `None` for a number that is not one.
679 pub fn host_name(host: u8) -> Option<&'static str> {
680 match host {
681 HOST_DWC2 => Some("dwc2"),
682 HOST_XHCI => Some("xhci"),
683 HOST_EHCI => Some("ehci"),
684 _ => None,
685 }
686 }
687
688 /// One report. `gen` counts every bind on that host, so the same dongle bound again - a replug, or the
689 /// host re-enumerating it - reads differently from the same dongle still bound.
690 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
691 pub struct Report {
692 pub present: bool,
693 pub gen: u32,
694 pub vid: u16,
695 pub pid: u16,
696 /// The host it is from (`HOST_*`).
697 pub host: u8,
698 }
699
700 pub fn encode(r: &Report) -> [u8; LEN] {
701 let mut b = [0u8; LEN];
702 b[0] = super::supcmd::MARKER;
703 b[1] = REPORT;
704 b[2] = r.present as u8;
705 b[3..7].copy_from_slice(&r.gen.to_le_bytes());
706 b[7..9].copy_from_slice(&r.vid.to_le_bytes());
707 b[9..11].copy_from_slice(&r.pid.to_le_bytes());
708 b[11] = r.host;
709 b
710 }
711
712 /// `None` for anything that is not exactly a report.
713 pub fn decode(p: &[u8]) -> Option<Report> {
714 if p.len() != LEN || p[0] != super::supcmd::MARKER || p[1] != REPORT || p[2] > 1 || host_name(p[11]).is_none() {
715 return None;
716 }
717 Some(Report {
718 present: p[2] == 1,
719 gen: u32::from_le_bytes([p[3], p[4], p[5], p[6]]),
720 vid: u16::from_le_bytes([p[7], p[8]]),
721 pid: u16::from_le_bytes([p[9], p[10]]),
722 host: p[11],
723 })
724 }
725}
726
727
728// (Three probe-parameter flags that once rode in the upper bits of `Spawn`'s `arg0` sat here. Nothing
729// set them after the kernel's probe path went, and `Spawn` itself was retired 2026-10-10; deleted the
730// same day, `backlog/fixed/80` K14 and K20.)
731
732/// Upper bound on a spawn name payload (`name` + NUL-separated peers). Matches the kernel's limit.
733pub const SPAWN_PAYLOAD_MAX: usize = 128;
734
735/// The most one boot-record read copies (InspectKernel query 27). Equal to the kernel's
736/// `log::BOOT_READ_CHUNK`; a reader's buffer is exactly this size, so the kernel never writes past it.
737pub const BOOT_READ_CHUNK: usize = 512;
738
739/// MUST match `kernel::task::SERVICE_CTX_MAGIC`. It names the page's LAYOUT, and changes when the layout does.
740const SERVICE_CTX_MAGIC: u32 = 0xD0_5D_EA_D6;
741/// MUST match `kernel::task::MAX_SEND_PEERS` - this indexes the kernel-written context page.
742const MAX_SEND_PEERS: usize = 6;
743/// MUST match `kernel::task::PEER_NAME_BYTES`: a send-peer's name in the page the kernel writes.
744const PEER_NAME_BYTES: usize = NAME_MAX;
745/// The longest name the kernel holds - a task's, a send-peer's, a directory entry's - and so the longest a
746/// program can run under and be reached by. 128 since 2026-10-18: an installed package runs under its
747/// `publisher/name`, which may be that long. It was 24 for a peer and for acquiring by name, and 32 for a
748/// task, and a 26-byte package name was then unreachable by its own clients. Growing it changed the context
749/// page's layout, so its magic changed with it, and a package built before that is refused at start
750/// (`LAYOUT_RELEASE`).
751pub const NAME_MAX: usize = 128;
752/// The first GodspeedOS release whose program layout this one runs: the context page and `TaskStat`, which
753/// changed when names grew to 128 bytes. A package built for an earlier release would read them wrongly, so
754/// the supervisor refuses to start it and says its publisher must rebuild it.
755pub const LAYOUT_RELEASE: &str = "0.29.0";
756
757#[repr(C)]
758struct SendPeerEntry {
759 slot: u32,
760 name_len: u32,
761 name: [u8; PEER_NAME_BYTES],
762}
763
764/// Layout of the kernel-written page at SERVICE_CTX_ADDR.
765#[repr(C)]
766struct ServiceContextData {
767 magic: u32,
768 log_write_slot: u32,
769 recv_slot: u32,
770 spawn_slot: u32,
771 send_peer_count: u32,
772 core_id: u32,
773 probe_mode: u32,
774 console_read_slot: u32, // u32::MAX = not present
775 xhci_mmio_va: u64, // 0 = not mapped; else VA of the mapped xHCI BAR
776 xhci_mmio_len: u64, // length of the mapped MMIO register window in bytes (SEC-4)
777 xhci_dma_va: u64, // 0 = none; else VA of the driver's DMA arena
778 xhci_dma_phys: u64, // physical base of the DMA arena
779 xhci_dma_len: u64, // length of the DMA arena in bytes
780 console_push_slot: u32, // u32::MAX = none; else CONSOLE_PUSH cap slot
781 self_grant_slot: u32, // u32::MAX = none; else SEND|GRANT cap to own endpoint (H11)
782 // --- Framebuffer grant (the `console` service only) ---
783 // The kernel maps the display's framebuffer into this service's address space Normal NON-cacheable
784 // + USER, as a driver's MMIO BAR is mapped, and describes it here. Deliberately PIXEL geometry only:
785 // no rows, no columns, no cell size. Character geometry belongs to the terminal, and the terminal is
786 // the service (`docs/console-service.md` §9.7).
787 fb_va: u64, // 0 = no framebuffer grant; else VA of the mapped framebuffer
788 fb_len: u64, // length of the mapping in bytes (pitch * height)
789 fb_pitch: u32, // bytes per scanline
790 fb_width: u32, // visible width in pixels
791 fb_height: u32, // visible height in pixels
792 fb_bpp: u32, // bytes per pixel
793 fb_shifts: u32, // r_shift | g_shift << 8 | b_shift << 16
794 send_peers: [SendPeerEntry; MAX_SEND_PEERS],
795 /// A SECOND endpoint, for REPLIES only. `u32::MAX` = none.
796 ///
797 /// A service that serves clients on the endpoint it also awaits replies on cannot drain that
798 /// endpoint while it is blocked for a reply. Sixteen client requests arrive, the queue is full,
799 /// and the reply it is waiting for is DROPPED by a peer that (correctly) uses `try_send` rather
800 /// than deadlocking. The wait then runs to its full deadline - 30 s per block operation on x86,
801 /// which is what made `write append` take 73 seconds.
802 ///
803 /// Correlation tags cannot reach this: a tag identifies a reply that ARRIVED, and this one never
804 /// did. `docs/net-tags-design.md` rejected a second endpoint for lacking a `CreateEndpoint`
805 /// syscall - true, and not needed: the first endpoint is minted at spawn and so is this one.
806 reply_recv_slot: u32,
807 /// SEND|GRANT cap to `reply_recv_slot`'s endpoint, for handing out as a reply cap. `u32::MAX` = none.
808 reply_grant_slot: u32,
809 /// The interrupt VECTOR(s) this service was granted, so a driver can learn which one it got.
810 ///
811 /// The kernel used to hand every driver a vector it could also hardcode: `XHCI_MSI_VECTOR` was a
812 /// kernel constant, so both sides knew 0x28 statically and a driver comparing against its own copy
813 /// was correct. The MSI pool (step D1b) makes the vector ALLOCATED AT SPAWN, and that quietly
814 /// invalidated the hardcoded copy: the kernel began routing 0x30 while `services/xhci` still tested
815 /// for 0x28, so it classified every real interrupt as an unknown message and reported `0 MSI, 7799
816 /// msg` on hardware. Nothing broke - the interrupt still WOKE the driver - but the one instrument
817 /// that says whether interrupts work had started reporting the opposite.
818 ///
819 /// The design gap was this field's absence, not the stale constant. A driver already learns its
820 /// MMIO window and DMA arena from the kernel (`ctx.mmio()`, `ctx.dma_region()`) precisely because
821 /// it may not name them; its vector is the same kind of fact and now travels the same way. A
822 /// caller still cannot ASK for a particular vector - that is authority (§12.3) - it can only be
823 /// told which one it was given.
824 irq_count: u32, // 0 = no interrupt granted
825 irqs: [u8; 4], // the granted vectors; only the first `irq_count` are meaningful
826}
827
828// The kernel writes this struct and the SDK reads it, from two crates, with no shared definition -
829// they are kept in step BY HAND. There was no check on that, and adding a field to one and not the
830// other silently misaligns every field after it: a service would read its neighbour's slot numbers.
831//
832// Pinned by SIZE in both crates. It does not prove field ORDER, but it catches the mistake that
833// actually happens - an append on one side only - and it fails at compile time in the crate that
834// drifted rather than at boot in a service that reads garbage.
835const SERVICE_CONTEXT_DATA_SIZE: usize = 952; // 320 + irq_count(4) + irqs[4] (D1b); then each of the 6 send-peer names 24 -> 128 bytes (2026-10-18), +624
836const _: () = assert!(
837 core::mem::size_of::<ServiceContextData>() == SERVICE_CONTEXT_DATA_SIZE,
838 "ServiceContextData changed size: update BOTH kernel/src/task/mod.rs and sdk/rust/src/service_context.rs, then update SERVICE_CONTEXT_DATA_SIZE in both"
839);
840
841
842// ---------------------------------------------------------------------------
843// Dynamic send-cap cache - updated by `reacquire_cap` after EndpointDead.
844// Safe: each service is a single-threaded process with its own BSS.
845// ---------------------------------------------------------------------------
846
847/// How many reacquired peers a service can hold at once.
848///
849/// **It must cover every peer a service names, because a peer that does not fit used to be dropped in
850/// silence.** It was 8. The shell is wired at spawn with `fs` alone and reaches every other peer through
851/// this cache, and it names more than eight - net-stack, wifi-driver, events, time, console, block-driver,
852/// power, supervisor, nic-driver, recorder and more. After a chaos storm the respawned shell's `selfcheck`
853/// filled the cache before reaching the network, so `reacquire_by_name("net-stack")` returned `true` while
854/// storing nothing: every `ping` then found no send slot, reported `net-stack not responding` at once, and
855/// leaked the capability it had just acquired - and killing net-stack could not help (Pi 4, 2026-10-04).
856///
857/// 32 is above the supervisor's whole managed roster. A full cache now EVICTS (see `reacquire_cap_detail`)
858/// and says so, rather than refusing the newcomer quietly.
859const CACHE_SIZE: usize = 32;
860
861/// The next entry to evict when the cache is full - round robin, so no one peer is always the victim.
862// SAFETY: single-threaded service process; no concurrent access.
863static mut CACHE_NEXT_EVICT: usize = 0;
864
865struct CacheEntry {
866 slot: u32,
867 name_len: u8,
868 name: [u8; NAME_MAX],
869}
870
871impl CacheEntry {
872 /// ALL ZEROS, and that is load-bearing: an empty entry is one with no name (`name_len == 0`), so the
873 /// whole table is zero and lives in `.bss`, which costs a program's image nothing. It used to mark empty
874 /// with `slot: u32::MAX`, which put the table in `.data` - stored in every program's file - and when names
875 /// grew to 128 bytes that was 4 KiB more in each of the forty-odd services the Pi 2's kernel embeds, and
876 /// pushed that kernel past the context page it must end below.
877 const fn empty() -> Self {
878 CacheEntry { slot: 0, name_len: 0, name: [0u8; NAME_MAX] }
879 }
880}
881
882// SAFETY: single-threaded service process; no concurrent access.
883static mut SEND_CAP_CACHE: [CacheEntry; CACHE_SIZE] =
884 [const { CacheEntry::empty() }; CACHE_SIZE];
885
886// ---------------------------------------------------------------------------
887// TaskStat - returned by ServiceContext::task_stat.
888// ---------------------------------------------------------------------------
889
890/// Snapshot of kernel task state for a single scheduler slot.
891#[derive(Clone, Copy)]
892pub struct TaskStat {
893 /// True if the slot holds a live task.
894 pub valid: bool,
895 /// Task state: 0=Ready, 1=Running, 2=BlockedOnRecv, 3=BlockedOnSend, 4=Dead.
896 pub state: u8,
897 /// Core the task is pinned to.
898 pub core: u8,
899 /// Bytes dynamically allocated so far.
900 pub mem_used: u64,
901 /// Maximum bytes the task may allocate.
902 pub mem_limit: u64,
903 /// Byte length of the name stored in `name`.
904 pub name_len: usize,
905 /// Task name bytes (zero-padded to `NAME_MAX` bytes).
906 pub name: [u8; NAME_MAX],
907 /// Number of times this service has been restarted (0 on a fresh boot / first spawn, +1 per
908 /// respawn). Saturating u64 - effectively unbounded.
909 pub restart_count: u64,
910 /// Current inbound IPC queue depth (0-16).
911 pub queue_depth: u8,
912 /// Timer ticks spent as the running task on its core (monotonic since boot).
913 pub run_ticks: u64,
914 /// Seconds since this service last (re)started - resets on restart. Per-service uptime.
915 pub uptime_secs: u64,
916}
917
918impl TaskStat {
919 /// Return the task name as a `&str`.
920 pub fn name_str(&self) -> &str {
921 let len = self.name_len.min(NAME_MAX);
922 core::str::from_utf8(&self.name[..len]).unwrap_or("?")
923 }
924
925 /// Return a short human-readable state label.
926 pub fn state_str(&self) -> &'static str {
927 match self.state {
928 0 => "Ready",
929 1 => "Running",
930 2 => "BlockRecv",
931 3 => "BlockSend",
932 4 => "Dead",
933 _ => "?",
934 }
935 }
936}
937
938/// One held capability, as reported by [`ServiceContext::task_caps`].
939#[derive(Clone, Copy, Default)]
940pub struct CapInfo {
941 /// Resource the cap targets. Stable kernel resources: 1=log_write, 2=spawn,
942 /// 3=console_read, 4=console_push, 5=introspect, 6..=18 the other kernel
943 /// authorities (`kernel/src/capability/mod.rs`, e.g. 6=service_control,
944 /// 8=reboot, 15=image_spawn); larger ids are IPC endpoints, and [4096, 6144)
945 /// is the delegated-resource band (§7.10).
946 pub resource_id: u64,
947 /// Rights bitfield: READ=1, WRITE=2, SEND=4, RECV=8, GRANT=16, REVOKE=32.
948 pub rights: u8,
949}
950
951/// THE KERNEL'S FIXED AUTHORITIES, by resource id: (id, the name a contract uses, what it lets a task do).
952///
953/// One table for everything that names them - the shell's `caps` and `ask "who can reboot?"` - so what a
954/// service is observed to HOLD reads the same everywhere, and in the words its contract DECLARES
955/// (`pci_cfg = true`). The ids are `kernel/src/capability/mod.rs`'s `*_RESOURCE`; anything above 18 is an
956/// IPC endpoint. A resource added there and not here would be shown as an endpoint, which names the
957/// wrong kind of thing (26.9), so the two move together.
958pub const AUTHORITIES: &[(u64, &str, &str)] = &[
959 (1, "log_write", "write to the kernel log and serial"),
960 (2, "spawn", "start services"),
961 (3, "console_read", "read the keyboard"),
962 (4, "console_push", "type into the console as if at the keyboard"),
963 (5, "introspect", "see every task, its capabilities and the kernel's counters"),
964 (6, "service_control", "kill and restart services"),
965 (7, "resource_mint", "mint capabilities for resources it owns, as fs does for files"),
966 (8, "reboot", "reboot the machine"),
967 (9, "acquire_any", "reach any service by name, not only its declared peers"),
968 (10, "net_device", "move raw ethernet frames on an in-kernel network device (none exists; granted to nothing now)"),
969 (11, "gpio_device", "drive the GPIO pins"),
970 (12, "usb_disk", "read and write any sector of an in-kernel USB disk (none exists; granted to nothing now)"),
971 (13, "set_clock", "set the wall clock (granted to nothing now)"),
972 (14, "fire_irq", "inject a test interrupt"),
973 (15, "image_spawn", "start a program from an image it supplies"),
974 (16, "pci_cfg", "read and write PCI configuration space"),
975 (17, "device_power", "cut and restore the power of the device it was granted"),
976 (18, "cpu_clock", "set the Arm clock"),
977];
978
979// ---------------------------------------------------------------------------
980// AllocError - returned by ServiceContext::alloc_mem.
981// ---------------------------------------------------------------------------
982
983/// Error from the AllocMem syscall (6).
984#[derive(Debug, Clone, Copy, PartialEq, Eq)]
985pub enum AllocError {
986 /// Allocation would exceed the task's memory limit (§10.3).
987 Denied,
988 /// Physical memory exhausted or other kernel-side failure.
989 Failed,
990}
991
992// ---------------------------------------------------------------------------
993// ServiceContext.
994// ---------------------------------------------------------------------------
995
996// (2026-10-09) HISTORY, kept as `//` so rustdoc does not hang it on `ServiceContext`. The wait
997// helpers it describes BLOCK again now - each waits in `await_slice`
998// (`RecvTimeout`), or in the kernel's `Call` / `CallDeadline` - so its first sentence is no longer true.
999//
1000// These wait helpers POLL (`try_recv` + `yield_cpu`); they do not block. That is deliberate, and it is
1001// a REVERSAL - they were made to block earlier on this branch, and the change was wrong twice over.
1002//
1003// Why it was tried: a blocked task lets the core reach the scheduler's idle path, which saves power and
1004// (on ARM) lets idle-hook work run while a command waits. Both are real benefits.
1005//
1006// Why it is reverted:
1007// 1. **It broke x86 networking.** net-stack and nic-driver both sit on core 1, and every exchange
1008// between them goes through `request_with_reply_deadline`. With blocking waits, net-stack degraded
1009// to "no NIC MAC yet (driver absent/not ready)" and DHCP/ARP never completed; restoring the poll
1010// made DHCP, ARP and a sustained 14 ms ping work immediately. Proven by an A/B on real hardware -
1011// identical build, this one difference.
1012// 2. **It bought nothing that survived.** It was introduced to make USB hot-plug get noticed during a
1013// `ping`. That symptom turned out to be the chaos harness pacing itself with a fixed yield count
1014// (3000 yields x a full quantum = 30 s on the Pi), not these helpers. The hot-plug fixes that
1015// actually worked were the hub change-latch and the catch-up sweep, neither of which needs this.
1016//
1017// It also exposed a genuine kernel bug on the way through - the BSP halting onto a consumed one-shot
1018// TSC deadline - which is FIXED and stays fixed independently of this revert.
1019//
1020// The lesson worth keeping: same-core request/reply through a blocking waiter is not exercised by the
1021// test suite (identity and fs-restart are all cross-core), so a change here cannot be validated by
1022// those suites. If blocking is attempted again, it needs a same-core request/reply test first.
1023
1024/// Passed by the kernel to `service_main`. Non-Copy; one per service instance.
1025pub struct ServiceContext {
1026 _private: (),
1027}
1028
1029/// The USB-disk syscalls' "device NAKed, re-ask" status.
1030///
1031/// The kernel claims no USB disk on any port any more (every arch's `usb_disk_sectors()` is 0, and the
1032/// USB stacks are services), so the four `usb_disk_*` syscalls have no caller in the tree; kept as the
1033/// record of their ABI.
1034///
1035/// Outside the capability-error range (-2..-7) ON PURPOSE, and this must stay in step with the kernel's
1036/// `USB_DISK_BUSY` (`kernel/src/syscall/dispatch.rs`). It was originally `-2`, which is `CapNotHeld`, so
1037/// a driver missing its `USB_DISK_RESOURCE` capability was indistinguishable from a busy device and got retried
1038/// thousands of times before being reported as a device that "stayed busy" - a cap failure wearing an
1039/// I/O failure's name.
1040pub const USB_DISK_BUSY: i64 = -20;
1041
1042/// No USB disk is attached - the opposite instruction to [`USB_DISK_BUSY`], and it must stay in step with
1043/// the kernel's `USB_DISK_ABSENT`. BUSY means "come back, I am working"; ABSENT means "there is nothing
1044/// here, and asking again cannot change that - only a hot-plug can". A caller that retries on this waits
1045/// out its entire budget against an empty socket and then reports the device as busy, which is a false
1046/// statement about hardware sitting on the operator's desk.
1047pub const USB_DISK_ABSENT: i64 = -21;
1048
1049/// Can this LBA survive the syscall ABI intact?
1050///
1051/// The ARM ABI gives each syscall argument exactly ONE 32-bit register (`arch/arm/CLAUDE.md`, hazard
1052/// A-U1), so a wider value is silently narrowed on the way in. That matters here more than anywhere:
1053/// the kernel guards `lba >= MSC_SECTORS` and then builds a READ(10)/WRITE(10) CDB, but it guards the
1054/// value it RECEIVED. An LBA of 0x1_0000_0000 arrives as 0, passes the range check, and overwrites the
1055/// GSFS superblock while the device, `block-driver` and `fs` all report success - a range check
1056/// defeated one level above itself, and silent corruption of exactly the block the check exists to
1057/// protect. On x86 the same request is rejected loudly, so the two ports would disagree about what is
1058/// a valid write.
1059///
1060/// Rejecting it in the wrapper is the fix the ABI hazard prescribes (clamp before the syscall, or split
1061/// into a register pair). Nothing legitimate is refused: capacity comes from SCSI READ CAPACITY(10),
1062/// whose last-LBA field is 32 bits wide, so an LBA above `u32::MAX` is a caller bug - and it is
1063/// reported as a failure rather than aliased onto a real block (Invariant 12, failures are loud).
1064#[inline]
1065fn lba_fits_syscall_abi(lba: u64) -> bool { lba <= u32::MAX as u64 }
1066
1067impl ServiceContext {
1068 /// A handle for the PANIC HANDLER, which has no `ctx` to borrow.
1069 ///
1070 /// This type is a zero-sized token - every method reads the kernel-written context block rather
1071 /// than any field - so making one costs nothing and grants nothing that was not already granted.
1072 /// It is `pub(crate)` because the panic path is the only caller that legitimately lacks a context:
1073 /// a service is handed one and must thread it, and an escape hatch for anyone else would let a
1074 /// caller pretend to authority it was never given.
1075 pub(crate) fn for_panic() -> Self {
1076 ServiceContext { _private: () }
1077 }
1078
1079 #[inline]
1080 fn ctx() -> &'static ServiceContextData {
1081 // SAFETY: kernel maps a valid ServiceContextData at SERVICE_CTX_ADDR
1082 // before SYSRETQ into the service; page is read-only and lifetime-stable.
1083 unsafe { &*(SERVICE_CTX_ADDR as *const ServiceContextData) }
1084 }
1085
1086 /// Look up a named capability from this service's cap table.
1087 pub fn capability(&self, name: &str) -> Result<CapHandle, CapError> {
1088 let data = Self::ctx();
1089 if data.magic != SERVICE_CTX_MAGIC { return Err(CapError::CapNotHeld); }
1090 match name {
1091 "log_write" if data.log_write_slot != u32::MAX =>
1092 Ok(CapHandle(data.log_write_slot)),
1093 "spawn" if data.spawn_slot != u32::MAX =>
1094 Ok(CapHandle(data.spawn_slot)),
1095 "recv" if data.recv_slot != u32::MAX =>
1096 Ok(CapHandle(data.recv_slot)),
1097 _ => Err(CapError::CapNotHeld),
1098 }
1099 }
1100
1101 /// Publish `msgs.received = 0` the first time this service WAITS for a message.
1102 ///
1103 /// The third case of the same bug the comment inside `count_recv` records twice. Publishing on the first
1104 /// message covers a service that is quiet; republishing on a sink change covers a sink that
1105 /// restarted. Neither covers a SERVICE that restarted and has not been spoken to since - and that
1106 /// is the ordinary state of `block-driver` after a chaos storm, because nothing sends to it until
1107 /// somebody does disk I/O. Observed on the Wyse: 1917 log lines between
1108 /// `supervisor: block-driver restarted` and a metrics query that found no row for it, with zero
1109 /// messages received in between. The selfcheck caught it as
1110 /// `where owner contains block-driver | assert contains msgs.received`.
1111 ///
1112 /// A row that is absent reads as dead, which is the exact question this metric exists to answer -
1113 /// so an idle live service must have one.
1114 ///
1115 /// THE ZERO IS EARNED, which is the only kind worth printing: the service is running, it has
1116 /// entered its receive loop, and it has genuinely received nothing. That is a fact about the
1117 /// system, not a placeholder standing in for one. Emitted once per instance, from the wait rather
1118 /// than from startup, because reaching the wait is what proves the service got as far as serving.
1119 #[inline]
1120 fn note_listening(&self) {
1121 static ANNOUNCED: core::sync::atomic::AtomicBool =
1122 core::sync::atomic::AtomicBool::new(false);
1123 if !ANNOUNCED.swap(true, core::sync::atomic::Ordering::Relaxed) {
1124 self.metric("msgs.received", 0);
1125 }
1126 }
1127
1128 /// Count one RECEIVED message, and publish the running total every 64.
1129 ///
1130 /// In the SDK rather than in each service, for the same reason trace emission is: it is the same
1131 /// counter in every one of them, and ten hand-placed copies is ten chances to place it wrong. Every
1132 /// receive path funnels here, and only a message that actually ARRIVED is counted - a polling
1133 /// driver calling `try_recv` in a tight loop counts nothing until something comes.
1134 ///
1135 /// WHY THIS ONE IS WORTH HAVING EVERYWHERE. Paired with the `age_s` column, it answers the
1136 /// question an operator actually has when something is wrong: not "how busy is this service" but
1137 /// "is it doing ANYTHING". A `block-driver` whose count last moved forty seconds ago is a
1138 /// different fault from one that is still receiving and failing, and nothing else in the system
1139 /// distinguishes them.
1140 ///
1141 /// Costs a service with no `events` cap one relaxed add and a not-taken branch; `events` itself
1142 /// holds no send cap to itself, so its own publish resolves to `u32::MAX` and returns - the same
1143 /// cut that stops it tracing its own sends. No recursion is possible.
1144 #[inline]
1145 fn count_recv(&self) {
1146 static SERVED: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
1147 const PUBLISH_EVERY: u64 = 64;
1148 let n = SERVED.fetch_add(1, core::sync::atomic::Ordering::Relaxed) + 1;
1149 // THE FIRST MESSAGE PUBLISHES TOO, not just every 64th. Publishing only on the interval meant a
1150 // service that had received fewer than 64 messages had NO ROW AT ALL - and a service with no row
1151 // is indistinguishable from one that is dead, which is precisely the question this metric exists
1152 // to answer. Observed: `block-driver`, `time`, `xhci`, `ehci` and `dwc2` all absent from a
1153 // healthy machine. One extra send per service for the life of the machine.
1154 // ...OR THE SINK JUST CAME BACK. A restarted sink has never heard of this service, and
1155 // waiting out the interval leaves it looking dead for up to 63 more messages - on a service
1156 // that only receives when someone else does I/O, that is however long the machine stays
1157 // quiet. Republishing on the first message after a reacquire is the metric half of "reacquire
1158 // and re-establish" (14.3). Observed: `block-driver` had no row for the whole window after a
1159 // chaos storm restarted the sink, purely because its next 64th message had not arrived.
1160 if n == 1 || n % PUBLISH_EVERY == 0 || crate::trace::take_sink_changed() {
1161 self.metric("msgs.received", n);
1162 }
1163 }
1164
1165 /// Block until a message arrives on this service's primary recv endpoint.
1166 ///
1167 /// **Every failure here now PANICS rather than spinning.** All three exits used to be `loop {}`:
1168 /// a silent, logless, non-yielding tight spin that pegged the core. That is the worst possible
1169 /// response to `EndpointDead`, which is not corruption but an ordinary runtime truth (§8.6) - the
1170 /// service was told its endpoint died and answered by burning a core forever, telling nobody.
1171 ///
1172 /// Panicking is now the RIGHT answer because the panic handler was fixed in the same pass: it
1173 /// logs and faults, so the kernel kills the task, bumps its endpoint generation, wakes any peer
1174 /// blocked in `call` with `ReplyDead`, and the supervisor restarts it. Loud, and recovered.
1175 ///
1176 /// A service that wants to HANDLE the error instead of dying should call `recv_result`.
1177 pub fn recv(&self) -> Message {
1178 self.note_listening();
1179 let data = Self::ctx();
1180 if data.magic != SERVICE_CTX_MAGIC {
1181 panic!("recv: corrupt ServiceContext (bad magic) - the kernel handed us an unusable context");
1182 }
1183 let slot = data.recv_slot;
1184 if slot == u32::MAX {
1185 panic!("recv: no receive endpoint - this service has no ipc_receive in its contract");
1186 }
1187 match crate::ipc::recv(CapHandle(slot)) {
1188 Ok(msg) => { self.count_recv(); msg }
1189 Err(e) => panic!("recv failed: {:?} - endpoint is gone; dying so the supervisor restarts us", e),
1190 }
1191 }
1192
1193 /// Non-blocking receive on this service's primary recv endpoint: `Some(msg)` if a
1194 /// message was waiting, `None` if the queue is empty. A busy-polling driver uses this
1195 /// to drain interrupt events (§12) each loop iteration without blocking.
1196 ///
1197 /// A receive ERROR on this service's OWN endpoint (it is dead, or the cap was refused) is not an
1198 /// empty queue: the service dies loudly, exactly as [`Self::recv`] does, so the supervisor
1199 /// restarts it. It read as `None` until 2026-10-10, and a loop polling a dead endpoint spun on it
1200 /// forever (`backlog/fixed/80` S5).
1201 pub fn try_recv(&self) -> Option<Message> {
1202 self.note_listening();
1203 let data = Self::ctx();
1204 if data.magic != SERVICE_CTX_MAGIC { return None; }
1205 let slot = data.recv_slot;
1206 if slot == u32::MAX { return None; }
1207 let m = match crate::ipc::try_recv(CapHandle(slot)) {
1208 Ok(m) => m,
1209 Err(e) => panic!("try_recv failed: {:?} - endpoint is gone; dying so the supervisor restarts us", e),
1210 };
1211 if m.is_some() { self.count_recv(); }
1212 m
1213 }
1214
1215 /// Block on this service's recv endpoint until a message arrives or `timeout_cycles`
1216 /// (cycle-counter ticks, the unit of `read_tsc` - convert with `duration_cycles`) elapse:
1217 /// `Some(msg)` = message, `None` = timed out. A receive ERROR on this service's own endpoint dies
1218 /// loudly, as [`Self::recv`] and [`Self::try_recv`] do (`backlog/fixed/80` S5).
1219 /// `timeout_cycles == 0` blocks forever. The kernel turns the count into whole scheduler ticks
1220 /// (at least one), so the bound is never finer than one quantum. A driver uses this to idle on
1221 /// its hardware interrupt while still waking on a timer for auto-repeat (§12 timed-wait).
1222 ///
1223 /// It returns a 4 KiB `Message` by value. It once carried `#[inline(always)]` to save that frame;
1224 /// 8f32a5e9 removed it, because inlining a body that holds a 4 KiB local into a caller that holds
1225 /// one puts both in the same frame.
1226 pub fn recv_timeout(&self, timeout_cycles: u64) -> Option<Message> {
1227 self.note_listening();
1228 let data = Self::ctx();
1229 if data.magic != SERVICE_CTX_MAGIC { return None; }
1230 let slot = data.recv_slot;
1231 if slot == u32::MAX { return None; }
1232 let m = match crate::ipc::recv_timeout(CapHandle(slot), timeout_cycles) {
1233 Ok(m) => m,
1234 Err(e) => panic!("recv_timeout failed: {:?} - endpoint is gone; dying so the supervisor restarts us", e),
1235 };
1236 if m.is_some() { self.count_recv(); }
1237 m
1238 }
1239
1240 /// Re-open the kernel's IOAPIC gate for a level-triggered IRQ `vector` after this driver
1241 /// has cleared its device's interrupt source (§12). The kernel masks a level INTx while the
1242 /// driver handles it (so it can't storm); call this to let it fire again. Only the driver
1243 /// registered for `vector` (via its `hw_interrupt` route) may unmask it. No-op for MSI.
1244 pub fn irq_unmask(&self, vector: u8) {
1245 // SAFETY: syscall(36) = IrqUnmask; gated kernel-side by the route registration.
1246 let _ = unsafe { raw_syscall(36, vector as u64, 0, 0) };
1247 }
1248
1249 /// Block this task for roughly `cycles` TSC cycles, then return (syscall 37). A real sleep:
1250 /// the core can halt while parked, so a poll/wait loop does not busy-`yield` (which pegs the
1251 /// core at ~100% and makes every task on it read as fully busy in `observe`). Like `yield`,
1252 /// needs no capability. A sleep under one quantum (10 ms) uses the arch's sub-tick timer where it
1253 /// has one (`hires_arm`), with a tick backstop; otherwise granularity is one scheduler quantum.
1254 /// Use for UI repaint pacing and "wait for child" loops - not for precise timing. On a 32-bit
1255 /// target `cycles` is clamped to what one register carries, so a long sleep is shortened to the
1256 /// longest the ABI can express rather than wrapping to a short one (`backlog/fixed/80` S5).
1257 pub fn sleep(&self, cycles: u64) {
1258 let cycles = cycles.min(usize::MAX as u64);
1259 // SAFETY: syscall(37) = Sleep; sleeping your own task is unprivileged (like yield).
1260 let _ = unsafe { raw_syscall(37, cycles, 0, 0) };
1261 }
1262
1263 // `net_frame_tx`, `net_frame_rx` and `net_info` (syscalls 42-44, NET_DEVICE) were here until
1264 // 2026-10-11: no port has an in-kernel network device, nothing is granted NET_DEVICE, and nothing
1265 // called them (`backlog/21` step 2). The syscalls themselves are the last step, a kernel change.
1266
1267 /// Block until a message arrives; returns the error instead of looping silently.
1268 pub fn recv_result(&self) -> Result<Message, crate::ipc::IpcError> {
1269 let data = Self::ctx();
1270 if data.magic != SERVICE_CTX_MAGIC { return Err(crate::ipc::IpcError::EndpointDead); }
1271 let slot = data.recv_slot;
1272 if slot == u32::MAX { return Err(crate::ipc::IpcError::EndpointDead); }
1273 crate::ipc::recv(CapHandle(slot))
1274 }
1275
1276 /// Send to a named peer declared in `ipc_send`. Blocking.
1277 pub fn send(&self, peer: &str, msg: &Message) -> Result<(), IpcError> {
1278 let slot = self.find_send_slot(peer).ok_or(IpcError::CapError(CapError::CapNotHeld))?;
1279 crate::ipc::send(CapHandle(slot), msg)
1280 }
1281
1282 /// Non-blocking send; returns `QueueFull` immediately if the queue is full.
1283 pub fn try_send(&self, peer: &str, msg: &Message) -> Result<(), IpcError> {
1284 let slot = self.find_send_slot(peer).ok_or(IpcError::CapError(CapError::CapNotHeld))?;
1285 crate::ipc::try_send(CapHandle(slot), msg)
1286 }
1287
1288 /// Acquire a fresh SEND cap to `peer` via the kernel name directory.
1289 ///
1290 /// Called after `try_send` returns `EndpointDead` (§14.2). Updates the
1291 /// per-service dynamic cap cache so subsequent `try_send` calls use the
1292 /// new slot without going to the kernel again. Every failure collapses to
1293 /// `CapNotHeld`; `reacquire_cap_detail` says which.
1294 pub fn reacquire_cap(&self, peer: &str) -> Result<CapHandle, CapError> {
1295 self.reacquire_cap_detail(peer).map_err(|_| CapError::CapNotHeld)
1296 }
1297
1298 /// `reacquire_cap`, but saying WHICH failure - see [`AcquireFailure`].
1299 ///
1300 /// Use this wherever the answer changes what the caller does or prints. The plain
1301 /// `reacquire_cap`/`reacquire_by_name` collapse every failure to one value, which is fine for a
1302 /// caller that only retries, and wrong for one that reports: a message naming two possible causes
1303 /// and committing to neither is a message that sends the reader to the wrong service (§26.7).
1304 pub fn reacquire_cap_detail(&self, peer: &str) -> Result<CapHandle, AcquireFailure> {
1305 let bytes = peer.as_bytes();
1306 let len = bytes.len();
1307 if len == 0 || len > NAME_MAX { return Err(AcquireFailure::Refused); }
1308
1309 // SAFETY: syscall(10) = AcquireSendCap; peer bytes are in user space.
1310 let ret = unsafe {
1311 raw_syscall(10, bytes.as_ptr() as u64, len as u64, 0)
1312 };
1313 if ret < 0 { return Err(AcquireFailure::from_code(ret)); }
1314 let new_slot = ret as u32;
1315
1316 // Update the dynamic cache. Reuse this peer's EXISTING entry if it has one (and reclaim its
1317 // now-stale cap), otherwise take a free slot. Reclaiming the old cap is essential: without it
1318 // every restart-reacquire orphans the previous cap, and a storm (e.g. `chaos max-carnage`)
1319 // fills the cap table until `derive_cap`/`acquire_send_cap` start returning None - which shows
1320 // up as "storage unavailable" (fs) and "never registered" (pipe filters). Searching the peer's
1321 // entry first also prevents creating a duplicate entry when a free slot precedes it.
1322 // SAFETY: single-threaded service; no concurrent cache writes. addr_of_mut! avoids a direct
1323 // &mut to the static (static_mut_refs lint).
1324 //
1325 // NEVER DROP THE NEW CAP. If the peer has no entry and no entry is free, an existing one is
1326 // evicted - its cap reclaimed, its peer reacquired on next use - and the eviction is logged. This
1327 // used to fall through doing nothing and still return Ok: the caller believed the peer was
1328 // reacquired, `find_send_slot` could not find it, and the cap just acquired leaked (CACHE_SIZE).
1329 let mut stale: Option<u32> = None;
1330 let mut evicted: Option<([u8; NAME_MAX], usize)> = None;
1331 let mut placed = false;
1332 unsafe {
1333 let cache = &mut *core::ptr::addr_of_mut!(SEND_CAP_CACHE);
1334 for entry in cache.iter_mut() {
1335 if entry.name_len as usize == len && &entry.name[..len] == bytes {
1336 if entry.slot != new_slot { stale = Some(entry.slot); }
1337 entry.slot = new_slot;
1338 placed = true;
1339 break;
1340 }
1341 }
1342 if !placed {
1343 let free = cache.iter().position(|e| e.name_len == 0);
1344 let i = match free {
1345 Some(i) => i,
1346 None => {
1347 let next = &mut *core::ptr::addr_of_mut!(CACHE_NEXT_EVICT);
1348 let i = *next % CACHE_SIZE;
1349 *next = next.wrapping_add(1);
1350 stale = Some(cache[i].slot);
1351 evicted = Some((cache[i].name, cache[i].name_len as usize));
1352 i
1353 }
1354 };
1355 let entry = &mut cache[i];
1356 entry.slot = new_slot;
1357 entry.name_len = len as u8;
1358 entry.name = [0u8; NAME_MAX];
1359 entry.name[..len].copy_from_slice(bytes);
1360 }
1361 }
1362 if let Some(old) = stale { self.remove_cap(CapHandle(old)); }
1363 if let Some((name, nlen)) = evicted {
1364 let gone = core::str::from_utf8(&name[..nlen.min(NAME_MAX)]).unwrap_or("?");
1365 self.log_fmt(format_args!(
1366 "sdk: the send-cap cache is full ({} peers) - dropped the cap to '{}' to hold '{}'; '{}' is reacquired when next used",
1367 CACHE_SIZE, gone, peer, gone));
1368 }
1369
1370 Ok(CapHandle(new_slot))
1371 }
1372
1373 /// Derive a duplicate of a capability this service holds **with the GRANT right**
1374 /// into a fresh slot (syscall 29 = `DeriveCap`). The copy carries the same
1375 /// resource, generation, and (non-widened) rights.
1376 ///
1377 /// Used to hand out many copies of a held endpoint cap: derive a copy per
1378 /// recipient and grant that copy away (via `send_with_cap_by_handle`) while
1379 /// keeping the original. Returns `None` if the cap lacks GRANT, is stale, or the
1380 /// cap table is full.
1381 pub fn derive_cap(&self, held: CapHandle) -> Option<CapHandle> {
1382 // SAFETY: syscall(29) = DeriveCap; `held.0` is a slot index into this task's
1383 // own cap table. The kernel validates GRANT + generation before duplicating.
1384 let ret = unsafe { raw_syscall(29, held.0 as u64, 0, 0) };
1385 if ret < 0 { None } else { Some(CapHandle(ret as u32)) }
1386 }
1387
1388 /// Return the probe mode written by the kernel at spawn (0 for all production services).
1389 pub fn probe_mode(&self) -> u32 { Self::ctx().probe_mode }
1390
1391 /// The REPLY mailbox: `(recv, grant)` for the endpoint that exists only to receive replies, or
1392 /// `None` on a task that has none (then the caller uses the shared endpoint, as before).
1393 ///
1394 /// Awaiting a reply on the endpoint you also SERVE means you cannot drain client traffic while
1395 /// you wait; the queue fills and your own reply is dropped. Replies come here instead, where
1396 /// nothing else is ever sent.
1397 fn reply_mailbox(&self) -> Option<(crate::capability::CapHandle, crate::capability::CapHandle)> {
1398 let d = Self::ctx();
1399 if d.reply_recv_slot == u32::MAX || d.reply_grant_slot == u32::MAX { return None; }
1400 Some((crate::capability::CapHandle(d.reply_recv_slot),
1401 crate::capability::CapHandle(d.reply_grant_slot)))
1402 }
1403
1404 /// Discard replies left over from requests this service already gave up on. Returns how many.
1405 ///
1406 /// **A DETECTED DESYNC HAS TO BE REPAIRABLE, and this is the repair.** `fs` tags every block
1407 /// request and checks the tag on the reply, so a stale reply is always DETECTED - and detection
1408 /// was all there was. The mismatched reply was discarded and the request failed, which leaves this
1409 /// service's own reply still queued for the NEXT request to find. Every subsequent request then
1410 /// receives its predecessor's reply, discards it, fails, and queues another: permanently one reply
1411 /// out of phase, alternating forever, with the volume mounted and every operation failing.
1412 ///
1413 /// Observed 133 times in one `osdev test peer-storm` run, still alternating after the storm had
1414 /// stopped. The code that discarded expected the queue to drain on its own - "the caller retries
1415 /// at its own level, by which point the queue has drained" - and it never does, because each
1416 /// retry consumes exactly one stale reply and contributes exactly one more.
1417 ///
1418 /// WHY DRAINING IS SAFE HERE, which is the whole reason this is in the SDK rather than hand-rolled
1419 /// at a call site: it drains the dedicated REPLY MAILBOX, an endpoint that carries nothing but
1420 /// replies to calls this service made. A caller that is synchronous - one outstanding request at a
1421 /// time, which is what `request_with_reply*` enforces - can have nothing legitimate waiting there
1422 /// at the moment it is about to issue a new request. Anything present is a reply it stopped
1423 /// waiting for.
1424 ///
1425 /// **It refuses to drain the SHARED endpoint**, and that refusal is load-bearing. A service
1426 /// without a reply mailbox awaits replies on the endpoint its CLIENTS send to, so draining there
1427 /// would silently eat client requests - the precise hazard the `CallDeadline` amendment (§8.2)
1428 /// exists to describe. No mailbox, no drain, and the caller keeps the behaviour it had.
1429 ///
1430 /// SO THIS REPAIR IS CONDITIONAL, and the condition is not theoretical. A reply mailbox is an
1431 /// OPTIONAL endpoint (`routing::try_register_optional`): it is refused when the routing table is
1432 /// near full, and a bare-metal T630 refuses three times during `selfcheck`. A service refused one
1433 /// falls back to its shared endpoint and silently loses this repair - which is correct, because
1434 /// the alternative is eating client traffic, but it means "the desync is fixed" holds only where
1435 /// the mailbox was granted.
1436 ///
1437 /// Measured rather than assumed: an arm32 Pi 2 run logged ZERO refusals, so `fs` had its mailbox
1438 /// and the repair was live on that port. `spawn[ipc]: '<name>' gets no reply mailbox` in a log
1439 /// names each service in the other case (it read `routing: reply endpoint refused`, unnamed, until
1440 /// `backlog/74`).
1441 pub fn drain_stale_replies(&self) -> usize {
1442 let Some((recv, _)) = self.reply_mailbox() else { return 0 };
1443 let mut n = 0usize;
1444 // Bounded by the endpoint's own depth (§8.5): a queue that cannot hold more than 16 cannot
1445 // yield more than 16, so this terminates without needing to trust the peer.
1446 for _ in 0..16 {
1447 match crate::ipc::try_recv(recv) {
1448 Ok(Some(_)) => { self.release_discarded_cap(); n += 1 }
1449 _ => break,
1450 }
1451 }
1452 n
1453 }
1454
1455 /// [`Self::drain_stale_replies`] at the top of a request, and SAID when it found anything - the one
1456 /// thing every request path that waits on the reply mailbox must do before it issues.
1457 ///
1458 /// It was done by ONE of the three such paths. `request_with_reply_deadline_into` drained (it is
1459 /// where `backlog/fixed/37` was fixed); `request_with_reply_call_err`, which `gs::call` and so every `gs::fs`
1460 /// call goes through, and `request_with_reply_deadline_outcome_into` did not. So the exact failure
1461 /// [`Self::drain_stale_replies`] describes was still live for every service on the standard library:
1462 /// in the jobs suite the shell's history save timed out while a background `drives check` held `fs`
1463 /// for five seconds, its reply arrived late, and from then on every shell request to `fs` received
1464 /// its predecessor's reply and rejected it as malformed - `churn verify` failing as "storage
1465 /// unavailable" and every later history save lost (2026-10-11). One helper now, so a path cannot
1466 /// have the drain without the log or the log without the drain.
1467 fn drain_before_request(&self, peer: &str) {
1468 let dropped = self.drain_stale_replies();
1469 if dropped > 0 {
1470 // LOUD, because it means a previous request was abandoned and this one would have
1471 // inherited its reply. Silent self-repair is how the fs desync stayed invisible for so
1472 // long (§26.7 - a recovery that hides what it recovered from is a silent fallback).
1473 self.log_fmt(format_args!(
1474 "sdk: discarded {} abandoned repl(y/ies) before a request to `{}` - an earlier request timed out",
1475 dropped, peer));
1476 }
1477 }
1478
1479 /// Give back a capability a DISCARDED message may have carried. A received `Message` does not
1480 /// record its caps; the kernel queues them for `take_pending_cap`. Every caller claims a cap
1481 /// straight after its own receive, so what is pending while a drain runs came with the message it
1482 /// is discarding - and was leaked one table slot at a time until 2026-10-10 (`backlog/fixed/80` S5).
1483 fn release_discarded_cap(&self) {
1484 if let Some(c) = self.take_pending_cap() {
1485 self.remove_cap(c);
1486 }
1487 }
1488
1489 /// Wait, bounded by `max_ms`, for up to `owed` replies to arrive in the reply MAILBOX, and discard
1490 /// them. Returns how many were discarded, or `None` when this service has no mailbox - in which case
1491 /// owed-reply accounting cannot work (replies land on the shared endpoint) and the caller must fall
1492 /// back. For a synchronous caller that gave up on `owed` requests whose answers are still in flight:
1493 /// `drain_stale_replies` clears what has ARRIVED; this also waits for what has not.
1494 pub fn drain_owed_replies(&self, owed: usize, max_ms: u64) -> Option<usize> {
1495 let (recv, _) = self.reply_mailbox()?;
1496 let t0 = self.read_tsc();
1497 let limit = self.duration_cycles(max_ms);
1498 let mut n = 0usize;
1499 while n < owed {
1500 match crate::ipc::try_recv(recv) {
1501 Ok(Some(_)) => { self.release_discarded_cap(); n += 1 }
1502 _ => {
1503 if self.read_tsc().wrapping_sub(t0) >= limit {
1504 break;
1505 }
1506 self.sleep_ms(10);
1507 }
1508 }
1509 }
1510 Some(n)
1511 }
1512
1513 /// Return the recv cap handle for direct-handle use (e.g. wrong-right test probing).
1514 pub fn recv_handle(&self) -> Option<crate::capability::CapHandle> {
1515 let slot = Self::ctx().recv_slot;
1516 if slot == u32::MAX { None } else { Some(crate::capability::CapHandle(slot)) }
1517 }
1518
1519 // (A stray doc block for `request_with_reply` sat here, attached by rustdoc to `trace_as` below. It
1520 // is a plain comment now; the function's own doc is at `request_with_reply`.)
1521 //
1522 // `request_with_reply` embeds a per-request reply cap - a `SEND|GRANT` copy of this service's own
1523 // endpoint cap - so the server can reply via `take_pending_cap()` + `send_by_handle()` (§8). Its
1524 // wait is a synchronous kernel CALL (syscall 41): if `peer` dies after receiving the request but
1525 // before replying, the kernel wakes the caller with `ReplyDead` (§8.6) instead of hanging it. It
1526 // returns `None` for that and for a failed send alike, which is exactly the distinction a caller
1527 // needs: `ReplyDead` means the request was DELIVERED and may have been acted on, so re-sending is
1528 // safe only for an operation that is safe to repeat. A service uses `gs::call::request_within`,
1529 // which keeps the two apart (`Error::PeerDied`, never re-sent, against `Error::Unreachable`).
1530 #[inline]
1531 /// Declare this service's own name for the trace ring, once, at startup.
1532 ///
1533 /// A service cannot ask what it is called - identity is not ambient - so a traced service says.
1534 /// Costs nothing in trust: the whole event is already the emitter's testimony (see `crate::trace`).
1535 /// A service that never calls this still traces; its events read `?` in the caller column, which
1536 /// is the honest answer rather than a guess.
1537 pub fn trace_as(&self, name: &str) {
1538 // A TRUNCATED SERVICE NAME IS REPORTED, not swallowed. `PEER_LEN` is 12 - sized for
1539 // `block-driver` - and `hw-enumerator` is 13, so it appears everywhere as `hw-enumerato`.
1540 // Readable, but it is still a name silently becoming a different name, and two services
1541 // sharing a 12-byte prefix would merge into ONE metric row with their counters interleaving
1542 // (the exact failure an unnamed service caused before every service declared itself).
1543 //
1544 // Widening `PEER_LEN` was the other option and was rejected on cost: the trace DUMP is bounded
1545 // by one 4 KiB message, so a 16-byte field drops it from 110 events per screen to 95. Trading
1546 // real scrollback in the tool for four characters of a name is the wrong way round. Say it
1547 // once instead, so the fix - a shorter name - is the developer's to make.
1548 if name.len() > crate::trace::PEER_LEN {
1549 self.log_fmt(format_args!(
1550 "sdk: service name '{}' exceeds {} bytes and is TRUNCATED in traces and metrics - shorten it, or two services could share one row",
1551 name, crate::trace::PEER_LEN));
1552 }
1553 crate::trace::set_caller(name);
1554 }
1555
1556 /// Declare that requests to `peer` carry their opcode at byte `at` (default 0).
1557 ///
1558 /// For protocols that prepend a correlation tag - `shell -> fs` and `fs -> block-driver` both do -
1559 /// byte 0 is a request id, so a trace that recorded it was showing noise in a column labelled
1560 /// "op". The SDK is generic and cannot know; the service that owns the protocol can. See
1561 /// `crate::trace::set_op_offset`.
1562 pub fn trace_op_at(&self, peer: &str, at: u8) { crate::trace::set_op_offset(peer, at); }
1563
1564 /// Emit one trace event, or do nothing at all.
1565 ///
1566 /// The whole cost when this service is not tracing is the `trace::should_resolve` and
1567 /// `trace::sink_slot` loads and a not-taken branch. When it IS tracing, the send is `try_send` and its result is DISCARDED on purpose: an
1568 /// observer must never be able to slow, block, or fail the thing it observes. A full events queue
1569 /// loses one event, which the ring counts and `events status` reports - a visible loss, not a
1570 /// silent one (invariant 12).
1571 ///
1572 /// Recursion is cut at the source: `events` itself holds no `events` send cap, so the sink cannot trace
1573 /// its own sends, and no event can beget another.
1574 fn trace_emit(&self, peer: &str, op: u8, kind: u8) {
1575 // Arm lazily, ONCE. A service is handed a context with no init hook to run in, so there is
1576 // nowhere else to resolve from. A service whose contract does not grant `ipc_send =
1577 // ["events"]` records `u32::MAX` here and never looks again: one relaxed load per call for the
1578 // rest of its life, which is the cost the untraced majority pays.
1579 if crate::trace::should_resolve() {
1580 let mut slot = self.find_send_slot(crate::trace::SINK_NAME).unwrap_or(u32::MAX);
1581 if slot == u32::MAX {
1582 // NO CAP WIRED AT SPAWN - ASK THE KERNEL FOR ONE. `find_send_slot` only inspects caps
1583 // this service ALREADY HOLDS, so a service spawned during the milliseconds `events`
1584 // was restarting has nothing to find, resolves to MAX, and returns early on every
1585 // emission for the rest of its life. The reacquire that would fix it sits behind a
1586 // send FAILURE - and a send that never happens never fails, so it was unreachable.
1587 //
1588 // Measured on the T630: after a storm that restarted the sink ~48 times, `fs` and
1589 // `block-driver` published NOTHING for the remaining 9.4 s of the run, while the
1590 // sink's own LOCALLY-written rows were all present. That split - local rows yes, every
1591 // IPC-delivered row no - is the signature of a publisher whose message never left.
1592 //
1593 // `AcquireSendCap` is authorised by the DECLARATION, which the kernel keeps now even
1594 // when the peer was down at spawn. This is the call that spends it (14.3).
1595 if self.reacquire_by_name(crate::trace::SINK_NAME) {
1596 slot = self.find_send_slot(crate::trace::SINK_NAME).unwrap_or(u32::MAX);
1597 }
1598 }
1599 crate::trace::set_sink_slot(slot);
1600 // AND SAY IT, IF IT TOOK MORE THAN ONE TRY. This report was written once already and
1601 // silently lost: the edit that added it failed to apply and only its trace.rs half landed,
1602 // so `take_late_resolve` sat with no caller and the hardware run reported "0 late
1603 // resolves" - a zero that measured nothing. A missing instrument reads exactly like a
1604 // healthy system, which is the worst way for one to fail.
1605 if slot != u32::MAX {
1606 if let Some(missed) = crate::trace::take_late_resolve() {
1607 if missed > 1 {
1608 self.log_fmt(format_args!(
1609 "sdk: events sink resolved LATE - {} emission(s) went nowhere first",
1610 missed - 1));
1611 }
1612 }
1613 }
1614 }
1615 let slot = crate::trace::sink_slot();
1616 if slot == u32::MAX {
1617 return;
1618 }
1619 // Do NOT record a call to the SINK ITSELF. The `trace` utility reads the ring by asking
1620 // `events` for it, so tracing that call would fill the ring with the reader's own questions -
1621 // each dump adding two more events, pushing out the traffic the reader came to see. An
1622 // instrument that mostly measures itself is not measuring anything. This hides nothing: every
1623 // OTHER peer is still recorded, and `events status` still counts every event accepted.
1624 if peer == crate::trace::SINK_NAME {
1625 return;
1626 }
1627 // NO CLOCK READ HERE. `epoch_secs_monotonic` is a full CMOS RTC read: `wait_update_clear`
1628 // spins until the update-in-progress flag clears (up to ~1 ms), then seven port-I/O reads,
1629 // repeated until two agree. Two of those per request/reply turned `dir`, `move`, `find` and tab
1630 // completion into TIMEOUTS, and cost the shell so much time inside the kernel that it stopped
1631 // draining the console and lost Enter keystrokes. Measured, not guessed: `osdev test files`
1632 // was 222/0 before, 213/9 with the clock read, 222/0 again without it.
1633 //
1634 // The SINK stamps the event instead. `events` has to wake to receive it either way, so the
1635 // cost lands on the service whose job this is rather than on every service that talks to
1636 // anyone - and the caller already used `try_send` and did not wait. An observer must not be
1637 // able to slow the thing it observes, and a millisecond of port I/O per IPC is exactly that.
1638 let ev = crate::trace::encode(peer, op, kind);
1639 if self.try_send_by_handle(CapHandle(slot), &crate::ipc::Message::from_bytes(&ev)).is_err() {
1640 // THE SINK RESTARTED - REACQUIRE IT. The slot is resolved once and cached, so a `events`
1641 // respawn left every emitter holding a stale generation FOREVER: tracing silently stopped,
1642 // `events ipc` could not reach the ring, and each emission logged a kernel gen-mismatch.
1643 // Seen on hardware after a chaos storm restarted `events` forty times - cap generation 985
1644 // against a live record of 1025.
1645 //
1646 // This is the ordinary reacquire-by-name recovery every client owes a restartable peer
1647 // (14.3); the trace path just never did it. Only on FAILURE, so the healthy path is
1648 // unchanged, and if the sink is genuinely down the next emission tries again.
1649 //
1650 // AND RETRY ON THE FRESH SLOT. Reacquiring but dropping the event that prompted it is a
1651 // recovery that discards its own result (26.7) - the peer is reachable again and the
1652 // sample is thrown away anyway. 14.3 says reacquire AND retry; this used to do half.
1653 if self.reacquire_by_name(crate::trace::SINK_NAME) {
1654 if let Some(fresh) = self.find_send_slot(crate::trace::SINK_NAME) {
1655 crate::trace::set_sink_slot(fresh);
1656 let _ = self.try_send_by_handle(
1657 CapHandle(fresh),
1658 &crate::ipc::Message::from_bytes(&ev),
1659 );
1660 }
1661 }
1662 }
1663 }
1664
1665 /// Publish a metric sample to `events`. Fire-and-forget, and never able to slow this service down.
1666 ///
1667 /// The value is a SET, not an increment: this service owns the counter (it is this service's own
1668 /// state, §3.8) and publishes what it currently reads; `events` remembers the last sample. So a
1669 /// service that dies leaves its final value behind rather than taking it along, which is the one
1670 /// useful thing an observer can still learn from a service that is gone.
1671 ///
1672 /// Shaped exactly like `trace_emit`, and the shape is the point:
1673 /// - `try_send`, result DISCARDED. An observer must never slow, block or fail the thing it
1674 /// observes. A full sink loses one sample, which the sink counts and reports (invariant 12).
1675 /// - No clock read here. The sink stamps it; a CMOS RTC read on every publish once cost the shell
1676 /// its keystrokes.
1677 /// - Reacquire on failure only, so the healthy path is unchanged and a restarted sink does not
1678 /// leave every emitter holding a stale generation forever (14.3).
1679 ///
1680 /// SELF-OBSERVATION NEEDS NO CHECK HERE, and that is deliberate. `events` holds no send cap to
1681 /// `events`, so this resolves to `u32::MAX` inside its own address space and returns - the same cut
1682 /// that stops the sink tracing its own sends. A service reporting on itself must never take an IPC
1683 /// hop, because the hop is itself a reportable event and the ring would fill with itself. `events`
1684 /// counts its own work locally instead (`docs/observability.md` 9).
1685 pub fn metric(&self, name: &str, value: u64) {
1686 // FUNCTION-LOCAL, so the latch is owned by the only code that reads it (§3.8, invariant 9).
1687 // A one-shot: it never resets, because the point is to say it once for the life of the service.
1688 static TRUNCATED_SAID: core::sync::atomic::AtomicBool =
1689 core::sync::atomic::AtomicBool::new(false);
1690 if crate::trace::should_resolve() {
1691 let mut slot = self.find_send_slot(crate::trace::SINK_NAME).unwrap_or(u32::MAX);
1692 if slot == u32::MAX {
1693 // NO CAP WIRED AT SPAWN - ASK THE KERNEL FOR ONE. `find_send_slot` only inspects caps
1694 // this service ALREADY HOLDS, so a service spawned during the milliseconds `events`
1695 // was restarting has nothing to find, resolves to MAX, and returns early on every
1696 // emission for the rest of its life. The reacquire that would fix it sits behind a
1697 // send FAILURE - and a send that never happens never fails, so it was unreachable.
1698 //
1699 // Measured on the T630: after a storm that restarted the sink ~48 times, `fs` and
1700 // `block-driver` published NOTHING for the remaining 9.4 s of the run, while the
1701 // sink's own LOCALLY-written rows were all present. That split - local rows yes, every
1702 // IPC-delivered row no - is the signature of a publisher whose message never left.
1703 //
1704 // `AcquireSendCap` is authorised by the DECLARATION, which the kernel keeps now even
1705 // when the peer was down at spawn. This is the call that spends it (14.3).
1706 if self.reacquire_by_name(crate::trace::SINK_NAME) {
1707 slot = self.find_send_slot(crate::trace::SINK_NAME).unwrap_or(u32::MAX);
1708 }
1709 }
1710 crate::trace::set_sink_slot(slot);
1711 // AND SAY IT, IF IT TOOK MORE THAN ONE TRY. This report was written once already and
1712 // silently lost: the edit that added it failed to apply and only its trace.rs half landed,
1713 // so `take_late_resolve` sat with no caller and the hardware run reported "0 late
1714 // resolves" - a zero that measured nothing. A missing instrument reads exactly like a
1715 // healthy system, which is the worst way for one to fail.
1716 if slot != u32::MAX {
1717 if let Some(missed) = crate::trace::take_late_resolve() {
1718 if missed > 1 {
1719 self.log_fmt(format_args!(
1720 "sdk: events sink resolved LATE - {} emission(s) went nowhere first",
1721 missed - 1));
1722 }
1723 }
1724 }
1725 }
1726 let slot = crate::trace::sink_slot();
1727 if slot == u32::MAX {
1728 return;
1729 }
1730 // A TRUNCATED NAME IS A DIFFERENT NAME. Said once per service, not per sample: repeating it
1731 // would flood the channel the warning has to be read on, and one line names the offender well
1732 // enough to fix it. Silence here would mean two metrics quietly merging into one row under a
1733 // shared prefix, with the numbers interleaving and nothing to say why (invariant 12).
1734 if name.len() > crate::trace::MET_NAME_LEN
1735 && !TRUNCATED_SAID.swap(true, core::sync::atomic::Ordering::Relaxed)
1736 {
1737 self.log_fmt(format_args!(
1738 "sdk: metric name '{}' is longer than {} bytes and was TRUNCATED - shorten it, or two metrics will share one row",
1739 name, crate::trace::MET_NAME_LEN));
1740 }
1741 let m = crate::trace::encode_metric(name, value);
1742 if self
1743 .try_send_by_handle(CapHandle(slot), &crate::ipc::Message::from_bytes(&m))
1744 .is_err()
1745 {
1746 // AND RETRY, for the reason given on the trace path above. It matters MORE here: a
1747 // publisher samples on an INTERVAL (`fs` every 32 requests), so a dropped sample is not
1748 // retried for another whole interval - and after a sink restart that meant TWO intervals
1749 // with no row for that service at all. A service with no row is indistinguishable from a
1750 // dead one, which is the single question this metric exists to answer.
1751 // THE FAILING BRANCH IS NAMED, NOT COLLAPSED TO A BOOL. This used to be
1752 // `if reacquire_by_name(..)`, so a reacquire that FAILED did nothing and said nothing -
1753 // the sample was dropped and the service simply stopped publishing, with no symptom but a
1754 // row that never appeared. The three causes are not alike and the difference is the whole
1755 // diagnosis: NameNotRegistered is transient (the sink is mid-restart, try again next
1756 // interval), while CapTableFull is TERMINAL until this service restarts - it will never
1757 // publish again, and that deserves to be said out loud once rather than discovered as an
1758 // absence. Invariant 12, and 26.7's "a recovery that itself fails is still a failure".
1759 // ONE LATCH PER SEVERITY, and the wording matches the severity. A single latch was
1760 // wrong twice over: it said "metrics stop here" for the TRANSIENT case (the sink is
1761 // simply mid-restart - the next interval reconnects, and a kill-storm produces this by
1762 // the dozen), and having latched on that it would then stay silent about a genuinely
1763 // TERMINAL cause arriving later. Observed on the T630: 17 of these during a 100-round
1764 // storm, every one of them transient and every one of them overstating the damage.
1765 static ACQ_TRANSIENT_SAID: core::sync::atomic::AtomicBool =
1766 core::sync::atomic::AtomicBool::new(false);
1767 static ACQ_TERMINAL_SAID: core::sync::atomic::AtomicBool =
1768 core::sync::atomic::AtomicBool::new(false);
1769 let acq = self.reacquire_cap_detail(crate::trace::SINK_NAME);
1770 if let Err(why) = acq {
1771 let transient = matches!(why, AcquireFailure::NameNotRegistered);
1772 let said = if transient { &ACQ_TRANSIENT_SAID } else { &ACQ_TERMINAL_SAID };
1773 if !said.swap(true, core::sync::atomic::Ordering::Relaxed) {
1774 if transient {
1775 self.log("sdk: events sink not registered right now - samples dropped until it is back");
1776 } else {
1777 self.log_fmt(format_args!(
1778 "sdk: cannot reacquire the events sink ({}) - metrics from this service stop here",
1779 why.reason()));
1780 }
1781 }
1782 }
1783 if acq.is_ok() {
1784 if let Some(fresh) = self.find_send_slot(crate::trace::SINK_NAME) {
1785 crate::trace::set_sink_slot(fresh);
1786 // A RETRY THAT STILL FAILS IS ITS OWN OUTCOME (26.7): the reacquire worked, so
1787 // this is not "the sink is down" - it is a fresh cap that still would not take the
1788 // sample. Said once per service; silence here would look identical to success.
1789 static RETRY_FAILED_SAID: core::sync::atomic::AtomicBool =
1790 core::sync::atomic::AtomicBool::new(false);
1791 if self
1792 .try_send_by_handle(CapHandle(fresh), &crate::ipc::Message::from_bytes(&m))
1793 .is_err()
1794 && !RETRY_FAILED_SAID.swap(true, core::sync::atomic::Ordering::Relaxed)
1795 {
1796 self.log("sdk: events sink reacquired but the retried sample still failed");
1797 }
1798 }
1799 }
1800 }
1801 }
1802
1803 // ---------------------------------------------------------------------------
1804 // The request/reply family: each wraps its `_inner` implementation with one
1805 // trace event on the way in and one on the way out.
1806 // ---------------------------------------------------------------------------
1807 //
1808 // THESE ARE WRAPPERS AND NOT EDITS TO EACH BODY, deliberately. There are a dozen of these now (it
1809 // was eight), most an independent implementation with several early returns inside a wait loop;
1810 // instrumenting the bodies means finding every exit in each function and getting all of them right, forever. A
1811 // wrapper cannot miss one - the value returned IS the outcome.
1812 //
1813 // Instrumenting them ALL is also deliberate. The first cut traced only `request_with_reply`, and
1814 // on hardware that produced an EMPTY ring while the shell was busily talking to `fs` - because the
1815 // shell uses `_abortable` and `_deadline`, not the plain one. Partial instrumentation is worse
1816 // than none: `events ipc` then shows SOME traffic and silently omits the rest, with nothing on
1817 // screen saying which. That is a silent gap in the instrument, which is the exact failure this
1818 // ring exists to prevent (26.4, invariant 12).
1819 //
1820 // `op` is byte 0 of OUR OWN message. A service is entitled to interpret its own protocol; the
1821 // kernel is not, and that asymmetry is the whole reason this lives here rather than at the
1822 // routing point (4.4, 26.10).
1823
1824 // `#[inline]` on every wrapper below is LOAD-BEARING, not decoration. `Message` is 4 KiB by
1825 // value, so an un-inlined wrapper adds a whole extra payload move to each request - and the shell
1826 // paths that do many round trips (tab completion listing a directory, `move`, `find` descending)
1827 // went from passing to timing out on it. Measured, not assumed: `osdev test files` was 222/0
1828 // before the wrappers, 213/9 after, and 213/9 again with tracing DISABLED - so the cost was the
1829 // restructuring, never the emission.
1830 #[inline]
1831 fn trace_out(&self, peer: &str, op: u8, kind: u8) { self.trace_emit(peer, op, kind); }
1832
1833 /// The op byte of an outgoing request. NO EVENT IS EMITTED HERE.
1834 ///
1835 /// One event per exchange, recording how it ENDED, not two recording that it started and how it
1836 /// ended. Two doubled the traffic through the sink's single 16-deep endpoint, and the sink drains
1837 /// on its own core's schedule - so the flood crowded out the READER's own request and `trace
1838 /// status` reported a service that was alive and busy as "unavailable". Every exchange still
1839 /// produces exactly one event carrying its fate (REPLY / TIMEOUT / PEER_LOST / QUEUE_FULL /
1840 /// ABORTED), so nothing is lost but the duplicate.
1841 ///
1842 /// A request still IN FLIGHT is therefore not in the ring - and that is the one question the ring
1843 /// was never the right instrument for: `events blocked` reads it from the kernel, live, which is
1844 /// what a hang needs (`utilities/46_trace.md` mechanism A).
1845 #[inline]
1846 fn trace_in(&self, peer: &str, msg: &crate::ipc::Message) -> u8 {
1847 // The opcode's byte is the PEER'S protocol's business, not a fixed 0 - see `trace_op_at`.
1848 let at = crate::trace::op_offset(peer);
1849 msg.payload_bytes().get(at).copied().unwrap_or(0)
1850 }
1851
1852 /// Synchronous request/reply on the caller's own endpoint (kernel `Call`, syscall 41). Blocks until
1853 /// the reply arrives, or until the kernel wakes it with `ReplyDead` because the replier died (8.6).
1854 /// It has NO deadline: a replier that stays alive and never answers holds the caller forever.
1855 #[inline]
1856 pub fn request_with_reply(&self, peer: &str, msg: &crate::ipc::Message)
1857 -> Option<crate::ipc::Message>
1858 {
1859 let op = self.trace_in(peer, msg);
1860 let out = self.request_with_reply_inner(peer, msg);
1861 // No deadline on this variant, so `None` is always a lost peer, never a timeout.
1862 self.trace_out(peer, op, if out.is_some() { crate::trace::KIND_REPLY }
1863 else { crate::trace::KIND_PEER_LOST });
1864 out
1865 }
1866
1867 /// `request_with_reply_call`, saying WHY it failed instead of collapsing everything to `None`.
1868 #[inline]
1869 pub fn request_with_reply_call_err(&self, peer: &str, msg: &crate::ipc::Message, max_secs: i64)
1870 -> Result<Option<crate::ipc::Message>, crate::ipc::IpcError>
1871 {
1872 let op = self.trace_in(peer, msg);
1873 let out = self.request_with_reply_call_err_inner(peer, msg, max_secs);
1874 // A full queue is a busy peer, not a lost one - the ring said PEER_LOST for both until
1875 // 2026-10-10, so `gs::trace` showed `gs::call`'s `Busy` as a lost peer (`backlog/fixed/80` S2).
1876 self.trace_out(peer, op, match &out {
1877 Ok(Some(_)) => crate::trace::KIND_REPLY,
1878 Ok(None) => crate::trace::KIND_TIMEOUT,
1879 Err(crate::ipc::IpcError::QueueFull) => crate::trace::KIND_QUEUE_FULL,
1880 Err(_) => crate::trace::KIND_PEER_LOST,
1881 });
1882 out
1883 }
1884
1885 /// Bounded request/reply that decodes straight into the caller buffer.
1886 ///
1887 /// This variant collapses "the deadline passed" and "the send never left" into one `None`, so the
1888 /// ring records both as TIMEOUT. Where that difference matters the caller should be using
1889 /// `_call_err` or `_deadline_outcome`, which keep it - and so does the ring.
1890 #[inline]
1891 pub fn request_with_reply_deadline_into(
1892 &self, peer: &str, req: &[u8], buf: &mut [u8], max_secs: i64,
1893 ) -> Option<usize> {
1894 let op = req.get(crate::trace::op_offset(peer)).copied().unwrap_or(0);
1895 let out = self.request_with_reply_deadline_into_inner(peer, req, buf, max_secs);
1896 self.trace_out(peer, op, if out.is_some() { crate::trace::KIND_REPLY }
1897 else { crate::trace::KIND_TIMEOUT });
1898 out
1899 }
1900
1901 /// Bounded request/reply. `None` is "no answer within the deadline" (see `_into` on the collapse).
1902 #[inline]
1903 pub fn request_with_reply_deadline(
1904 &self, peer: &str, msg: &crate::ipc::Message, max_secs: i64,
1905 ) -> Option<crate::ipc::Message> {
1906 let op = self.trace_in(peer, msg);
1907 let out = self.request_with_reply_deadline_inner(peer, msg, max_secs);
1908 self.trace_out(peer, op, if out.is_some() { crate::trace::KIND_REPLY }
1909 else { crate::trace::KIND_TIMEOUT });
1910 out
1911 }
1912
1913 /// `request_with_reply_deadline` with a millisecond budget.
1914 #[inline]
1915 pub fn request_with_reply_ms(
1916 &self, peer: &str, msg: &crate::ipc::Message, max_ms: u64,
1917 ) -> Option<crate::ipc::Message> {
1918 let op = self.trace_in(peer, msg);
1919 let out = self.request_with_reply_ms_inner(peer, msg, max_ms);
1920 self.trace_out(peer, op, if out.is_some() { crate::trace::KIND_REPLY }
1921 else { crate::trace::KIND_TIMEOUT });
1922 out
1923 }
1924
1925 /// Bounded request/reply that distinguishes every way it can fail.
1926 #[inline]
1927 pub fn request_with_reply_deadline_outcome(
1928 &self, peer: &str, msg: &crate::ipc::Message, max_secs: i64,
1929 ) -> DeadlineOutcome {
1930 let op = self.trace_in(peer, msg);
1931 let out = self.request_with_reply_deadline_outcome_inner(peer, msg, max_secs);
1932 self.trace_out(peer, op, match &out {
1933 DeadlineOutcome::Reply(_) => crate::trace::KIND_REPLY,
1934 DeadlineOutcome::SendFailed => crate::trace::KIND_PEER_LOST,
1935 DeadlineOutcome::QueueFull => crate::trace::KIND_QUEUE_FULL,
1936 DeadlineOutcome::Timeout => crate::trace::KIND_TIMEOUT,
1937 });
1938 out
1939 }
1940
1941 /// Bounded request/reply that SIFTS what arrives instead of believing the first thing that lands.
1942 ///
1943 /// Every other variant on this page returns whatever message turns up next, which is correct for a
1944 /// caller whose endpoint carries nothing but its own replies and WRONG for a caller that also
1945 /// SERVES on it. `net-stack` is the second kind: it receives client requests and `nic-driver`
1946 /// replies in one mailbox, so a client that spoke while it was waiting for the driver was consumed
1947 /// as the driver's answer, misparsed, and never served - the failure `docs/net-tags-design.md`
1948 /// opens with, and the reason that service still cannot do any background work.
1949 ///
1950 /// `mine` is asked about each message as it arrives, BEFORE anything else is done with it, and
1951 /// answers one question: is this the reply I sent for? Say `true` and the wait ends with it. Say
1952 /// `false` and the wait continues - the message is the caller's to deal with, and the caller is
1953 /// the only thing that can, because only it knows whether that means stashing a client request or
1954 /// discarding a reply that arrived too late. This is deliberately not an enum of dispositions: the
1955 /// SDK would then be deciding what to keep, which is policy, and the thing being kept usually has
1956 /// a reply capability that must be reclaimed or answered rather than merely stored (26.10, 8.5).
1957 ///
1958 /// The kernel's per-task pending cap and receive badge are both readable from inside `mine`
1959 /// (`take_pending_cap`, `last_recv_badge`), which is the point of calling it at the moment of
1960 /// arrival rather than afterwards: they describe THE MESSAGE JUST RECEIVED and are overwritten by
1961 /// the next one. A discriminator built on the pending cap is what `net-stack` uses, and it is free
1962 /// - a client request carries a reply cap, a driver reply does not.
1963 ///
1964 /// Everything else matches [`Self::request_with_reply_deadline_outcome`] exactly, including the
1965 /// reply-cap reclaim on a failed send (a delivered request's cap is the peer's - backlog/67) and the warning attached to `Timeout`: the request was
1966 /// SENT, so a late reply is still coming and re-sending desyncs the protocol.
1967 #[inline]
1968 pub fn request_with_reply_deadline_sifted(
1969 &self, peer: &str, msg: &crate::ipc::Message, max_secs: i64,
1970 mine: impl FnMut(&crate::ipc::Message) -> bool,
1971 ) -> DeadlineOutcome {
1972 let op = self.trace_in(peer, msg);
1973 let out = self.request_with_reply_deadline_sifted_inner(peer, msg, max_secs, mine);
1974 self.trace_out(peer, op, match &out {
1975 DeadlineOutcome::Reply(_) => crate::trace::KIND_REPLY,
1976 DeadlineOutcome::SendFailed => crate::trace::KIND_PEER_LOST,
1977 DeadlineOutcome::QueueFull => crate::trace::KIND_QUEUE_FULL,
1978 DeadlineOutcome::Timeout => crate::trace::KIND_TIMEOUT,
1979 });
1980 out
1981 }
1982
1983 /// [`Self::request_with_reply_deadline_sifted`], bounded in MILLISECONDS.
1984 ///
1985 /// The same reason the unsifted `_ms` form exists: a sub-second window cannot be built out of a
1986 /// whole-second bound, and the drain inside `net-stack`'s ~900 ms ping window is exactly such a
1987 /// window. Bounded by the CYCLE counter for the same reason too - it is the only clock here with
1988 /// sub-second resolution, and it is the one the caller's own budget is measured in.
1989 #[inline]
1990 pub fn request_with_reply_ms_sifted(
1991 &self, peer: &str, msg: &crate::ipc::Message, max_ms: u64,
1992 mut mine: impl FnMut(&crate::ipc::Message) -> bool,
1993 ) -> Option<crate::ipc::Message> {
1994 let op = self.trace_in(peer, msg);
1995 let target = CapHandle(self.find_send_slot(peer)?);
1996 let self_grant = self.self_grant_handle()?;
1997 let reply_cap = self.derive_cap(self_grant)?;
1998 if self.offer_request(target, reply_cap, msg).is_err() {
1999 self.remove_cap(reply_cap);
2000 self.trace_out(peer, op, crate::trace::KIND_PEER_LOST);
2001 return None;
2002 }
2003 let t0 = self.read_tsc();
2004 let budget = self.duration_cycles(max_ms);
2005 loop {
2006 if let Some(r) = self.await_slice(Self::AWAIT_SLICE_MS.min(max_ms.max(1))) {
2007 if mine(&r) {
2008 self.trace_out(peer, op, crate::trace::KIND_REPLY);
2009 return Some(r);
2010 }
2011 }
2012 // wrapping_sub, so a counter that wraps mid-wait reads as a small elapsed rather than as
2013 // an enormous one that expires the deadline instantly.
2014 if self.read_tsc().wrapping_sub(t0) >= budget {
2015 // NOT RECLAIMED: the send succeeded, so the kernel moved the reply cap to the peer and emptied
2016 // this slot - and the first cap this task RECEIVED since went into it. Removing "the reply
2017 // cap" here destroyed that one: a client's reply cap stashed mid-wait, so its answer went
2018 // into a dead cap (backlog/67). The peer removes the cap after answering.
2019 self.trace_out(peer, op, crate::trace::KIND_TIMEOUT);
2020 return None;
2021 }
2022 }
2023 }
2024
2025 /// Bounded request/reply the user can abandon with `q`.
2026 #[inline]
2027 pub fn request_with_reply_abortable(
2028 &self, peer: &str, msg: &crate::ipc::Message, max_secs: i64,
2029 ) -> ReqOutcome {
2030 let op = self.trace_in(peer, msg);
2031 let out = self.request_with_reply_abortable_inner(peer, msg, max_secs);
2032 self.trace_out(peer, op, match &out {
2033 ReqOutcome::Reply(_) => crate::trace::KIND_REPLY,
2034 ReqOutcome::Aborted => crate::trace::KIND_ABORTED,
2035 ReqOutcome::Timeout => crate::trace::KIND_TIMEOUT,
2036 });
2037 out
2038 }
2039
2040 /// `request_with_reply_abortable`, plus a callback when the wait starts to linger.
2041 #[inline]
2042 pub fn request_with_reply_qhint(
2043 &self, peer: &str, msg: &crate::ipc::Message, hint_after_secs: i64, max_secs: i64,
2044 on_linger: impl FnOnce(),
2045 ) -> ReqOutcome {
2046 self.request_with_reply_keyhint(peer, msg, hint_after_secs, max_secs, Self::QUIT_KEYS, on_linger)
2047 }
2048
2049 /// [`Self::request_with_reply_qhint`] that HANDS BACK every key it reads that is not a quit key.
2050 ///
2051 /// A wait that watches the console for `q` has to read what is typed, and a key read is a key
2052 /// taken: the plain form discards the rest, so whatever the operator typed ahead while it waited
2053 /// was lost (`backlog/fixed/80` H16). This passes each one to `keep`, in order, for the caller to put
2054 /// where its own input reader will find it first.
2055 #[inline]
2056 pub fn request_with_reply_qhint_keeping(
2057 &self, peer: &str, msg: &crate::ipc::Message, hint_after_secs: i64, max_secs: i64,
2058 on_linger: impl FnOnce(), keep: &dyn Fn(u8),
2059 ) -> ReqOutcome {
2060 let op = self.trace_in(peer, msg);
2061 let out = self.request_with_reply_qhint_inner(
2062 peer, msg, hint_after_secs, max_secs, Self::QUIT_KEYS, on_linger, |_| true, Some(keep));
2063 self.trace_out(peer, op, match &out {
2064 ReqOutcome::Reply(_) => crate::trace::KIND_REPLY,
2065 ReqOutcome::Aborted => crate::trace::KIND_ABORTED,
2066 ReqOutcome::Timeout => crate::trace::KIND_TIMEOUT,
2067 });
2068 out
2069 }
2070
2071 /// The keys [`Self::request_with_reply_qhint`] leaves on: `q`, `Q` and Escape.
2072 pub const QUIT_KEYS: &'static [u8] = &[b'q', b'Q', 0x1b];
2073
2074 /// [`Self::request_with_reply_qhint`] with the LEAVE KEYS chosen by the caller. The convention
2075 /// (`utilities/0_conventions.md`) gives `q` to a task that STOPS when the operator leaves and `b` to
2076 /// one that keeps running; a request whose peer finishes the work whether or not this caller waits -
2077 /// `wifi radio off hard`, where the driver cuts the chip's power either way - is the second kind, and
2078 /// offering `q` for it would promise a stop that cannot happen. `Aborted` means one of `leave_keys`
2079 /// was pressed; what that means for the task is the caller's to say.
2080 pub fn request_with_reply_keyhint(
2081 &self, peer: &str, msg: &crate::ipc::Message, hint_after_secs: i64, max_secs: i64,
2082 leave_keys: &[u8], on_linger: impl FnOnce(),
2083 ) -> ReqOutcome {
2084 let op = self.trace_in(peer, msg);
2085 let out = self.request_with_reply_qhint_inner(peer, msg, hint_after_secs, max_secs, leave_keys, on_linger, |_| true, None);
2086 self.trace_out(peer, op, match &out {
2087 ReqOutcome::Reply(_) => crate::trace::KIND_REPLY,
2088 ReqOutcome::Aborted => crate::trace::KIND_ABORTED,
2089 ReqOutcome::Timeout => crate::trace::KIND_TIMEOUT,
2090 });
2091 out
2092 }
2093
2094 /// [`Self::request_with_reply_keyhint`] that SIFTS what arrives, as
2095 /// [`Self::request_with_reply_deadline_sifted`] does for the unabortable wait. `mine` is asked about
2096 /// each message as it lands; `true` ends the wait with it, `false` hands it to the caller - who must
2097 /// release any cap it carries - and the wait goes on, its deadline unchanged by how many arrive.
2098 /// Messages ALREADY queued when it is called are drained first without being asked (the stale-reply
2099 /// drain every abortable wait opens with), so a caller that counts late answers drains before calling.
2100 ///
2101 /// For a peer whose replies the caller can recognise (a correlation tag) on an endpoint that also
2102 /// takes other traffic: the unsifted form returns the first message whatever it is, so a late
2103 /// answer to a request this caller already gave up on is read as the answer to this one.
2104 pub fn request_with_reply_keyhint_sifted(
2105 &self, peer: &str, msg: &crate::ipc::Message, hint_after_secs: i64, max_secs: i64,
2106 leave_keys: &[u8], on_linger: impl FnOnce(),
2107 mine: impl FnMut(&crate::ipc::Message) -> bool,
2108 ) -> ReqOutcome {
2109 let op = self.trace_in(peer, msg);
2110 let out = self.request_with_reply_qhint_inner(peer, msg, hint_after_secs, max_secs, leave_keys, on_linger, mine, None);
2111 self.trace_out(peer, op, match &out {
2112 ReqOutcome::Reply(_) => crate::trace::KIND_REPLY,
2113 ReqOutcome::Aborted => crate::trace::KIND_ABORTED,
2114 ReqOutcome::Timeout => crate::trace::KIND_TIMEOUT,
2115 });
2116 out
2117 }
2118
2119 /// [`Self::request_with_reply_keyhint_sifted`] that HANDS BACK every key it reads that is not a leave
2120 /// key, in order, as [`Self::request_with_reply_qhint_keeping`] does for the unsifted request. For a
2121 /// caller whose waits must say `[q] quit` and must not lose what the operator types meanwhile.
2122 pub fn request_with_reply_keyhint_sifted_keeping(
2123 &self, peer: &str, msg: &crate::ipc::Message, hint_after_secs: i64, max_secs: i64,
2124 leave_keys: &[u8], on_linger: impl FnOnce(),
2125 mine: impl FnMut(&crate::ipc::Message) -> bool, keep: &dyn Fn(u8),
2126 ) -> ReqOutcome {
2127 let op = self.trace_in(peer, msg);
2128 let out = self.request_with_reply_qhint_inner(peer, msg, hint_after_secs, max_secs, leave_keys, on_linger, mine, Some(keep));
2129 self.trace_out(peer, op, match &out {
2130 ReqOutcome::Reply(_) => crate::trace::KIND_REPLY,
2131 ReqOutcome::Aborted => crate::trace::KIND_ABORTED,
2132 ReqOutcome::Timeout => crate::trace::KIND_TIMEOUT,
2133 });
2134 out
2135 }
2136
2137
2138
2139 fn request_with_reply_inner(
2140 &self,
2141 peer: &str,
2142 msg: &crate::ipc::Message,
2143 ) -> Option<crate::ipc::Message> {
2144 let target = CapHandle(self.find_send_slot(peer)?);
2145 let self_grant = self.self_grant_handle()?;
2146 let reply_cap = self.derive_cap(self_grant)?;
2147 let recv = self.recv_handle()?;
2148 match crate::ipc::call(target, reply_cap, recv, msg) {
2149 Ok(reply) => Some(reply),
2150 Err(_) => {
2151 // Send failed (dead endpoint) or the peer died before replying (ReplyDead): the
2152 // embedded reply cap may not have been transferred, so reclaim it. Safe when the kernel
2153 // DID move it out: a Call receives nothing but its own reply, so no other cap can have
2154 // been inserted into the emptied slot, and removing an empty slot does nothing. (The
2155 // send-then-recv paths cannot say that, and do not reclaim - backlog/67.) Without this, a
2156 // storm of failed calls would leak reply caps until the table fills and every request
2157 // returns None.
2158 self.remove_cap(reply_cap);
2159 None
2160 }
2161 }
2162 }
2163
2164 #[inline(never)]
2165 /// A mark of how deep the stack currently is, in bytes below the first mark taken.
2166 ///
2167 /// THE INSTRUMENT THAT WAS MISSING. Five attempts were made to fit correlation tags into `fs`
2168 /// without ever measuring the frame they had to fit in; each removed one 4 KiB item, the overflow
2169 /// returned at a different pc, and QEMU - which has no disk, so `fs` never mounts and never makes
2170 /// the deep calls - reported every build clean. A change to stack usage that cannot be measured
2171 /// can only be guessed at, and five guesses is the evidence.
2172 ///
2173 /// Safe Rust and no kernel involvement: the address of a local IS the stack pointer, near enough.
2174 /// Relative to a mark taken at service start, so it needs no knowledge of where the stack lives.
2175 pub fn stack_mark(&self) -> usize {
2176 let probe = 0u8;
2177 &probe as *const u8 as usize
2178 }
2179
2180 /// Bytes of stack used at this point, without needing a reference mark.
2181 ///
2182 /// A mark taken at service start only works if the report is taken at the DEEPEST point, and the
2183 /// first attempt took it after `mount` had returned - so it measured a popped frame and printed
2184 /// zero. Absolute is harder to misuse: it can be called from anywhere and still means the same
2185 /// thing.
2186 ///
2187 /// The stack is one region, 256 KiB, and top-aligned, so rounding the current pointer UP to the
2188 /// next 256 KiB boundary lands on its top and the difference is what has been used. That derives
2189 /// the top from the alignment the kernel already guarantees rather than restating the address here,
2190 /// which would be a second copy of a layout decision (Commandment III).
2191 pub fn stack_used(&self) -> usize {
2192 const REGION: usize = 256 * 1024;
2193 let sp = self.stack_mark();
2194 let top = (sp + (REGION - 1)) & !(REGION - 1);
2195 top - sp
2196 }
2197
2198 /// A bounded request/reply through the KERNEL's `CallDeadline`, returning the reply as a message.
2199 ///
2200 /// The same primitive `request_with_reply_deadline_into` already uses, in the shape the other
2201 /// request helpers have, so a caller can move to it without restructuring its buffers.
2202 ///
2203 /// Use this rather than `request_with_reply_deadline` / `_deadline_outcome` when the caller SERVES
2204 /// clients on the same endpoint it awaits replies on. Those helpers wait with a plain timed `recv`,
2205 /// which takes whatever is next - so under client load the peer's reply can be crowded out of a 16-deep queue and the
2206 /// caller reports a timeout that blames the peer for something it did correctly. `net-stack` hit
2207 /// exactly that: the wire showed 7 DHCP REQUESTs sent and 7 ACKs returned while the log said
2208 /// "6 of 6 REQUESTs never left the host - the driver refused them".
2209 ///
2210 /// `CallDeadline` does not have that failure: `call_dequeue` matches the reply BY ITS SENDER and
2211 /// leaves every other message queued, which is the entire reason §8.2 added it - hand-rolling the
2212 /// bounded wait out of send + recv is what lost messages. No new kernel surface: syscall 50 and
2213 /// its semantics are existing, ratified law. `max_secs <= 0` is a one-second
2214 /// bound, never none: the kernel reads a 0 as NO deadline, and a bounded call turning into an
2215 /// unbounded one by a unit slip is the hang it exists to prevent (`backlog/fixed/80` S5).
2216 pub fn request_with_reply_call(&self, peer: &str, msg: &crate::ipc::Message, max_secs: i64)
2217 -> Option<crate::ipc::Message>
2218 {
2219 self.request_with_reply_call_err(peer, msg, max_secs).ok().flatten()
2220 }
2221
2222 /// `request_with_reply_call`, but saying WHY it failed instead of collapsing everything to None.
2223 ///
2224 /// Exists because "no answer" is not a diagnosis. A caller that turns every failure into one word
2225 /// then reports it as the PEER's fault - `net-stack` blamed the driver for refusing frames the wire
2226 /// showed it had sent - and there is no way to tell a send that never left from a reply that never
2227 /// came. `Ok(None)` is the deadline passing; `Err(e)` is the send failing, and `e` says how.
2228 fn request_with_reply_call_err_inner(&self, peer: &str, msg: &crate::ipc::Message, max_secs: i64)
2229 -> Result<Option<crate::ipc::Message>, crate::ipc::IpcError>
2230 {
2231 let target = match self.find_send_slot(peer) {
2232 Some(s) => CapHandle(s),
2233 None => return Err(crate::ipc::IpcError::CapError(crate::capability::CapError::CapNotHeld)),
2234 };
2235 // Abandoned replies out first, or this request takes one of them (`drain_before_request`).
2236 self.drain_before_request(peer);
2237 let (recv, grant) = match self.reply_mailbox() {
2238 Some((r, g)) => (r, g),
2239 None => match (self.recv_handle(), self.self_grant_handle()) {
2240 (Some(r), Some(g)) => (r, g),
2241 _ => return Err(crate::ipc::IpcError::CapError(crate::capability::CapError::CapNotHeld)),
2242 },
2243 };
2244 let reply_cap = match self.derive_cap(grant) {
2245 Some(c) => c,
2246 None => return Err(crate::ipc::IpcError::CapError(crate::capability::CapError::CapNotHeld)),
2247 };
2248 let secs = deadline_secs(max_secs);
2249 let mut buf = [0u8; crate::ipc::MAX_PAYLOAD];
2250 let n = msg.payload_bytes().len().min(buf.len());
2251 buf[..n].copy_from_slice(&msg.payload_bytes()[..n]);
2252 match crate::ipc::call_deadline_into(target, reply_cap, recv, &msg.payload_bytes()[..n],
2253 &mut buf, secs) {
2254 Ok(Some(len)) => Ok(Some(crate::ipc::Message::from_bytes(&buf[..len]))),
2255 // Delivered, deadline passed: the reply cap is the peer's now (backlog/67).
2256 Ok(None) => Ok(None),
2257 Err(e) => { self.remove_cap(reply_cap); Err(e) }
2258 }
2259 }
2260
2261 /// Bounded request/reply into a caller buffer, reporting WHICH failure happened.
2262 ///
2263 /// Same work as [`Self::request_with_reply_deadline_into`], same stack shape - the caller supplies
2264 /// the buffer so no reply-sized frame appears here - but it does not throw the reason away. See
2265 /// [`DeadlineOutcomeInto`] for why the distinction is not a nicety: the two common failures want
2266 /// opposite responses, and a caller given `None` has to guess wrong in one direction or the other.
2267 pub fn request_with_reply_deadline_outcome_into(
2268 &self, peer: &str, req: &[u8], buf: &mut [u8], max_secs: i64,
2269 ) -> DeadlineOutcomeInto {
2270 let op = req.get(crate::trace::op_offset(peer)).copied().unwrap_or(0);
2271 let out = self.request_with_reply_deadline_outcome_into_inner(peer, req, buf, max_secs);
2272 self.trace_out(peer, op, match &out {
2273 DeadlineOutcomeInto::Reply(_) => crate::trace::KIND_REPLY,
2274 DeadlineOutcomeInto::SendFailed => crate::trace::KIND_PEER_LOST,
2275 DeadlineOutcomeInto::PeerDied => crate::trace::KIND_PEER_LOST,
2276 DeadlineOutcomeInto::QueueFull => crate::trace::KIND_QUEUE_FULL,
2277 DeadlineOutcomeInto::Timeout => crate::trace::KIND_TIMEOUT,
2278 });
2279 out
2280 }
2281
2282 fn request_with_reply_deadline_outcome_into_inner(
2283 &self, peer: &str, req: &[u8], buf: &mut [u8], max_secs: i64,
2284 ) -> DeadlineOutcomeInto {
2285 // Each of these is a DISTINCT reason the request never left, and every one of them used to be
2286 // an anonymous `None`: no such peer wired or resolvable, no endpoint of our own to be replied
2287 // to, and - the one that made this permanent rather than transient - NO FREE CAP SLOT to derive
2288 // a reply cap into. A service whose table has filled reports "peer did not answer", which sends
2289 // the reader looking at the peer.
2290 let target = match self.find_send_slot(peer) {
2291 Some(s) => CapHandle(s),
2292 None => return DeadlineOutcomeInto::SendFailed,
2293 };
2294 // Abandoned replies out first, or this request takes one of them (`drain_before_request`).
2295 self.drain_before_request(peer);
2296 let (recv, grant) = match self.reply_mailbox() {
2297 Some((r, g)) => (r, g),
2298 None => match (self.recv_handle(), self.self_grant_handle()) {
2299 (Some(r), Some(g)) => (r, g),
2300 _ => return DeadlineOutcomeInto::SendFailed,
2301 },
2302 };
2303 let reply_cap = match self.derive_cap(grant) {
2304 Some(c) => c,
2305 None => return DeadlineOutcomeInto::SendFailed,
2306 };
2307 let secs = deadline_secs(max_secs);
2308 let out = crate::ipc::call_deadline_into(target, reply_cap, recv, req, buf, secs);
2309 // The kernel consumes the reply cap on a delivered call - and `Ok(None)` is a delivered call
2310 // whose deadline passed, so it is the peer's then too (backlog/67). On a failed send it is ours
2311 // to reclaim, or the slot leaks one per failed request (§8.5, the three checks).
2312 match out {
2313 Ok(Some(n)) => DeadlineOutcomeInto::Reply(n),
2314 Ok(None) => DeadlineOutcomeInto::Timeout,
2315 Err(crate::ipc::IpcError::QueueFull) => {
2316 self.remove_cap(reply_cap);
2317 DeadlineOutcomeInto::QueueFull
2318 }
2319 // Delivered: the reply cap went with the request, as on `Ok(None)` - nothing to reclaim.
2320 Err(crate::ipc::IpcError::ReplyDead) => DeadlineOutcomeInto::PeerDied,
2321 Err(_) => { self.remove_cap(reply_cap); DeadlineOutcomeInto::SendFailed }
2322 }
2323 }
2324
2325 /// Send `req` to `peer` and receive the reply into `buf`, bounded by `max_secs`; returns the
2326 /// reply's length. `max_secs <= 0` is a one-second bound, never an unbounded wait (see
2327 /// `request_with_reply_call`).
2328 ///
2329 /// The by-value path costs 4 KiB for the request `Message`, 4 KiB for the reply `Message`, and
2330 /// 4 KiB again at every wrapper that forwards it. This costs none of that: the caller owns both
2331 /// buffers, and only a length is returned. Drains abandoned replies from the reply mailbox first
2332 /// (`drain_stale_replies`), and reclaims the reply cap only when the send failed (§8.5, backlog/67).
2333 ///
2334 /// **This used to be send + `recv_timeout_into`, and that was wrong in a way that cost days.**
2335 /// `recv_timeout_into` takes whatever is next on the endpoint, and a service that SERVES clients on
2336 /// the same endpoint it awaits replies on would receive an unrelated client request, fail to match
2337 /// it, and drop it - losing that request outright while its own reply arrived later as an orphan
2338 /// that desynced the next exchange. `fs` does exactly that, which is why its clients hung and its
2339 /// block protocol "lost step" on both boards.
2340 ///
2341 /// It now uses `CallDeadline`, which dequeues the REPLY specifically (the kernel's `call_dequeue`
2342 /// takes only a message stamped as sent by the target's owner) and leaves everything else queued.
2343 /// The correct primitive, bounded.
2344 fn request_with_reply_deadline_into_inner(
2345 &self, peer: &str, req: &[u8], buf: &mut [u8], max_secs: i64,
2346 ) -> Option<usize> {
2347 let target = CapHandle(self.find_send_slot(peer)?);
2348 // DRAIN ABANDONED REPLIES BEFORE ISSUING, FOR EVERY CALLER.
2349 //
2350 // `drain_stale_replies` documents this failure precisely and had exactly one caller - `fs` -
2351 // so every OTHER service was one timeout away from being permanently broken. The shell was:
2352 // a single scroll request that timed out left its reply queued, and from that moment every
2353 // request waited out its full deadline and failed, for the rest of the boot. Observed on a
2354 // Dell Wyse as `scrollback: the console did not answer` once a second, indefinitely, with the
2355 // console sitting idle at `BlockRecv, queue 0` the whole time (`backlog/fixed/37`).
2356 //
2357 // **It is safe here for the reason that function already gives**, and that reason is exactly
2358 // why this belongs in the SDK rather than at each call site: `request_with_reply*` enforces
2359 // ONE OUTSTANDING REQUEST AT A TIME, so at the instant a new request is issued nothing
2360 // legitimate can be waiting in the reply mailbox. Anything present is a reply this service
2361 // stopped waiting for. Leaving it to each caller to remember means a caller that forgets is
2362 // not slightly worse - it is permanently broken after its first timeout, silently.
2363 //
2364 // Cheap: a non-blocking `try_recv` that finds nothing in the healthy case, bounded by the
2365 // endpoint depth.
2366 self.drain_before_request(peer);
2367 // Reply mailbox when the task has one, shared endpoint when it does not. The reply cap and the
2368 // endpoint waited on must name the SAME endpoint, or the kernel's reply-matched dequeue waits
2369 // for something that will never be delivered there.
2370 let (recv, grant) = match self.reply_mailbox() {
2371 Some((r, g)) => (r, g),
2372 None => (self.recv_handle()?, self.self_grant_handle()?),
2373 };
2374 let reply_cap = self.derive_cap(grant)?;
2375 let secs = deadline_secs(max_secs);
2376 let out = crate::ipc::call_deadline_into(target, reply_cap, recv, req, buf, secs);
2377 // The kernel consumes the reply cap on a delivered call - and `Ok(None)` is a delivered call
2378 // whose deadline passed, so it is the peer's then too (backlog/67). On a failed send it is ours
2379 // to reclaim, or the slot leaks one per failed request (§8.5, the three checks).
2380 match out {
2381 Ok(Some(n)) => Some(n),
2382 Ok(None) => None,
2383 Err(_) => { self.remove_cap(reply_cap); None }
2384 }
2385 }
2386
2387 /// How long one blocking wait slice lasts.
2388 ///
2389 /// 20 ms: two scheduler quanta, so a waiter never holds its core, and short enough that a caller
2390 /// checking for a keypress between slices still feels instant to a human. Nothing depends on the
2391 /// value for correctness - a message arriving mid-slice wakes the task immediately; the slice only
2392 /// bounds how often a caller gets to look at anything OTHER than its endpoint.
2393 const AWAIT_SLICE_MS: u64 = 20;
2394 /// The slice a noticing wait blocks for before its notice is due, when it is not reading the
2395 /// console and has only the reply and the clock to watch.
2396 const PRE_NOTICE_SLICE_MS: u64 = 250;
2397
2398 /// Wait for a message on our own endpoint, BLOCKING, for at most one slice.
2399 ///
2400 /// The whole point of this helper, and the reason it exists rather than a `try_recv` loop: a task
2401 /// that spins while waiting is not idle, it is COMPETING with whatever it is waiting for. On this
2402 /// system that is not an abstract cost - `net-stack` waiting for a DHCP reply spun through the SDK
2403 /// and through its own `drain_scan`, two nested loops, and saturated `nic-driver` and the USB
2404 /// driver that had to fetch the very reply it was waiting for. The waiting starved the answering.
2405 ///
2406 /// Pinning the services to different cores hid it and fixed nothing: on a single-core machine the
2407 /// spinner simply consumes the whole system. A blocked task consumes nothing and is woken the
2408 /// instant a message lands (`RecvTimeout`, §12), so this is correct at any core count - which is
2409 /// the only kind of correct worth having.
2410 ///
2411 /// Sliced rather than one long block so callers that must also watch something else - a keypress,
2412 /// an abort flag - get the chance, without any of them going back to spinning.
2413 ///
2414 /// (2026-10-09) The `#[inline(always)]` the next paragraphs describe is NOT on this function any
2415 /// more: 8f32a5e9 removed it, because inlining a body that holds a 4 KiB local into a caller that
2416 /// already holds one puts both in the same frame. The cost they describe is real either way.
2417 ///
2418 /// `#[inline(always)]`, and that attribute is load-bearing rather than an optimisation hint. A
2419 /// `Message` is 4096 bytes BY VALUE, so a wrapper that returns one is not free: it is a whole
2420 /// extra 4 KiB stack frame on every request in every service. Introducing this helper cost `fs`
2421 /// exactly that, and `fs` was already the service closest to its 256 KiB limit - it began taking a
2422 /// data abort at `mount`, with SP below the bottom of its stack region.
2423 ///
2424 /// It only showed on hardware. QEMU's raspi2b has no disk, so `fs` never mounts and never makes
2425 /// the deep block calls, and thirty-second QEMU boots kept coming back clean while the board
2426 /// restart-looped. A one-line wrapper around a large return value has to be inlined or it must not
2427 /// exist.
2428 fn await_slice(&self, ms: u64) -> Option<crate::ipc::Message> {
2429 self.recv_timeout(self.duration_cycles(ms))
2430 }
2431
2432 /// Offer a request to `target`, riding out a FULL peer queue instead of calling it a failure.
2433 ///
2434 /// A full queue and an unreachable peer are different things, and reporting them the same way is
2435 /// what desynced the shell against `fs` on x86. The caller's answer to "send failed" is to
2436 /// reacquire the peer by name and RE-SEND with a fresh tag - correct when a restart made the cap
2437 /// stale, and actively harmful when the queue was merely full: the original request is still in
2438 /// flight, so its reply arrives as an orphan and every request afterwards collects the PREVIOUS
2439 /// one's reply. The log showed exactly that, permanently one behind:
2440 ///
2441 /// shell: discarded an fs reply for tag 68 while awaiting 69
2442 /// shell: discarded an fs reply for tag 72 while awaiting 73
2443 ///
2444 /// Congestion clears as soon as the peer is scheduled, so the fix is to wait a moment and offer
2445 /// the SAME request again - never a second one. Bounded, and a queue still full after that is
2446 /// reported honestly to the caller.
2447 fn offer_request(
2448 &self, target: CapHandle, reply_cap: CapHandle, msg: &crate::ipc::Message,
2449 ) -> Result<(), crate::ipc::IpcError> {
2450 const BUSY_MS: u64 = 2;
2451 const BUSY_TRIES: u32 = 8;
2452 let mut last = crate::ipc::IpcError::QueueFull;
2453 for _ in 0..BUSY_TRIES {
2454 match self.send_with_cap_by_handle(target, reply_cap, msg) {
2455 Ok(()) => return Ok(()),
2456 Err(crate::ipc::IpcError::QueueFull) => {
2457 last = crate::ipc::IpcError::QueueFull;
2458 self.sleep(self.duration_cycles(BUSY_MS));
2459 }
2460 Err(e) => return Err(e),
2461 }
2462 }
2463 Err(last)
2464 }
2465
2466 /// Like `request_with_reply`, but the wait for the reply is **bounded** by `max_secs` of
2467 /// **wall-clock** time (`epoch_secs_monotonic`, InspectKernel query 17). Returns `None` on timeout,
2468 /// and on a send that never left - so a peer that receives the request and then stays silent cannot
2469 /// hold the caller forever (`request_with_reply`, which has no deadline, would). Use it when the
2470 /// peer may be unstable - e.g. writing a report to `fs` right after a chaos storm hammered `fs` +
2471 /// its `block-driver`.
2472 ///
2473 /// Uses the RTC (not a TSC-cycle deadline) deliberately: a cycle bound is not portable - under
2474 /// QEMU's TCG the guest TSC races ahead and expires the deadline before the reply arrives, while
2475 /// the RTC is real wall-clock on both TCG and hardware. Waits in blocking slices (`await_slice`),
2476 /// so it does not spin, and its resolution is one second.
2477 /// **Costs one message-sized stack frame, not two.**
2478 ///
2479 /// This used to be a thin wrapper over `request_with_reply_deadline_outcome`, and that made it
2480 /// unusable on a tight stack: `DeadlineOutcome` CARRIES a `Message`, so the enum is a second
2481 /// 4 KiB temporary on top of the `Option<Message>` returned. `fs` already sits near its 256 KiB
2482 /// user stack, and switching its block RPC to this wrapper produced an instant data abort at
2483 /// startup and an 826-deep restart loop on real hardware - the machine unusable, from a change
2484 /// whose whole purpose was to make a hang impossible.
2485 ///
2486 /// So the wait is written out here instead of borrowed. It is the same loop, minus the enum: a
2487 /// caller that does not need to distinguish "the send never left" from "the peer was silent"
2488 /// should not pay a message-sized frame for the distinction. Callers that DO need it still have
2489 /// `..._outcome`, and should keep an eye on their own stack.
2490 ///
2491 /// **Shares the flaw `request_with_reply_deadline_into` was fixed for, and is NOT fixed here.**
2492 /// It sends with a reply cap and then receives generically, so a caller that also SERVES on its
2493 /// endpoint can consume an unrelated message and drop it. `_into` moved to `CallDeadline`, which
2494 /// dequeues the reply specifically; this one has not, because nothing has yet been shown to be
2495 /// harmed by it and a blind sweep of four helpers is how a fix becomes a regression.
2496 ///
2497 /// Note that `_abortable` and `_qhint` CANNOT simply follow: they interleave on purpose, to
2498 /// notice a `q` keypress while waiting. Making them dequeue only the reply would delete that. If
2499 /// they need this too, the answer is a bounded stash, not a substitution.
2500 fn request_with_reply_deadline_inner(
2501 &self,
2502 peer: &str,
2503 msg: &crate::ipc::Message,
2504 max_secs: i64,
2505 ) -> Option<crate::ipc::Message> {
2506 let target = CapHandle(self.find_send_slot(peer)?);
2507 let self_grant = self.self_grant_handle()?;
2508 let reply_cap = self.derive_cap(self_grant)?;
2509 let recv = self.recv_handle()?;
2510 if self.offer_request(target, reply_cap, msg).is_err() {
2511 // The send never left, so the embedded cap was not transferred - reclaim it or a storm of
2512 // failures leaks the table (§8.5).
2513 self.remove_cap(reply_cap);
2514 return None;
2515 }
2516 let t0 = self.epoch_secs_monotonic();
2517 loop {
2518 // BLOCK, do not spin. See `await_slice`.
2519 if let Some(r) = self.await_slice(Self::AWAIT_SLICE_MS) {
2520 return Some(r);
2521 }
2522 let now = self.epoch_secs_monotonic();
2523 // Guard the clock going BACKWARDS as well as forwards: on hardware whose counter is
2524 // unreliable, `now - t0` can read huge and expire the deadline on the first pass.
2525 if now >= t0 && now - t0 >= max_secs {
2526 // Abandoned: the reply may still arrive later and sit in our queue, which is the
2527 // hazard `..._outcome` documents. The caller reports the failure.
2528 // NOT RECLAIMED: the send succeeded, so the kernel moved the reply cap to the peer and emptied
2529 // this slot - and the first cap this task RECEIVED since went into it. Removing "the reply
2530 // cap" here destroyed that one: a client's reply cap stashed mid-wait, so its answer went
2531 // into a dead cap (backlog/67). The peer removes the cap after answering.
2532 let _ = recv;
2533 return None;
2534 }
2535 }
2536 }
2537
2538 /// `request_with_reply_deadline`, bounded in MILLISECONDS instead of whole seconds.
2539 ///
2540 /// Exists because a sub-second window cannot be built out of a whole-second bound, and one was
2541 /// being attempted. `net-stack`'s ping window is ~900 ms and cycle-based, but the drain inside it
2542 /// was bounded by `LINK_SECS = 1` on `epoch_secs_monotonic`, whose resolution IS one second - so a
2543 /// single drain could legitimately outlast the entire window. Measured on a Pi 4: the window
2544 /// reported `closed after 1017663 us [budget 900000 us]` with `0 drains`, meaning it completed
2545 /// exactly ONE call and that call overran the whole budget. Ping sends once a second, so a window
2546 /// that runs 1.018 s misses roughly every third reply - which read as "the network is flaky" and
2547 /// was arithmetic.
2548 ///
2549 /// Bounded by the CYCLE counter, deliberately, because that is the only clock here with sub-second
2550 /// resolution and it is the same clock the caller's own window uses - a bound and the budget it
2551 /// must fit inside should not be measured by different clocks. `duration_cycles` already floors an
2552 /// uncalibrated counter to one quantum, so a board with no calibration degrades to "return almost
2553 /// at once" rather than to "wait forever".
2554 ///
2555 /// No new kernel surface: this is the same userspace poll the seconds variants use, with a
2556 /// different clock read in the deadline test.
2557 fn request_with_reply_ms_inner(
2558 &self,
2559 peer: &str,
2560 msg: &crate::ipc::Message,
2561 max_ms: u64,
2562 ) -> Option<crate::ipc::Message> {
2563 let target = CapHandle(self.find_send_slot(peer)?);
2564 let self_grant = self.self_grant_handle()?;
2565 let reply_cap = self.derive_cap(self_grant)?;
2566 let recv = self.recv_handle()?;
2567 if self.offer_request(target, reply_cap, msg).is_err() {
2568 self.remove_cap(reply_cap);
2569 return None;
2570 }
2571 let t0 = self.read_tsc();
2572 let budget = self.duration_cycles(max_ms);
2573 loop {
2574 if let Some(r) = self.await_slice(Self::AWAIT_SLICE_MS.min(max_ms.max(1))) {
2575 return Some(r);
2576 }
2577 // wrapping_sub, so a counter that wraps mid-wait reads as a small elapsed rather than as
2578 // an enormous one that expires the deadline instantly.
2579 if self.read_tsc().wrapping_sub(t0) >= budget {
2580 // NOT RECLAIMED: the send succeeded, so the kernel moved the reply cap to the peer and emptied
2581 // this slot - and the first cap this task RECEIVED since went into it. Removing "the reply
2582 // cap" here destroyed that one: a client's reply cap stashed mid-wait, so its answer went
2583 // into a dead cap (backlog/67). The peer removes the cap after answering.
2584 let _ = recv;
2585 return None;
2586 }
2587 }
2588 }
2589
2590 /// Like [`Self::request_with_reply_deadline`] but returns the DISTINCTION between a send that never
2591 /// left (stale/unresolvable peer cap) and a peer that received the request but stayed silent past
2592 /// the deadline (see [`DeadlineOutcome`]). Same bounded, RTC-deadline wait; use this when a caller
2593 /// must reacquire+retry a *restarted* peer (`SendFailed`) but must NOT double the wait on a merely
2594 /// *silent* one (`Timeout`).
2595 fn request_with_reply_deadline_outcome_inner(
2596 &self,
2597 peer: &str,
2598 msg: &crate::ipc::Message,
2599 max_secs: i64,
2600 ) -> DeadlineOutcome {
2601 let target = match self.find_send_slot(peer) { Some(s) => CapHandle(s), None => return DeadlineOutcome::SendFailed };
2602 let self_grant = match self.self_grant_handle() { Some(g) => g, None => return DeadlineOutcome::SendFailed };
2603 let reply_cap = match self.derive_cap(self_grant) { Some(c) => c, None => return DeadlineOutcome::SendFailed };
2604 if let Err(e) = self.send_with_cap_by_handle(target, reply_cap, msg) {
2605 self.remove_cap(reply_cap); // send failed: reclaim the untransferred reply cap (no leak)
2606 return match e {
2607 crate::ipc::IpcError::QueueFull => DeadlineOutcome::QueueFull,
2608 _ => DeadlineOutcome::SendFailed,
2609 };
2610 }
2611 // Deglitched monotonic clock, not the raw RTC: a single CMOS misread (the "4383d" glitch on the
2612 // T630) would otherwise make `now - t0` read huge and expire the deadline instantly.
2613 let t0 = self.epoch_secs_monotonic();
2614 loop {
2615 // Block, do not spin - and now it actually does. This comment said so while the line
2616 // beneath it polled, which is how the claim survived so long unexamined.
2617 if let Some(r) = self.await_slice(Self::AWAIT_SLICE_MS) { return DeadlineOutcome::Reply(r); }
2618 if self.epoch_secs_monotonic() - t0 >= max_secs {
2619 // NOT RECLAIMED: the send succeeded, so the kernel moved the reply cap to the peer and emptied
2620 // this slot - and the first cap this task RECEIVED since went into it. Removing "the reply
2621 // cap" here destroyed that one: a client's reply cap stashed mid-wait, so its answer went
2622 // into a dead cap (backlog/67). The peer removes the cap after answering.
2623 // CALLER BEWARE: the request was already SENT, so the peer will reply into our endpoint
2624 // whether we are listening or not. Leaving the reply cap alone does not remove that message
2625 // from the queue - the NEXT `try_recv` on this endpoint may return the ABANDONED reply
2626 // instead of the answer it expects. That has bitten for real: a timed-out fs read left a
2627 // 1-byte `[FS_NOTFOUND]` behind, and the next command consumed it and reported a healthy
2628 // mounted disk as absent. `request_with_reply_abortable` avoids this with a DRAIN at its
2629 // own top; this variant cannot drain blindly, because a service that also SERVES on this
2630 // endpoint (net-stack) would discard live client requests. So a caller that can time out
2631 // must deal with the late reply itself - see the shell's `drain_stale_fs_replies`, which
2632 // discards leftovers BEFORE sending rather than chasing one afterwards (a race that
2633 // cannot be won: if the late reply has not arrived yet, the NEXT command eats it).
2634 return DeadlineOutcome::Timeout;
2635 }
2636 self.yield_cpu();
2637 }
2638 }
2639
2640 /// The sifting wait. A deliberate near-copy of `request_with_reply_deadline_outcome_inner`
2641 /// rather than a shared body that one of them passes a closure into.
2642 ///
2643 /// Folding the two together would put a generic parameter on the hot path of every service that
2644 /// makes a bounded request, for the benefit of the one that sifts. The SDK has been bitten by
2645 /// exactly that kind of tidying before: a one-line wrapper returning a 4 KiB `Message` by value
2646 /// cost `fs` a whole extra stack frame per request and took it over its stack limit on hardware
2647 /// while every QEMU run stayed green (see `await_slice`). The duplication is four lines of loop,
2648 /// it is visible, and both copies are right here.
2649 fn request_with_reply_deadline_sifted_inner(
2650 &self,
2651 peer: &str,
2652 msg: &crate::ipc::Message,
2653 max_secs: i64,
2654 mut mine: impl FnMut(&crate::ipc::Message) -> bool,
2655 ) -> DeadlineOutcome {
2656 let target = match self.find_send_slot(peer) { Some(s) => CapHandle(s), None => return DeadlineOutcome::SendFailed };
2657 let self_grant = match self.self_grant_handle() { Some(g) => g, None => return DeadlineOutcome::SendFailed };
2658 let reply_cap = match self.derive_cap(self_grant) { Some(c) => c, None => return DeadlineOutcome::SendFailed };
2659 if let Err(e) = self.send_with_cap_by_handle(target, reply_cap, msg) {
2660 self.remove_cap(reply_cap); // send failed: reclaim the untransferred reply cap (no leak)
2661 return match e {
2662 crate::ipc::IpcError::QueueFull => DeadlineOutcome::QueueFull,
2663 _ => DeadlineOutcome::SendFailed,
2664 };
2665 }
2666 let t0 = self.epoch_secs_monotonic();
2667 loop {
2668 if let Some(r) = self.await_slice(Self::AWAIT_SLICE_MS) {
2669 // Asked at the moment of arrival, so `take_pending_cap` and `last_recv_badge` inside
2670 // it still describe THIS message. A `false` hands ownership to the caller and the
2671 // wait goes on; the deadline below is unchanged by how many arrive, so a flood of
2672 // other traffic cannot extend it.
2673 if mine(&r) { return DeadlineOutcome::Reply(r); }
2674 }
2675 if self.epoch_secs_monotonic() - t0 >= max_secs {
2676 // NOT RECLAIMED: the send succeeded, so the kernel moved the reply cap to the peer and emptied
2677 // this slot - and the first cap this task RECEIVED since went into it. Removing "the reply
2678 // cap" here destroyed that one: a client's reply cap stashed mid-wait, so its answer went
2679 // into a dead cap (backlog/67). The peer removes the cap after answering.
2680 // Same caveat as the unsifted twin: the request WAS sent, so the peer answers whether
2681 // or not anyone is still listening, and that answer will arrive later. A caller that
2682 // times out must expect to meet it - here, `mine` will simply be asked about it and
2683 // can say no.
2684 return DeadlineOutcome::Timeout;
2685 }
2686 self.yield_cpu();
2687 }
2688 }
2689
2690 /// Like [`Self::request_with_reply_deadline`] but ABORTABLE: while waiting it also drains the console
2691 /// and, on q/Q/ESC, returns [`ReqOutcome::Aborted`] IMMEDIATELY - it does NOT wait for the in-flight
2692 /// reply (that wait felt like "it pauses instead of quitting"). The request is already sent, so the
2693 /// peer replies into our endpoint whether we listen or not; that late reply is cleared by the DRAIN at
2694 /// the top of the *next* abortable request, so it never pollutes a later command (the `net scan` ->
2695 /// `net` "0.0.0.0 / 00:00 MAC" bug that drain closes). Sends the request ONCE (no re-trigger). Use for
2696 /// any interactive command that blocks on a peer (the "q to quit" rule, `utilities/0_conventions.md`).
2697 /// A service with no console foreground never sees input, so this degrades to the plain deadline wait.
2698 fn request_with_reply_abortable_inner(
2699 &self,
2700 peer: &str,
2701 msg: &crate::ipc::Message,
2702 max_secs: i64,
2703 ) -> ReqOutcome {
2704 // Drain any stale reply a prior INSTANT-abort left in our endpoint (the peer replied after we
2705 // stopped listening), so this request cannot read it as its own. Safe for the shell - a client
2706 // whose endpoint only holds replies; between commands it is otherwise empty.
2707 while self.try_recv().is_some() {}
2708 let target = match self.find_send_slot(peer) { Some(s) => CapHandle(s), None => return ReqOutcome::Timeout };
2709 let self_grant = match self.self_grant_handle() { Some(g) => g, None => return ReqOutcome::Timeout };
2710 let reply_cap = match self.derive_cap(self_grant) { Some(c) => c, None => return ReqOutcome::Timeout };
2711 if self.offer_request(target, reply_cap, msg).is_err() {
2712 self.remove_cap(reply_cap);
2713 return ReqOutcome::Timeout;
2714 }
2715 let t0 = self.epoch_secs_monotonic();
2716 loop {
2717 // BLOCK for the reply, with a short timeout - do not spin. `try_recv` + `yield_cpu` kept this
2718 // task permanently RUNNABLE, so a core running a waiting command never reached the scheduler's
2719 // idle path at all. On ARM that path is where USB hot-plug is watched, so plugging or
2720 // unplugging anything during a `ping` was noticed only once the ping ended: `events` queued
2721 // up and all arrived at the prompt. It also pegged the core at 100% for a task that is, in
2722 // truth, doing nothing (the same busy-wait the `observe` and muted loops were already fixed
2723 // for - see the shell's MUTED_POLL_MS). Blocking parks the task, the core halts, idle work runs.
2724 if let Some(r) = self.await_slice(Self::AWAIT_SLICE_MS) {
2725 // DO NOT remove the reply cap on a REPLY. The send already removed it.
2726 //
2727 // §8.5: a cap embedded in a message "is transferred and REMOVED from sender's table".
2728 // The instant the request went out this slot stopped being ours, and by the time the
2729 // reply lands the kernel has REUSED it - `CapTable::insert` hands out the first EMPTY
2730 // slot, and this one is empty. When the reply carries an embedded cap (a file cap from
2731 // `fs`), that cap lands in exactly this slot, so removing "the reply cap" DELETES IT.
2732 //
2733 // Measured on the Pi 4: `fcap` printed `file=12 reply=12` - the file handle and the
2734 // next derived cap were the same slot, which is only possible if the slot was empty.
2735 // Every file-cap read/write then failed before reaching `fs`, and two negative
2736 // sub-checks "passed" vacuously because no invoke could get that far.
2737 //
2738 // A remove-by-stale-index can bite ANY request whose reply carries a cap, not just
2739 // fcap - and the abort and timeout paths below are no different: the send DID deliver
2740 // there too, so the slot is not ours either, and they leave it alone (backlog/67).
2741 return ReqOutcome::Reply(r);
2742 }
2743 while let Some(b) = self.try_console_read() {
2744 // q/Q/ESC aborts IMMEDIATELY - never wait for the in-flight reply (that wait was the
2745 // "it pauses instead of quitting" complaint). The peer's late reply lands in our endpoint
2746 // and the drain atop the NEXT abortable request clears it, so it pollutes nothing.
2747 // "Immediately" still holds: the abort does not wait on the peer. What the block adds is
2748 // up to one poll interval before the keypress is LOOKED at - tens of milliseconds, under
2749 // the threshold at which a person can tell, and the same trade the observe loop makes.
2750 if b == b'q' || b == b'Q' || b == 0x1b { return ReqOutcome::Aborted; }
2751 }
2752 if self.epoch_secs_monotonic() - t0 >= max_secs {
2753 return ReqOutcome::Timeout;
2754 }
2755 }
2756 }
2757
2758 /// Like [`Self::request_with_reply_abortable`], but if no reply has arrived after
2759 /// `hint_after_secs` it invokes `on_linger` ONCE (e.g. to print a "[q] quit" hint) and keeps
2760 /// waiting/aborting. A snappy reply never fires the hint, so a fast request stays silent and only
2761 /// a genuinely lingering wait tells the user they can bail. Abort semantics are identical to
2762 /// `request_with_reply_abortable` (q/Q/ESC -> `Aborted` immediately; the request is sent once and
2763 /// a late reply is drained atop the next abortable/qhint request). The hint text lives in the
2764 /// caller's closure, so this stays mechanism - the SDK provides the *timing*, the caller the UX.
2765 fn request_with_reply_qhint_inner(
2766 &self,
2767 peer: &str,
2768 msg: &crate::ipc::Message,
2769 hint_after_secs: i64,
2770 max_secs: i64,
2771 leave_keys: &[u8],
2772 on_linger: impl FnOnce(),
2773 mut mine: impl FnMut(&crate::ipc::Message) -> bool,
2774 keep: Option<&dyn Fn(u8)>,
2775 ) -> ReqOutcome {
2776 // Drain any stale reply a prior INSTANT-abort left in our endpoint (see the abortable variant).
2777 while self.try_recv().is_some() {}
2778 let target = match self.find_send_slot(peer) { Some(s) => CapHandle(s), None => return ReqOutcome::Timeout };
2779 let self_grant = match self.self_grant_handle() { Some(g) => g, None => return ReqOutcome::Timeout };
2780 let reply_cap = match self.derive_cap(self_grant) { Some(c) => c, None => return ReqOutcome::Timeout };
2781 if self.offer_request(target, reply_cap, msg).is_err() {
2782 self.remove_cap(reply_cap);
2783 return ReqOutcome::Timeout;
2784 }
2785 let t0 = self.epoch_secs_monotonic();
2786 let mut on_linger = Some(on_linger); // FnOnce, fired at most once when the wait lingers
2787 loop {
2788 // Block, do not spin - see `request_with_reply_abortable`. This is the variant `net`/`ping`
2789 // actually use (the "[q] quit" hint), so it is the one that kept core 0 permanently
2790 // busy during a continuous ping and starved the idle-path USB hot-plug watch.
2791 //
2792 // THE CONSOLE IS NOT TOUCHED UNTIL `[q] quit` HAS BEEN SHOWN. Before the notice the
2793 // operator has been offered nothing to press, so a key typed then is typing ahead, not an
2794 // answer: reading it would either lose it or - a `q` meant for the NEXT command, a pager
2795 // say - end this wait. And most requests are answered before the notice is due, so they
2796 // now block in the kernel for the whole wait instead of polling the console every slice
2797 // (`backlog/fixed/80` H16: both of those made the shell keep its noticing waits out of scripts
2798 // and pipelines).
2799 let noticed = on_linger.is_none();
2800 let slice = if noticed { Self::AWAIT_SLICE_MS } else { Self::PRE_NOTICE_SLICE_MS };
2801 if let Some(r) = self.await_slice(slice) {
2802 // DO NOT remove the reply cap on a REPLY. The send already removed it.
2803 //
2804 // §8.5: a cap embedded in a message "is transferred and REMOVED from sender's table".
2805 // The instant the request went out this slot stopped being ours, and by the time the
2806 // reply lands the kernel has REUSED it - `CapTable::insert` hands out the first EMPTY
2807 // slot, and this one is empty. When the reply carries an embedded cap (a file cap from
2808 // `fs`), that cap lands in exactly this slot, so removing "the reply cap" DELETES IT.
2809 //
2810 // Measured on the Pi 4: `fcap` printed `file=12 reply=12` - the file handle and the
2811 // next derived cap were the same slot, which is only possible if the slot was empty.
2812 // Every file-cap read/write then failed before reaching `fs`, and two negative
2813 // sub-checks "passed" vacuously because no invoke could get that far.
2814 //
2815 // A remove-by-stale-index can bite ANY request whose reply carries a cap, not just
2816 // fcap - and the abort and timeout paths below are no different: the send DID deliver
2817 // there too, so the slot is not ours either, and they leave it alone (backlog/67).
2818 //
2819 // Asked at the moment of arrival, so a cap the message carries is still pending for
2820 // `mine` to take; `false` hands the message to the caller and the wait goes on.
2821 if mine(&r) {
2822 return ReqOutcome::Reply(r);
2823 }
2824 }
2825 if noticed {
2826 while let Some(b) = self.try_console_read() {
2827 if leave_keys.contains(&b) { return ReqOutcome::Aborted; }
2828 // Not a leave key: the operator's typing, handed back when the caller can keep it.
2829 if let Some(k) = keep { k(b); }
2830 }
2831 }
2832 let elapsed = self.epoch_secs_monotonic() - t0;
2833 if elapsed >= hint_after_secs {
2834 if let Some(f) = on_linger.take() { f(); }
2835 }
2836 if elapsed >= max_secs {
2837 return ReqOutcome::Timeout;
2838 }
2839 }
2840 }
2841
2842 /// Wait for the next message on our own recv endpoint, ABORTABLE (q/Q/ESC) and bounded by an RTC
2843 /// deadline, WITHOUT sending anything. The failure-aware twin of [`Self::recv`]: where `recv`
2844 /// blocks forever (and panics on error), this returns [`ReqOutcome::Timeout`] if no message
2845 /// arrives within `max_secs` and [`ReqOutcome::Aborted`] the instant the user presses q. A receive
2846 /// ERROR is not reported: it reads as no message, so the wait runs to its deadline. Use it to
2847 /// await a reply we already sent by some path OTHER than a named-peer request - a badged
2848 /// `resource_invoke` (a file/socket capability), or draining a pipe filter's stream - where
2849 /// [`Self::request_with_reply_abortable`] (which does its own send) does not fit. This is the wait
2850 /// half of the abortable request, factored out: a peer that received our invocation but died before
2851 /// replying, or a filter that wedges mid-stream, can no longer hang us (Commandment VIII - wait on
2852 /// truth *including failure*). The caller owns any reply cap it derived and reclaims it only when the send failed (backlog/67), not on every
2853 /// outcome. A service with no console foreground never sees input, so this degrades to a plain
2854 /// deadline wait. Does NOT drain a stale reply first - a caller that can be re-entered after an
2855 /// abort should `while self.try_recv().is_some() {}` before it sends (as the request variants do).
2856 pub fn recv_abortable_deadline(&self, max_secs: i64) -> ReqOutcome {
2857 self.recv_abortable_deadline_keeping(max_secs, &|_| {})
2858 }
2859
2860 /// [`Self::recv_abortable_deadline`] that HANDS BACK every key it reads that is not a quit key, in
2861 /// order, as [`Self::request_with_reply_qhint_keeping`] does for a request. The plain form reads the
2862 /// console to see a `q` and discards everything else, so what the operator typed while a pipe stage
2863 /// ran was lost: `ask`'s answer takes seconds, and the next command typed during it vanished in QEMU
2864 /// (the receive-side half of `backlog/fixed/80` H16).
2865 pub fn recv_abortable_deadline_keeping(&self, max_secs: i64, keep: &dyn Fn(u8)) -> ReqOutcome {
2866 self.recv_abortable_deadline_noticing(max_secs, i64::MAX, &|| {}, keep)
2867 }
2868
2869 /// [`Self::recv_abortable_deadline_keeping`] that calls `on_linger` ONCE when no message has come
2870 /// after `hint_after_secs` - the receive-side twin of [`Self::request_with_reply_qhint`]. The SDK
2871 /// supplies the timing and the caller the words, as there: a task with no prompt has nothing to
2872 /// print, so this never writes to the console itself.
2873 pub fn recv_abortable_deadline_noticing(
2874 &self, max_secs: i64, hint_after_secs: i64, on_linger: &dyn Fn(), keep: &dyn Fn(u8),
2875 ) -> ReqOutcome {
2876 let t0 = self.epoch_secs_monotonic();
2877 let mut lingered = false;
2878 loop {
2879 // Block, do not spin - see `await_slice`.
2880 if let Some(r) = self.await_slice(Self::AWAIT_SLICE_MS) { return ReqOutcome::Reply(r); }
2881 while let Some(b) = self.try_console_read() {
2882 if b == b'q' || b == b'Q' || b == 0x1b { return ReqOutcome::Aborted; }
2883 keep(b);
2884 }
2885 let elapsed = self.epoch_secs_monotonic() - t0;
2886 if !lingered && elapsed >= hint_after_secs {
2887 lingered = true;
2888 on_linger();
2889 }
2890 if elapsed >= max_secs {
2891 return ReqOutcome::Timeout;
2892 }
2893 }
2894 }
2895
2896 /// Reacquire a fresh SEND cap to `peer` and point the named-peer cache at it, so subsequent
2897 /// `try_send(peer)` / `send(peer)` use the new cap. Returns `false` if `peer` cannot currently
2898 /// be resolved (e.g. it has not finished respawning) - the caller should retry on a later tick.
2899 ///
2900 /// A thin shim over `reacquire_cap` (syscall 10): name resolution is the **kernel name
2901 /// directory**, not a service. The directory is populated synchronously at each service's spawn,
2902 /// so there is no round-trip and no bootstrap chicken-and-egg (the directory lives in the kernel,
2903 /// always reachable). `reacquire_cap` also updates the send-cap cache.
2904 #[must_use = "the peer is NOT reacquired if this is false"]
2905 pub fn reacquire_by_name(&self, peer: &str) -> bool {
2906 self.reacquire_cap(peer).is_ok()
2907 }
2908
2909 /// Handle to this service's `SEND|GRANT` cap to its **own** endpoint, minted at
2910 /// spawn. A service hands a copy of its endpoint to a peer by deriving one
2911 /// (`derive_cap`) and granting it across - keeping this original so it can derive
2912 /// again later. `None` if the service has no endpoint.
2913 pub fn self_grant_handle(&self) -> Option<crate::capability::CapHandle> {
2914 let slot = Self::ctx().self_grant_slot;
2915 if slot == u32::MAX { None } else { Some(crate::capability::CapHandle(slot)) }
2916 }
2917
2918 /// The send cap for a named peer, or `None` if this service has no such peer.
2919 ///
2920 /// The public form of what `request_with_reply` resolves internally. Exposed for the case that
2921 /// helper cannot serve: a request whose reply must NOT be waited for. A single-threaded service that
2922 /// owns an in-memory answer (the `time` service and its wall clock) has to be able to send a
2923 /// best-effort request to a slow peer without becoming unable to answer its own clients meanwhile -
2924 /// so it pairs this with `derive_cap` + `send_with_cap_by_handle` and picks the reply up later, out
2925 /// of its ordinary receive loop, matched by the protocol's correlation tag.
2926 pub fn send_peer_handle(&self, peer: &str) -> Option<crate::capability::CapHandle> {
2927 self.find_send_slot(peer).map(crate::capability::CapHandle)
2928 }
2929
2930 /// Return the cap handle for the Nth send-peer entry (0-indexed).
2931 ///
2932 /// Used by property-test probes (P9) to access multiple cap slots wired to
2933 /// the same endpoint, verifying all are invalidated on endpoint death (§7.5).
2934 pub fn send_peer_at(&self, idx: usize) -> Option<crate::capability::CapHandle> {
2935 let data = Self::ctx();
2936 let count = (data.send_peer_count as usize).min(MAX_SEND_PEERS);
2937 if idx >= count { return None; }
2938 let slot = data.send_peers[idx].slot;
2939 if slot == u32::MAX { None } else { Some(crate::capability::CapHandle(slot)) }
2940 }
2941
2942 /// Send to a specific cap handle directly, bypassing peer-name lookup.
2943 ///
2944 /// Used by the probe service to test kernel cap enforcement (§22 Tests 3B, 9B).
2945 pub fn try_send_by_handle(
2946 &self,
2947 handle: crate::capability::CapHandle,
2948 msg: &crate::ipc::Message,
2949 ) -> Result<(), crate::ipc::IpcError> {
2950 crate::ipc::try_send(handle, msg)
2951 }
2952
2953 /// Send a message to a named peer WITH an embedded capability grant.
2954 ///
2955 /// The send-peer cap (which must carry the `GRANT` right) is transferred to
2956 /// the receiver. On success the calling service loses that cap (§7.6).
2957 /// Returns `CapNotGrantable` if the cap lacks `GRANT` - the cap is kept.
2958 pub fn send_with_cap(&self, peer: &str, msg: &crate::ipc::Message) -> Result<(), crate::ipc::IpcError> {
2959 let slot = self.find_send_slot(peer)
2960 .ok_or(crate::ipc::IpcError::CapError(crate::capability::CapError::CapNotHeld))?;
2961 // syscall 11 = SendWithCap
2962 // arg0 = (grant_slot << 16) | endpoint_slot - same slot holds both SEND and GRANT.
2963 let packed = ((slot as u64) << 16) | (slot as u64);
2964 let payload = msg.payload_bytes();
2965 // SAFETY: syscall(11) = SendWithCap; packed and payload are from user space.
2966 let ret = unsafe {
2967 raw_syscall(11, packed, payload.as_ptr() as u64, payload.len() as u64)
2968 };
2969 if ret == 0 { Ok(()) } else { Err(crate::ipc::i64_to_ipc_error(ret)) }
2970 }
2971
2972 /// Take the next pending received capability, if any.
2973 ///
2974 /// After `recv()` delivers a message containing an embedded cap, the kernel
2975 /// installs the cap into this task's table and queues the slot index. Call
2976 /// this once per embedded cap to retrieve each one.
2977 pub fn take_pending_cap(&self) -> Option<CapHandle> {
2978 // SAFETY: syscall(12) = TakePendingCap; no args.
2979 let ret = unsafe { raw_syscall(12, 0, 0, 0) };
2980 if ret >= 0 { Some(CapHandle(ret as u32)) } else { None }
2981 }
2982
2983 /// Acquire a fresh SEND|GRANT cap to `peer` via the kernel name directory.
2984 ///
2985 /// Used by property-test probes that need to transfer capabilities (P3).
2986 /// Returns `None` if the service is not registered or the cap table is full.
2987 pub fn acquire_send_grant_cap(&self, peer: &str) -> Option<CapHandle> {
2988 let bytes = peer.as_bytes();
2989 let len = bytes.len();
2990 if len == 0 || len > NAME_MAX { return None; }
2991 // SAFETY: syscall(10) = AcquireSendCap; arg2=1 requests SEND|GRANT.
2992 let ret = unsafe { raw_syscall(10, bytes.as_ptr() as u64, len as u64, 1) };
2993 if ret < 0 { None } else { Some(CapHandle(ret as u32)) }
2994 }
2995
2996 /// Acquire a fresh SEND cap to `peer` via the kernel name directory.
2997 ///
2998 /// Returns the new cap handle, or `None` if the name is not registered.
2999 pub fn acquire_send_cap(&self, peer: &str) -> Option<CapHandle> {
3000 let bytes = peer.as_bytes();
3001 let len = bytes.len();
3002 if len == 0 || len > NAME_MAX { return None; }
3003 // SAFETY: syscall(10) = AcquireSendCap; arg2=0 = SEND only.
3004 let ret = unsafe { raw_syscall(10, bytes.as_ptr() as u64, len as u64, 0) };
3005 if ret < 0 { None } else { Some(CapHandle(ret as u32)) }
3006 }
3007
3008 /// Query the current generation of the named endpoint.
3009 ///
3010 /// Returns the generation counter as a u64, or 0 if the name is not
3011 /// registered OR the caller lacks INTROSPECT (query 2 is gated). Used by
3012 /// property tests P2 and P8 (§7.5, §14.2).
3013 pub fn inspect_endpoint_generation(&self, name: &str) -> u64 {
3014 let bytes = name.as_bytes();
3015 let len = bytes.len();
3016 if len == 0 || len > NAME_MAX { return 0; }
3017 // SAFETY: syscall(13) = InspectKernel; query_id=2 = endpoint generation by name.
3018 let ret = unsafe {
3019 raw_syscall(13, 2, bytes.as_ptr() as u64, len as u64)
3020 };
3021 if ret < 0 { 0 } else { ret as u64 }
3022 }
3023
3024 /// One byte from the COM2 operator channel, or `None` when the port is empty (kernel query 21).
3025 ///
3026 /// The kernel owns the UART - hardware, and §11.4 already sanctions it owning a serial console -
3027 /// and hands bytes out; the `control` service owns what they MEAN. Transport in the kernel,
3028 /// interpretation in a service (C1-6).
3029 pub fn com2_byte(&self) -> Option<u8> {
3030 // SAFETY: syscall(13) = InspectKernel; query 21 = pop one COM2 byte, -1 when empty.
3031 let v = unsafe { raw_syscall(13, 21, 0, 0) };
3032 if v < 0 { None } else { Some(v as u8) }
3033 }
3034
3035 /// The board's own MAC address (kernel query 23), or `None` where the board cannot say.
3036 ///
3037 /// A NIC driver runs in userspace and cannot reach the board identity: on the Pi this address lives
3038 /// in the VideoCore mailbox, nowhere near the controller's register window. Without it a driver has
3039 /// to invent an address, and an invented address that is hardcoded is the same on every board -
3040 /// which stops being harmless the moment two of them share a network.
3041 pub fn board_mac(&self) -> Option<[u8; 6]> {
3042 // SAFETY: syscall(13) = InspectKernel; query 23 = the board MAC packed little-endian, -1 if none.
3043 let v = unsafe { raw_syscall(13, 23, 0, 0) };
3044 if v < 0 { return None; }
3045 let v = v as u64;
3046 Some([v as u8, (v >> 8) as u8, (v >> 16) as u8,
3047 (v >> 24) as u8, (v >> 32) as u8, (v >> 40) as u8])
3048 }
3049
3050 /// Inject a test interrupt. Requires the FIRE_IRQ capability, held only by `control`; a non-holder
3051 /// gets `false` rather than a silent no-op.
3052 pub fn fire_irq(&self, irq: u8) -> bool {
3053 // SAFETY: syscall(51) = FireIrq; the kernel validates FIRE_IRQ by holdings.
3054 unsafe { raw_syscall(51, irq as u64, 0, 0) == 0 }
3055 }
3056
3057 /// Return the bytes dynamically allocated by this task so far.
3058 ///
3059 /// Wraps InspectKernel query 0. Used by property test P4 (§10.3).
3060 pub fn inspect_kernel_alloc_bytes(&self) -> u64 {
3061 // SAFETY: syscall(13) = InspectKernel; query_id=0 = task alloc bytes.
3062 let ret = unsafe { raw_syscall(13, 0, 0, 0) };
3063 if ret < 0 { 0 } else { ret as u64 }
3064 }
3065
3066 /// Return the count of live endpoints in the kernel routing table.
3067 ///
3068 /// Wraps InspectKernel query 1 (INTROSPECT-gated; 0 when refused). Used by property test P5 (§8.3).
3069 pub fn inspect_kernel_endpoint_count(&self) -> Option<u32> {
3070 // SAFETY: syscall(13) = InspectKernel; query_id=1 = live endpoint count.
3071 let ret = unsafe { raw_syscall(13, 1, 0, 0) };
3072 // A refusal (no INTROSPECT) or an error is `None`, never a value: it read as 0 until
3073 // 2026-10-10, a believable count of nothing (`backlog/fixed/80` K18).
3074 if ret < 0 { None } else { Some(ret as u32) }
3075 }
3076
3077 /// Capacity of the in-kernel USB mass-storage device in 512-byte sectors, 0 if none is attached.
3078 /// Requires the `USB_DISK_RESOURCE` capability. Syscall 46. The kernel drives no USB disk on any
3079 /// port now (see [`USB_DISK_BUSY`]), so this is always 0 and the block calls below always fail.
3080 pub fn usb_disk_sectors(&self) -> u64 {
3081 // SAFETY: syscall(46) = UsbDiskInfo; no arguments, gated by the USB_DISK capability.
3082 let ret = unsafe { raw_syscall(46, 0, 0, 0) };
3083 if ret < 0 { 0 } else { ret as u64 }
3084 }
3085
3086 /// Read the 512-byte block at `lba` from the USB mass-storage device into `dst`. Returns false if
3087 /// there is no device, the LBA is past the end, or the transfer failed. Requires `USB_DISK_RESOURCE`.
3088 /// Syscall 47.
3089 #[must_use = "the destination buffer is NOT valid data if this is false"]
3090 pub fn usb_disk_read(&self, lba: u64, dst: &mut [u8; 512]) -> bool {
3091 // SAFETY: syscall(47) = UsbDiskRead; the kernel writes exactly 512 bytes to `dst` on success,
3092 // through its checked user-pointer path.
3093 if !lba_fits_syscall_abi(lba) { return false; }
3094 let ret = unsafe { raw_syscall(47, lba, dst.as_mut_ptr() as u64, 0) };
3095 ret == 0
3096 }
3097
3098 /// Write `src` as the 512-byte block at `lba` on the USB mass-storage device. Requires `USB_DISK_RESOURCE`.
3099 /// Syscall 48.
3100 #[must_use = "the block did NOT reach the medium if this is false"]
3101 pub fn usb_disk_write(&self, lba: u64, src: &[u8; 512]) -> bool {
3102 // SAFETY: syscall(48) = UsbDiskWrite; the kernel reads exactly 512 bytes from `src` through its
3103 // checked user-pointer path.
3104 if !lba_fits_syscall_abi(lba) { return false; }
3105 let ret = unsafe { raw_syscall(48, lba, src.as_ptr() as u64, 0) };
3106 ret == 0
3107 }
3108
3109 /// Read a block, returning the raw status so BUSY is distinguishable from FAILED: `0` ok, the
3110 /// device is busy (`USB_DISK_BUSY` - re-ask, nothing is wrong), anything else a real error. The `bool` variants
3111 /// above collapse those two, which is exactly the conflation that made a busy stick look broken.
3112 pub fn usb_disk_read_status(&self, lba: u64, dst: &mut [u8; 512]) -> i64 {
3113 // SAFETY: syscall(47) = UsbDiskRead; the kernel writes exactly 512 bytes through its checked
3114 // user-pointer path.
3115 if !lba_fits_syscall_abi(lba) { return -1; }
3116 unsafe { raw_syscall(47, lba, dst.as_mut_ptr() as u64, 0) }
3117 }
3118
3119 /// Write a block, returning the raw status (see `usb_disk_read_status`).
3120 pub fn usb_disk_write_status(&self, lba: u64, src: &[u8; 512]) -> i64 {
3121 // SAFETY: syscall(48) = UsbDiskWrite; the kernel reads exactly 512 bytes from `src`.
3122 if !lba_fits_syscall_abi(lba) { return -1; }
3123 unsafe { raw_syscall(48, lba, src.as_ptr() as u64, 0) }
3124 }
3125
3126 /// Make previously written blocks durable on the USB mass-storage device (SCSI SYNCHRONIZE CACHE).
3127 /// Requires `USB_DISK_RESOURCE` WRITE. Syscall 49.
3128 ///
3129 /// A write is only ACKNOWLEDGED when `usb_disk_write` returns - the device may still be holding the
3130 /// bytes in a volatile buffer. Anything that promises durability (a format, a journal commit) has to
3131 /// ask for it, and check the answer: `false` means the data is NOT known to be on the medium.
3132 #[must_use = "prior writes are NOT durable if this is false"]
3133 pub fn usb_disk_flush(&self) -> bool {
3134 // SAFETY: syscall(49) = UsbDiskFlush; takes no arguments and touches no user memory.
3135 let ret = unsafe { raw_syscall(49, 0, 0, 0) };
3136 ret == 0
3137 }
3138
3139 /// The SD/EMMC controller's base clock in Hz (0 if the platform does not report one), via
3140 /// InspectKernel query 20. The block driver needs it to compute its clock divider: on the BCM283x
3141 /// the controller's own capability register reports the base clock wrongly, and the driver holds
3142 /// only its controller's registers, so it cannot ask the platform firmware itself. A driver that
3143 /// gets 0 should report that rather than guess - a wrong divider runs the card's identification
3144 /// clock at the wrong speed, which fails on hardware and not under emulation. Ungated - task-neutral
3145 /// hardware info, like the console geometry and the RTC.
3146 pub fn emmc_base_clock_hz(&self) -> u32 {
3147 // SAFETY: syscall(13) = InspectKernel; query_id=20 = EMMC base clock (Hz).
3148 let ret = unsafe { raw_syscall(13, 20, 0, 0) };
3149 if ret < 0 { 0 } else { ret as u32 }
3150 }
3151
3152 /// The discovered NIC's PCI identity as `vendor | device<<16` (0 if no NIC), via InspectKernel
3153 /// query 14. A NIC driver reads it to know which chip it is driving (e.g. Intel e1000 =
3154 /// 0x100E_8086 vs Realtek RTL8168 = 0x8168_10EC). Ungated - task-neutral hardware info.
3155 pub fn nic_vendor_device(&self) -> u32 {
3156 // SAFETY: syscall(13) = InspectKernel; query_id=14 = NIC vendor|device.
3157 let ret = unsafe { raw_syscall(13, 14, 0, 0) };
3158 if ret < 0 { 0 } else { ret as u32 }
3159 }
3160
3161 /// Whether the PCI scan found an xHCI USB host controller (InspectKernel query 18, ungated
3162 /// task-neutral hardware fact). The supervisor reads it to skip spawning the `xhci` driver on a
3163 /// machine that has no xHCI controller (an idle driver would busy-hold a core). Falls back to
3164 /// `true` if the query is unavailable, preserving the always-spawn behaviour.
3165 pub fn xhci_present(&self) -> bool {
3166 // SAFETY: syscall(13) = InspectKernel; query_id=18 = USB controller presence bitmask.
3167 let ret = unsafe { raw_syscall(13, 18, 0, 0) };
3168 if ret < 0 { return true; } // query unavailable - spawn as before
3169 (ret & 0b01) != 0
3170 }
3171
3172 /// Whether the PCI scan found an EHCI (USB 2.0) host controller (InspectKernel query 18, ungated).
3173 /// The supervisor reads it to skip spawning the `ehci` driver on a machine with no EHCI at all
3174 /// (e.g. the Wyse 5070), so an idle driver does not busy-hold a core. Falls back to `true` if the
3175 /// query is unavailable, preserving the always-spawn behaviour.
3176 pub fn ehci_present(&self) -> bool {
3177 // SAFETY: syscall(13) = InspectKernel; query_id=18 = USB controller presence bitmask.
3178 let ret = unsafe { raw_syscall(13, 18, 0, 0) };
3179 if ret < 0 { return true; } // query unavailable - spawn as before
3180 (ret & 0b10) != 0
3181 }
3182
3183 /// Whether the PCI scan found a NIC this build can actually drive (present AND an e1000 or RTL8168),
3184 /// via InspectKernel query 18 (ungated). The supervisor reads it to skip spawning `nic-driver` (and
3185 /// its dependent `net-stack`) on a machine with no usable NIC, so they do not busy-hold cores.
3186 /// Falls back to `true` if the query is unavailable, preserving the always-spawn behaviour.
3187 pub fn nic_present(&self) -> bool {
3188 // SAFETY: syscall(13) = InspectKernel; query_id=18 = hardware-driver presence bitmask.
3189 let ret = unsafe { raw_syscall(13, 18, 0, 0) };
3190 if ret < 0 { return true; } // query unavailable - spawn as before
3191 (ret & 0b100) != 0
3192 }
3193
3194 /// The NIC's register-space MMIO base (the BAR the PCI scan chose), 0 if none. InspectKernel query
3195 /// 15, ungated. A diagnostic - a driver reads it to confirm which BAR it was handed.
3196 pub fn nic_mmio_base(&self) -> u64 {
3197 // SAFETY: syscall(13) = InspectKernel; query_id=15 = NIC MMIO base.
3198 let ret = unsafe { raw_syscall(13, 15, 0, 0) };
3199 if ret < 0 { 0 } else { ret as u64 }
3200 }
3201
3202 /// Return the number of free physical frames.
3203 ///
3204 /// Wraps InspectKernel query 4. Queries 4-8 are INTROSPECT-gated, and these wrappers read a
3205 /// refusal as 0 (or, for the core count, 1) - a value the caller cannot tell from a real one.
3206 pub fn inspect_kernel_free_frames(&self) -> Option<u64> {
3207 // SAFETY: syscall(13) = InspectKernel; query_id=4 = free frame count.
3208 let ret = unsafe { raw_syscall(13, 4, 0, 0) };
3209 // A refusal (no INTROSPECT) or an error is `None`, never a value: it read as 0 until
3210 // 2026-10-10, a believable count of nothing (`backlog/fixed/80` K18).
3211 if ret < 0 { None } else { Some(ret as u64) }
3212 }
3213
3214 /// Return the total usable physical frames at boot time.
3215 ///
3216 /// Wraps InspectKernel query 5.
3217 pub fn inspect_kernel_total_frames(&self) -> Option<u64> {
3218 // SAFETY: syscall(13) = InspectKernel; query_id=5 = total frame count.
3219 let ret = unsafe { raw_syscall(13, 5, 0, 0) };
3220 // A refusal (no INTROSPECT) or an error is `None`, never a value: it read as 0 until
3221 // 2026-10-10, a believable count of nothing (`backlog/fixed/80` K18).
3222 if ret < 0 { None } else { Some(ret as u64) }
3223 }
3224
3225 /// The endpoint task `slot` OWNS, or 0 if it owns none (InspectKernel query 24, INTROSPECT).
3226 ///
3227 /// With [`Self::task_awaits_endpoint`] this is the whole of the blocked-chain walk: map the
3228 /// endpoint a stuck task awaits back to the task that owns it, then ask what THAT one awaits.
3229 /// See `utilities/47_events.md`.
3230 pub fn task_own_endpoint(&self, slot: u32) -> Option<u64> {
3231 // SAFETY: syscall(13) = InspectKernel; query_id=24 = the task's own endpoint.
3232 let ret = unsafe { raw_syscall(13, 24, slot as u64, 0) };
3233 // A refusal (no INTROSPECT) or an error is `None`, never a value: it read as 0 until
3234 // 2026-10-10, a believable count of nothing (`backlog/fixed/80` K18).
3235 if ret < 0 { None } else { Some(ret as u64) }
3236 }
3237
3238 /// The endpoint task `slot` is blocked-in-CALL awaiting a reply from, or 0 if no call is in
3239 /// flight (InspectKernel query 25, INTROSPECT).
3240 ///
3241 /// A best-effort snapshot, on the same contract as [`Self::task_stat`]: the kernel reads it
3242 /// without taking the routing lock, so an observer cannot stall the thing it observes.
3243 pub fn task_awaits_endpoint(&self, slot: u32) -> Option<u64> {
3244 // SAFETY: syscall(13) = InspectKernel; query_id=25 = the endpoint awaited in a CALL.
3245 let ret = unsafe { raw_syscall(13, 25, slot as u64, 0) };
3246 // A refusal (no INTROSPECT) or an error is `None`, never a value: it read as 0 until
3247 // 2026-10-10, a believable count of nothing (`backlog/fixed/80` K18).
3248 if ret < 0 { None } else { Some(ret as u64) }
3249 }
3250
3251 /// The kernel's BOOT RECORD size: `(held, capacity)` in bytes, or `None` without INTROSPECT
3252 /// (InspectKernel query 27). The record is a fixed copy of the first bytes ever logged, which
3253 /// never wraps; `held == capacity` means it filled, and later lines are only in the kernel's ring
3254 /// (until it wraps) and on serial.
3255 pub fn boot_record_size(&self) -> Option<(usize, usize)> {
3256 let held = self.boot_record_query(0, 0);
3257 let cap = self.boot_record_query(1, 0);
3258 if held < 0 || cap < 0 { None } else { Some((held as usize, cap as usize)) }
3259 }
3260
3261 /// Copy the boot record from `offset` into `buf`: `Some(n)` bytes, `Some(0)` at the end, `None`
3262 /// refused (InspectKernel query 27, INTROSPECT). The buffer is the kernel's chunk size exactly, so
3263 /// no read can ask it to write past the end of one.
3264 pub fn boot_record_read(&self, offset: usize, buf: &mut [u8; BOOT_READ_CHUNK]) -> Option<usize> {
3265 let r = self.boot_record_query(offset as u64, buf.as_mut_ptr() as u64);
3266 if r < 0 { None } else { Some(r as usize) }
3267 }
3268
3269 fn boot_record_query(&self, a1: u64, a2: u64) -> i64 {
3270 // SAFETY: syscall(13) = InspectKernel; query_id=27 = the boot record. `a2` is 0 or a buffer of
3271 // BOOT_READ_CHUNK bytes the caller holds mutably; the kernel validates the range before writing.
3272 unsafe { raw_syscall(13, 27, a1, a2) }
3273 }
3274
3275 /// The wall-clock datetime captured by the kernel at **boot** (InspectKernel query 12, ungated).
3276 /// Same packed layout as `datetime`. Pairs with `datetime` to compute uptime as a wall-clock
3277 /// delta - portable across timer modes (a tick counter's rate is not: periodic-mode QEMU ticks
3278 /// at ~10 Hz, TSC-deadline HW at 100 Hz). Returns the epoch (all-zero fields) if not captured.
3279 pub fn boot_datetime(&self) -> Datetime {
3280 // SAFETY: syscall(13) = InspectKernel; query_id=12 = packed boot datetime.
3281 let p = unsafe { raw_syscall(13, 12, 0, 0) } as u64;
3282 Self::unpack_datetime(p)
3283 }
3284
3285 /// System uptime in **seconds** = now − boot, both from the hardware RTC. Never negative
3286 /// (saturates at 0). The `uptime` shell command renders this. Wall-clock based, so it is
3287 /// correct regardless of the APIC timer mode (unlike a raw tick counter).
3288 pub fn uptime_secs(&self) -> i64 {
3289 // "now" is the DEGLITCHED MONOTONIC clock (query 17), not the raw RTC datetime (which is frozen
3290 // at 0 on a board with no RTC, e.g. the Pi 2 - the reason the old `datetime - boot` read 0 there).
3291 let now = self.epoch_secs_monotonic();
3292 let boot_epoch = self.boot_datetime().epoch_secs();
3293 // A board with no RTC reports a zero/garbage boot datetime whose epoch is far before 2001 (a
3294 // packed all-zeros datetime unpacks to ~year 0 = ~-6.2e10 s - subtracting THAT would ADD ~1970
3295 // years). There the monotonic clock is already seconds-since-boot (its CNTPCT baseline, ARM), so
3296 // it IS the uptime. With a real RTC, uptime is the elapsed wall-clock since boot.
3297 if boot_epoch < 1_000_000_000 { now.max(0) } else { (now - boot_epoch).max(0) }
3298 }
3299
3300 /// Timer ticks the given core spent running a user task (not idle) since boot.
3301 ///
3302 /// Wraps InspectKernel query 6 (arg1 = core index).
3303 pub fn inspect_core_active_ticks(&self, core: u32) -> Option<u64> {
3304 // SAFETY: syscall(13) = InspectKernel; query_id=6, arg1=core.
3305 let ret = unsafe { raw_syscall(13, 6, core as u64, 0) };
3306 // A refusal (no INTROSPECT) or an error is `None`, never a value: it read as 0 until
3307 // 2026-10-10, a believable count of nothing (`backlog/fixed/80` K18).
3308 if ret < 0 { None } else { Some(ret as u64) }
3309 }
3310
3311 /// Total timer ticks seen on the given core since boot.
3312 ///
3313 /// Wraps InspectKernel query 7 (arg1 = core index).
3314 pub fn inspect_core_total_ticks(&self, core: u32) -> Option<u64> {
3315 // SAFETY: syscall(13) = InspectKernel; query_id=7, arg1=core.
3316 let ret = unsafe { raw_syscall(13, 7, core as u64, 0) };
3317 // A refusal (no INTROSPECT) or an error is `None`, never a value: it read as 0 until
3318 // 2026-10-10, a believable count of nothing (`backlog/fixed/80` K18).
3319 if ret < 0 { None } else { Some(ret as u64) }
3320 }
3321
3322 /// Number of CPU cores ready since boot.
3323 ///
3324 /// Wraps InspectKernel query 8.
3325 pub fn inspect_core_count(&self) -> u32 {
3326 // SAFETY: syscall(13) = InspectKernel; query_id=8.
3327 let ret = unsafe { raw_syscall(13, 8, 0, 0) };
3328 // A refusal or an error is NOT "one core": it said 1 until 2026-10-10, so a caller without
3329 // INTROSPECT reported a one-core machine (`backlog/fixed/80` H20). 0 is "the kernel did not say", and
3330 // a caller must not present it as a count.
3331 if ret <= 0 { 0 } else { ret as u32 }
3332 }
3333
3334 /// Terminal geometry as `(rows, cols)` text cells, or `(0, 0)` if it cannot be determined.
3335 ///
3336 /// **Asked of the `console` service, not the kernel.** It used to be `InspectKernel` query 9, which
3337 /// is now deleted: rows and columns are derived from the safe-area inset, the cell size and the
3338 /// font-scale rule, all of which live in the terminal - so the terminal is the only party that can
3339 /// answer, and asking anyone else would be a second source of truth (Commandment III).
3340 ///
3341 /// `(0, 0)` means **unknown**, and callers already treat it that way explicitly rather than
3342 /// substituting a size behind the user's back (the pager prints unpaged; `edit` falls back to 24x80
3343 /// and says so). It is returned when there is no console service, when it is mid-restart, or when it
3344 /// holds no framebuffer - all cases where a guessed geometry would be a silent fallback.
3345 ///
3346 /// Costs one IPC round trip, so callers that lay out a screen should ask once and keep the answer
3347 /// for that screen rather than per line.
3348 pub fn console_dims(&self) -> (u16, u16) {
3349 let mut buf = [0u8; 8];
3350 // Opcode 1 = REQ_DIMS (`services/console/src/main.rs`). The reply is rows then cols, u16 LE.
3351 // ACQUIRE THE PEER BY NAME IF WE WERE NEVER WIRED TO IT.
3352 //
3353 // The shell's contract declares `console` as a send peer, but the supervisor cannot wire it:
3354 // the shell is spawned BEFORE the console service exists, so there is nothing to hand it. The
3355 // kernel name directory is exactly the answer to that (§14.3 - reacquire by name), and
3356 // `time_rpc` in the shell already does this. This did not, so `find_send_slot` missed, the
3357 // request returned None, and the caller read (0, 0).
3358 //
3359 // Zero rows is not a small error here: `cmd_help` takes it to mean "no terminal" and prints
3360 // its whole table instead of paging it. A feature disappeared because a lookup missed, which
3361 // is the quiet kind of failure §26.7 is about - nothing was reported, it simply stopped
3362 // behaving.
3363 let mut n = self.request_with_reply_deadline_into("console", &[1u8], &mut buf, 2);
3364 if n.is_none() && self.reacquire_by_name("console") {
3365 n = self.request_with_reply_deadline_into("console", &[1u8], &mut buf, 2);
3366 }
3367 match n {
3368 Some(k) if k >= 4 => (
3369 u16::from_le_bytes([buf[0], buf[1]]),
3370 u16::from_le_bytes([buf[2], buf[3]]),
3371 ),
3372 _ => (0, 0),
3373 }
3374 }
3375
3376 // RETAINED REASONING FROM THE DELETED `console_scroll`, and it is `//` rather than `///` on
3377 // purpose. A `///` run attaches to the item BELOW it, and the item below this one is
3378 // `console_history` - so while these lines were doc comments, rustdoc documented
3379 // `console_history` as "Move the console's scrolled-back view" and handed the reader a
3380 // deleted method's contract as if it were that method's. The words are kept because two of
3381 // the lessons in them were expensive and are still true of anything that asks the console a
3382 // question: a failure must not wear a valid value, and a KEYSTROKE may not wait seconds.
3383 // The last sentence below stops mid-clause; its tail went with the code, and it is left
3384 // truncated rather than completed by guess.
3385 //
3386 // Move the console's scrolled-back view, returning `(lines back, most it can go back)`.
3387 //
3388 // `action` is one of the `SCROLL_*` values in `services/console/src/term.rs`: 0 live, 1 line
3389 // up, 2 line down, 3 page up, 4 page down, 5 oldest kept.
3390 //
3391 // The framebuffer console has no scrollback of its own - reaching the bottom of the screen used
3392 // to mean the top was gone permanently - and this is how a holder of the keyboard asks to look
3393 // back. The console does the arithmetic, because only it knows how many lines it is holding and
3394 // how tall the screen is; a second copy of either number here would be a second thing to drift.
3395 //
3396 // **THE SCROLL IS A REQUEST, NOT AN ESCAPE SEQUENCE.** Console output is untrusted content: a
3397 // file being `read` can hold any bytes, so a scroll expressed in the byte stream would let a
3398 // file scroll the view of the terminal showing it. A request carries a reply cap and output
3399 // does not, so the two channels are separated by construction.
3400 //
3401 // **`None` MEANS THE CONSOLE DID NOT ANSWER, and that is not the same as `(0, 0)`.**
3402 //
3403 // It returned `(0, 0)` for both "the view is at live" and "I could not reach the console",
3404 // and the caller could not tell them apart. On a Dell Wyse, 2026-09-18, that turned a timed-out
3405 // request into a plausible success: the shell believed it had scrolled back to live, left the
3406 // scrollback view, and returned to the prompt **while the screen was still showing history** -
3407 // the two disagreeing about what was on the display, with nothing said. A failure wearing a
3408 // valid value is the exact shape invariant 12 and 26.7 exist to forbid, and it is worth the
3409 // `Option` to make it unrepresentable.
3410 //
3411 // **ONE SECOND, AND NO RETRY - because this is a KEYSTROKE.** It used to be the same two
3412 // seconds plus a reacquire-and-retry that `console_dims` uses, which is right for a
3413 // once-per-command lookup and wrong here: it let a single press of an arrow key block the
3414 // shell for four seconds, which the operator experiences as the machine locking up. Nothing
3415 // above the kernel may hang on a peer (Commandment V); a scroll that cannot be served quickly
3416 // must FAIL quickly and say so.
3417 //
3418 // Dropping the retry costs little. It exists so a client survives the peer RESTARTING, and a
3419 // console that has just restarted is rebuilding the screen anyway; the next `console_dims`
3420 // `console_scroll` IS GONE, and it is the mechanism `backlog/fixed/37` was about.
3421 //
3422 // It asked the console to move its own view, and the console could not answer until it had
3423 // repainted the framebuffer - `paint_view` + `present`, synchronously, inside the request. On a
3424 // 3840x2160 panel that is the most expensive thing it does, and the caller gave it one second
3425 // from the same core. That is why a scroll took "over two seconds to answer": nothing was lost
3426 // or stuck, the work simply did not fit the deadline.
3427 //
3428 // `console_history` below replaced it by moving the WORK rather than tuning the number: the
3429 // console hands over bytes, and the caller paints its own screen through ordinary output, which
3430 // is a send and carries no deadline at all. Deleting this leaves no way to reintroduce the
3431 // shape by accident.
3432
3433 /// Read a page of the console's scrollback AS DATA, starting at line `from` (0 = oldest KEPT).
3434 ///
3435 /// Returns `(lines, total retained, any line has aged out, bytes written)`. The payload after
3436 /// the 4-byte header is `lines` records of `[len, bytes...]`; `bytes` is the whole reply so a
3437 /// caller can bound its own walk.
3438 ///
3439 /// **The replacement for the deleted `console_scroll`, which could not be made cheap.**
3440 /// Moving the terminal's own view forces a full repaint before the console can reply - on a
3441 /// 3840x2160 panel the most expensive thing it does, paid inside the caller's blocking request,
3442 /// from the same core (`backlog/fixed/37`). This asks for BYTES: the console copies out of its ring
3443 /// and replies, and the caller paints its own screen through ordinary output, which is a send
3444 /// rather than a call and carries no deadline at all.
3445 ///
3446 /// The history is BOUNDED (32 KiB / 512 lines). `aged` is how a view says "this is the oldest
3447 /// line kept" rather than implying it is the start of the session (§26.7).
3448 pub fn console_history(&self, from: u16, buf: &mut [u8]) -> Option<(usize, u16, bool, usize)> {
3449 // Opcode 3 = REQ_HISTORY (`services/console/src/main.rs`).
3450 let req = [3u8, from as u8, (from >> 8) as u8];
3451 let k = self.request_with_reply_deadline_into("console", &req, buf, 1)?;
3452 if k < 4 { return None; }
3453 Some((buf[0] as usize, u16::from_le_bytes([buf[1], buf[2]]), buf[3] != 0, k))
3454 }
3455
3456 /// Whether the input driver has reported setup complete (syscall 13, query 10).
3457 /// The deterministic end-of-boot signal: the shell watches it to auto-clear the
3458 /// boot screen the moment the keyboard subsystem is up. Ambient.
3459 pub fn input_ready(&self) -> bool {
3460 // SAFETY: syscall(13) = InspectKernel; query_id=10 = input-ready flag.
3461 unsafe { raw_syscall(13, 10, 0, 0) > 0 }
3462 }
3463
3464 /// Report that input-subsystem setup is complete (syscall 27). Called by the
3465 /// USB keyboard driver (xHCI) in every terminal path once it has finished - the
3466 /// end-of-boot signal. Requires the CONSOLE_PUSH cap (the input driver only).
3467 pub fn signal_input_ready(&self) {
3468 let slot = Self::ctx().console_push_slot;
3469 if slot == u32::MAX { return; }
3470 // SAFETY: syscall(27) = SignalInputReady; slot is the kernel-written cap index.
3471 let _ = unsafe { raw_syscall(27, slot as u64, 0, 0) };
3472 }
3473
3474 /// Read the hardware TSC (Time Stamp Counter) via the kernel.
3475 ///
3476 /// Returns the arch's free-running counter (InspectKernel query 3): RDTSC on x86, CNTPCT on
3477 /// ARM; its rate is `tsc_ticks_per_10ms`. Useful for measuring kernel operation latencies
3478 /// in benchmark probes (§22 Perf B1-B10). Not comparable across hosts.
3479 pub fn read_tsc(&self) -> u64 {
3480 // SAFETY: syscall(13) = InspectKernel; query_id=3 = read TSC.
3481 let ret = unsafe { raw_syscall(13, 3, 0, 0) };
3482 if ret < 0 { 0 } else { ret as u64 }
3483 }
3484
3485 /// Counter ticks per 10 ms, from the kernel's boot-time calibration (CPUID on x86, the measured
3486 /// timer rate / 100 on ARM) (InspectKernel query 16).
3487 /// Convert a TSC delta to milliseconds with `delta_cycles * 10 / tsc_ticks_per_10ms()`. Returns 0
3488 /// if the TSC was not calibrated (callers should then skip the millisecond conversion). `ping` uses
3489 /// it to report round-trip time.
3490 pub fn tsc_ticks_per_10ms(&self) -> u64 {
3491 // SAFETY: syscall(13) = InspectKernel; query_id=16 = TSC ticks per 10 ms quantum.
3492 let ret = unsafe { raw_syscall(13, 16, 0, 0) };
3493 if ret < 0 { 0 } else { ret as u64 }
3494 }
3495
3496 /// Sleep for approximately `ms` milliseconds - the PORTABLE way to pace a loop.
3497 ///
3498 /// Prefer this over `sleep(cycles)` for anything expressing a DURATION. A "cycle" is not a portable
3499 /// unit: on x86 it is a ~2 GHz CPU cycle, on the Pi 2 it is a tick of a ~1 MHz timer, so the same
3500 /// literal means two wildly different lengths of time. Services were written with x86 in mind -
3501 /// `60_000_000` meaning "~30 ms at 2 GHz" - and on ARM that same constant asks for ~60 SECONDS once
3502 /// the quantum is calibrated. Three loops (the shell's muted poll and observe q-poll, and `observe`'s
3503 /// repaint pace) were exactly that, and each would have become a minute-long stall.
3504 ///
3505 /// Converts through the kernel's own calibration, so it is right on any machine. Floors at one
3506 /// scheduler quantum: the underlying `sleep(0)` returns immediately, which would turn a paced loop
3507 /// into a busy spin - the opposite of what every caller wants.
3508 pub fn sleep_ms(&self, ms: u64) {
3509 self.sleep(self.duration_cycles(ms));
3510 }
3511
3512 /// Cycles for approximately `ms` milliseconds, for the two syscalls that take a cycle count
3513 /// (`sleep`, `recv_timeout`). See [`Self::sleep_ms`] for why a raw cycle literal is not portable.
3514 ///
3515 /// Never returns 0. That matters beyond pacing: `recv_timeout(0)` means **block forever**, so a
3516 /// zero here would convert a bounded wait into an unbounded one - a hang produced by a unit
3517 /// conversion. An uncalibrated machine (query 16 reads 0) also lands on the floor, which is exactly
3518 /// the one-quantum behaviour those platforms had before any of this existed.
3519 pub fn duration_cycles(&self, ms: u64) -> u64 {
3520 let per_10ms = self.tsc_ticks_per_10ms();
3521 if per_10ms == 0 { return 1; } // uncalibrated: floor to one quantum
3522 let c = per_10ms.saturating_mul(ms) / 10;
3523 if c == 0 { 1 } else { c }
3524 }
3525
3526 /// Read the hardware real-time clock (wall-clock date/time) via the kernel.
3527 ///
3528 /// Ambient - the time of day is task-neutral hardware info, like the TSC.
3529 /// Wraps InspectKernel query 11; the kernel returns the fields packed into a
3530 /// `u64` (see `kernel/src/arch/x86_64/rtc.rs`), which this unpacks.
3531 pub fn datetime(&self) -> Datetime {
3532 // SAFETY: syscall(13) = InspectKernel; query_id=11 = packed RTC datetime.
3533 let p = unsafe { raw_syscall(13, 11, 0, 0) } as u64;
3534 Self::unpack_datetime(p)
3535 }
3536
3537 /// Deglitched monotonic "now" in epoch seconds (kernel query 17). Unlike `datetime().epoch_secs()`
3538 /// (the raw RTC, query 11 - a CMOS misread on an in-range year slips through and reads years off), this
3539 /// drops backward / huge-forward glitches. Use it for time-DELTA deadlines and pacing, NOT for display.
3540 pub fn epoch_secs_monotonic(&self) -> i64 {
3541 // SAFETY: syscall(13) = InspectKernel; query 17 = deglitched monotonic epoch seconds.
3542 unsafe { raw_syscall(13, 17, 0, 0) }
3543 }
3544
3545 // `set_wall_clock`, `set_clock_floor` and `clock_synced_secs_ago` were REMOVED with the kernel's
3546 // wall clock (clock slice 3). They called syscall 50 and query 21, which no longer mean what they
3547 // meant: 50 is deleted, and 21 was REUSED for `com2_byte`, so the stale reader did not merely fail -
3548 // it popped a byte off the operator channel. Ask the `time` service; it owns the clock now.
3549 // (2026-10-09) 50 has since been REUSED too: it is `CallDeadline`. Neither number is free.
3550
3551
3552
3553
3554 /// A hardware-random u32 from the SoC RNG (the BCM2835 RNG on the Pi 2), or None if this build exposes
3555 /// no hardware RNG. Ungated (entropy confers no authority). The `random` shell utility consumes it.
3556 pub fn hw_random(&self) -> Option<u32> {
3557 // SAFETY: syscall(13) = InspectKernel; query 19 = a hardware-random u32 (-1 if unavailable).
3558 let r = unsafe { raw_syscall(13, 19, 0, 0) };
3559 if r < 0 { None } else { Some(r as u32) }
3560 }
3561
3562 /// Drive a SoC GPIO pin (the shell `gpio` command; the Pi 2's BCM2835). `op`: 0 input / 1 output /
3563 /// 2 high / 3 low / 4 read. Returns the level (0/1) for a read, 0 on success, -1 on a bad pin /
3564 /// unsupported arch. Gated by the GPIO_DEVICE cap (only the shell holds it).
3565 pub fn gpio(&self, op: u32, pin: u32) -> i64 {
3566 // SAFETY: syscall(45) = Gpio; the kernel validates the GPIO_DEVICE cap and bounds op/pin.
3567 unsafe { raw_syscall(45, op as u64, pin as u64, 0) }
3568 }
3569
3570 /// Decode the packed RTC `u64` (the layout shared by query 11 / 12) into a `Datetime`.
3571 fn unpack_datetime(p: u64) -> Datetime {
3572 Datetime {
3573 second: (p & 0x3F) as u8,
3574 minute: ((p >> 6) & 0x3F) as u8,
3575 hour: ((p >> 12) & 0x1F) as u8,
3576 day: ((p >> 17) & 0x1F) as u8,
3577 month: ((p >> 22) & 0x0F) as u8,
3578 year: ((p >> 26) & 0xFFF) as u16,
3579 }
3580 }
3581
3582 /// Query the kernel task stat for scheduler slot `slot` (syscall 16).
3583 ///
3584 /// Returns a best-effort snapshot. If `slot` is out of range or the task
3585 /// is dead, `valid` will be false - and also when the caller lacks the
3586 /// INTROSPECT cap the kernel requires, which this cannot tell apart.
3587 pub fn task_stat(&self, slot: u32) -> TaskStat {
3588 // The kernel's `STAT_SIZE`: 176 bytes since names grew to 128 (`handle_task_stat`).
3589 let mut buf = [0u8; 176];
3590 // SAFETY: syscall(16) = TaskStat; buf is a local array on the user stack.
3591 let ret = unsafe {
3592 raw_syscall(16, slot as u64, buf.as_mut_ptr() as u64, 176)
3593 };
3594 if ret != 0 {
3595 return TaskStat {
3596 valid: false, state: 0, core: 0,
3597 mem_used: 0, mem_limit: 0, name_len: 0, name: [0u8; NAME_MAX],
3598 restart_count: 0, queue_depth: 0, run_ticks: 0, uptime_secs: 0,
3599 };
3600 }
3601 let valid = buf[0] != 0;
3602 let state = buf[1];
3603 let core = buf[2];
3604 let name_len = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]) as usize;
3605 let mem_used = u64::from_le_bytes([buf[8], buf[9], buf[10], buf[11],
3606 buf[12], buf[13], buf[14], buf[15]]);
3607 let mem_limit = u64::from_le_bytes([buf[16], buf[17], buf[18], buf[19],
3608 buf[20], buf[21], buf[22], buf[23]]);
3609 let mut name = [0u8; NAME_MAX];
3610 let copy_len = name_len.min(NAME_MAX);
3611 name[..copy_len].copy_from_slice(&buf[24..24 + copy_len]);
3612 let restart_count = u64::from_le_bytes([buf[152], buf[153], buf[154], buf[155],
3613 buf[156], buf[157], buf[158], buf[159]]);
3614 let queue_depth = buf[3];
3615 let run_ticks = u64::from_le_bytes([buf[160], buf[161], buf[162], buf[163],
3616 buf[164], buf[165], buf[166], buf[167]]);
3617 let uptime_secs = u64::from_le_bytes([buf[168], buf[169], buf[170], buf[171],
3618 buf[172], buf[173], buf[174], buf[175]]);
3619 TaskStat { valid, state, core, mem_used, mem_limit, name_len: copy_len, name,
3620 restart_count, queue_depth, run_ticks, uptime_secs }
3621 }
3622
3623 /// List the capabilities held by the task in `slot`, into `out`. Returns the
3624 /// number of entries written (capped at `out.len()` and 64). Requires the
3625 /// INTROSPECT cap; a refusal returns 0, the same as a task holding nothing.
3626 /// Best-effort snapshot - see [`task_stat`](Self::task_stat).
3627 pub fn task_caps(&self, slot: u32, out: &mut [CapInfo]) -> usize {
3628 const ENTRY: usize = 16;
3629 const MAX: usize = 64;
3630 let want = out.len().min(MAX);
3631 if want == 0 { return 0; }
3632 let mut buf = [0u8; ENTRY * MAX];
3633 // SAFETY: syscall(28) = TaskCaps; buf is a local array on the user stack.
3634 let ret = unsafe {
3635 raw_syscall(28, slot as u64, buf.as_mut_ptr() as u64, (want * ENTRY) as u64)
3636 };
3637 if ret <= 0 { return 0; }
3638 let count = (ret as usize).min(want);
3639 for i in 0..count {
3640 let o = i * ENTRY;
3641 out[i].resource_id = u64::from_le_bytes([
3642 buf[o], buf[o + 1], buf[o + 2], buf[o + 3],
3643 buf[o + 4], buf[o + 5], buf[o + 6], buf[o + 7],
3644 ]);
3645 out[i].rights = buf[o + 8];
3646 }
3647 count
3648 }
3649
3650 /// Send a message via an explicit cap handle (blocking).
3651 ///
3652 /// Used by benchmark probes that dynamically acquire send caps rather than
3653 /// using named peer slots, avoiding repeated name-lookup overhead.
3654 pub fn send_by_handle(&self, handle: CapHandle, msg: &Message) -> Result<(), IpcError> {
3655 crate::ipc::send(handle, msg)
3656 }
3657
3658 /// Query the rights bitfield of the cap at `handle`.
3659 ///
3660 /// Returns the rights byte as a u64, or `None` if the slot is empty.
3661 /// Used by property test P3 to verify rights do not widen on transfer (§7.3).
3662 pub fn query_cap_rights(&self, handle: CapHandle) -> Option<u64> {
3663 // SAFETY: syscall(14) = QueryCapRights; slot is a cap table index.
3664 let ret = unsafe { raw_syscall(14, handle.0 as u64, 0, 0) };
3665 if ret < 0 { None } else { Some(ret as u64) }
3666 }
3667
3668 /// Remove the cap at `handle` from this task's cap table.
3669 ///
3670 /// Idempotent: removing an already-empty slot is a no-op.
3671 pub fn remove_cap(&self, handle: CapHandle) {
3672 // SAFETY: syscall(15) = RemoveCap; slot is a cap table index.
3673 unsafe { raw_syscall(15, handle.0 as u64, 0, 0); }
3674 }
3675
3676 /// Send a message to `endpoint` with cap `grant` embedded.
3677 ///
3678 /// Unlike `send_with_cap` (which looks up a peer by name), this takes
3679 /// explicit handles - used by P3 where the endpoint and grant slot are
3680 /// the same (self-referential cap transfer). On success the grant cap is
3681 /// removed from the caller's table (§7.6).
3682 pub fn send_with_cap_by_handle(
3683 &self,
3684 endpoint: CapHandle,
3685 grant: CapHandle,
3686 msg: &crate::ipc::Message,
3687 ) -> Result<(), crate::ipc::IpcError> {
3688 let packed = ((grant.0 as u64) << 16) | (endpoint.0 as u64);
3689 let payload = msg.payload_bytes();
3690 // SAFETY: syscall(11) = SendWithCap; packed and payload are from user space.
3691 let ret = unsafe {
3692 raw_syscall(11, packed, payload.as_ptr() as u64, payload.len() as u64)
3693 };
3694 if ret == 0 { Ok(()) } else { Err(crate::ipc::i64_to_ipc_error(ret)) }
3695 }
3696
3697 /// Mint a **delegated resource** (§7.10, P2 file-as-capability): the kernel allocates a
3698 /// fresh resource owned by this service and a cap for it carrying `rights` (use the
3699 /// `RIGHT_*` bits), returning `(resource_id, cap)`. Requires the `RESOURCE_MINT` authority
3700 /// (held by `fs`). The `resource_id` is what the kernel badges into a later
3701 /// `resource_invoke`, so this service knows which resource a client is acting on (e.g. `fs`
3702 /// maps it → file). `None` if the authority is missing, the band is full, or the cap table
3703 /// is full. Syscall 30 = `ResourceMint`.
3704 pub fn resource_mint(&self, rights: u8) -> Option<(u64, CapHandle)> {
3705 let mut id: u64 = 0;
3706 // SAFETY: syscall(30) = ResourceMint; arg1 points at our own `id` for the kernel to
3707 // fill via write_user_bytes (validated kernel-side). Return is the cap slot or <0.
3708 let ret = unsafe { raw_syscall(30, rights as u64, &mut id as *mut u64 as u64, 0) };
3709 if ret < 0 { None } else { Some((id, CapHandle(ret as u32))) }
3710 }
3711
3712 /// Use a delegated resource cap (§7.10) - the "send" of file-as-capability. The kernel
3713 /// validates the cap holds `right` (a read needs `RIGHT_READ`, a write needs `RIGHT_WRITE`;
3714 /// a cap lacking it fails `CapInsufficientRights` - the non-escalation check), then routes
3715 /// `msg` to the owning service badged with the resource id + the validated right, embedding
3716 /// `reply` (a SEND|GRANT cap) so the owner can reply. `Ok(())` on delivery. Syscall 31.
3717 pub fn resource_invoke(&self, file: CapHandle, right: u8, reply: CapHandle, msg: &Message)
3718 -> Result<(), IpcError> {
3719 // Packed into ONE 32-BIT word: file[0..12] | reply[12..24] | right[24..32].
3720 //
3721 // A syscall argument is a single register, and on a 32-bit target (arm32's r1/r2/r3) that
3722 // register is 32 bits - so anything placed above bit 31 is silently truncated on the way in.
3723 // The original layout put `right` at bit 32, which arrived as 0 on ARM: the kernel then had
3724 // no right to validate, `fs` received a badge of 0, and every real operation failed `op <=
3725 // right` while a read-only cap invoked declaring WRITE sailed past the kernel check that was
3726 // supposed to stop it. Three symptoms, one truncated field.
3727 //
3728 // 12 bits per slot is 4095 against a MAX_CAPS_PER_TASK of 64 - room to grow by 60x - and
3729 // `right` is a u8, so the whole thing lands in 24 bits with 8 to spare. This is the A-U1
3730 // rule from arch/arm/CLAUDE.md: on a 32-bit ABI, a syscall argument that does not fit in one
3731 // register must be narrowed at the wrapper, never assumed to survive.
3732 // REJECT a handle that does not fit its field instead of letting it corrupt the next one.
3733 //
3734 // Each slot gets 12 bits. A handle above 4095 does not simply get truncated - its high bits
3735 // land in the NEXT field. The `fcap` forged-handle check passes 60000 (0xEA60) and the kernel
3736 // logged `file_slot=2656 reply_slot=14`: the 0xE spilled out of `file` and rewrote `reply`, so
3737 // a bad file handle silently redirected the REPLY to whatever cap happened to sit in slot 14.
3738 // The invocation was rejected for the right reason by luck, not by design.
3739 //
3740 // A slot above 4095 cannot be valid anyway (MAX_CAPS_PER_TASK is 64), so this rejects with the
3741 // same error the kernel gives for a slot it does not hold - the caller sees no difference,
3742 // and no neighbouring field is ever silently rewritten.
3743 const SLOT_MAX: u32 = 0xFFF;
3744 if file.0 > SLOT_MAX || reply.0 > SLOT_MAX {
3745 return Err(crate::ipc::i64_to_ipc_error(-2)); // -2 = capability not held
3746 }
3747 let packed = ((right as u64) << 24) | ((reply.0 as u64) << 12) | (file.0 as u64);
3748
3749 let payload = msg.payload_bytes();
3750 // SAFETY: syscall(31) = ResourceInvoke; packed + payload are user values the kernel
3751 // validates (cap slots, rights, generation, and the message bounds) before acting.
3752 let ret = unsafe {
3753 raw_syscall(31, packed, payload.as_ptr() as u64, payload.len() as u64)
3754 };
3755 if ret == 0 { Ok(()) } else { Err(crate::ipc::i64_to_ipc_error(ret)) }
3756 }
3757
3758 /// Read (and clear) the delegated-resource badge of the message just `recv`'d (§7.10). A
3759 /// service that owns delegated resources (e.g. `fs`) calls this right after `recv`: `Some((
3760 /// resource_id, right))` means the message was a **kernel-validated** invocation of a real
3761 /// cap on `resource_id` with `right` already checked (the owner enforces op ≤ `right`); `None`
3762 /// means an ordinary message (no badge - handle it on the name-addressed path). The badge
3763 /// cannot be forged over a plain `send`, so its presence is trustworthy. Syscall 33.
3764 pub fn last_recv_badge(&self) -> Option<(u64, u8)> {
3765 // SAFETY: syscall(33) = LastRecvBadge; reads+clears this task's stored badge.
3766 let packed = unsafe { raw_syscall(33, 0, 0, 0) } as u64;
3767 if packed == 0 {
3768 None
3769 } else {
3770 Some((packed & 0xFFFF_FFFF, ((packed >> 32) & 0xFF) as u8))
3771 }
3772 }
3773
3774 /// Revoke a delegated resource this service owns (§7.10): bumps its generation so every
3775 /// outstanding cap to it goes stale (clients see `CapRevoked`/`EndpointDead` on next use).
3776 /// Owner-gated by the kernel (ownership is the check). `true` on success. Syscall 32.
3777 #[must_use = "the capability is STILL VALID if this is false"]
3778 pub fn resource_revoke(&self, resource_id: u64) -> bool {
3779 // SAFETY: syscall(32) = ResourceRevoke; the kernel checks this task owns the resource.
3780 unsafe { raw_syscall(32, resource_id, 0, 0) == 0 }
3781 }
3782
3783 /// Inject one byte into the console input ring (syscall 20). Only effective
3784 /// for an input-driver service holding a CONSOLE_PUSH cap (the USB keyboard
3785 /// driver, §12); the byte reaches the shell exactly like a serial keystroke.
3786 /// No-op for services without the cap.
3787 pub fn console_push(&self, byte: u8) {
3788 let slot = Self::ctx().console_push_slot;
3789 if slot == u32::MAX {
3790 return;
3791 }
3792 // SAFETY: syscall(20) = ConsolePush; slot is the kernel-written cap index.
3793 let _ = unsafe { raw_syscall(20, slot as u64, byte as u64, 0) };
3794 }
3795
3796 /// Block until one byte is available on COM1 console input (syscall 17).
3797 ///
3798 /// Returns the byte value. Only usable by a service spawned with a CONSOLE_READ cap
3799 /// (`SPAWN_FLAG_REQ_CONSOLE` in its spawn request: the shell, `chaos`, `observe-live`). A kernel error
3800 /// is returned as byte 0, and a missing cap or corrupt context parks the caller in a busy loop.
3801 pub fn console_read(&self) -> u8 {
3802 let data = Self::ctx();
3803 // A wrong magic means the kernel handed us a corrupt ServiceContext - `self.log()` reads the
3804 // same corrupt ctx, so it cannot be trusted to report; parking is the service-level analog of
3805 // the kernel's halt-on-corrupt-state (§6.2). The slot guard below CAN speak (audit L8).
3806 if data.magic != SERVICE_CTX_MAGIC { loop {} }
3807 let slot = data.console_read_slot;
3808 if slot == u32::MAX {
3809 // No CONSOLE_READ cap: a caller that isn't the shell reached a shell-only syscall. Say so
3810 // LOUDLY (inv12) rather than wedge silently, then park - the caller's contract is wrong.
3811 self.log("sdk: console_read called without a CONSOLE_READ cap - parking (contract error)");
3812 loop {}
3813 }
3814 // SAFETY: syscall(17) = ConsoleRead; slot is kernel-written cap index.
3815 let ret = unsafe { raw_syscall(17, slot as u64, 0, 0) };
3816 if ret >= 0 { ret as u8 } else { 0 }
3817 }
3818
3819 /// Non-blocking console read (syscall 24). Returns `Some(byte)` if a keystroke
3820 /// is waiting, `None` if the ring is empty. A foreground full-screen app polls
3821 /// this for `q`-to-quit between repaints instead of blocking in `console_read`.
3822 /// Requires the CONSOLE_READ cap (`SPAWN_FLAG_REQ_CONSOLE` in the spawn request).
3823 pub fn try_console_read(&self) -> Option<u8> {
3824 let data = Self::ctx();
3825 if data.magic != SERVICE_CTX_MAGIC { return None; }
3826 let slot = data.console_read_slot;
3827 if slot == u32::MAX { return None; }
3828 // SAFETY: syscall(24) = TryConsoleRead; slot is kernel-written cap index.
3829 // Returns 0..=255 (byte), 256 (empty), or negative (cap error).
3830 let ret = unsafe { raw_syscall(24, slot as u64, 0, 0) };
3831 if (0..=255).contains(&ret) { Some(ret as u8) } else { None }
3832 }
3833
3834 /// Enable (`true`) or disable (`false`) console keystroke echo (syscall 25).
3835 /// A foreground full-screen app disables echo while it owns the screen - so
3836 /// its raw key polls do not smear its frame - and re-enables it on exit.
3837 /// Requires the CONSOLE_READ cap.
3838 pub fn console_echo(&self, on: bool) {
3839 let data = Self::ctx();
3840 if data.magic != SERVICE_CTX_MAGIC { return; }
3841 let slot = data.console_read_slot;
3842 if slot == u32::MAX { return; }
3843 // SAFETY: syscall(25) = ConsoleEcho; slot is kernel-written cap index.
3844 let _ = unsafe { raw_syscall(25, slot as u64, on as u64, 0) };
3845 }
3846
3847 /// End boot-log mirroring to the framebuffer and clear the TV (syscall 26).
3848 /// The shell calls this once, on the first keystroke, so the user sees the
3849 /// boot sequence on the display and then gets a clean interactive console.
3850 /// Requires the CONSOLE_READ cap.
3851 pub fn console_boot_complete(&self) {
3852 let data = Self::ctx();
3853 if data.magic != SERVICE_CTX_MAGIC { return; }
3854 let slot = data.console_read_slot;
3855 if slot == u32::MAX { return; }
3856 // SAFETY: syscall(26) = ConsoleBootComplete; slot is kernel-written cap index.
3857 let _ = unsafe { raw_syscall(26, slot as u64, 0, 0) };
3858 }
3859
3860 /// Whether THIS task currently owns (or shares, when unclaimed) console input - i.e. its console
3861 /// reads return bytes. False when another task holds the foreground (syscall 40, e.g. `chaos`): a
3862 /// backgrounded task should then stay quiet (not draw, not read) and redraw its prompt only when
3863 /// this returns true again. InspectKernel query 13 (ungated, caller-specific).
3864 pub fn is_console_foreground(&self) -> bool {
3865 // SAFETY: syscall(13) = InspectKernel; query 13 = is-foreground for the caller.
3866 unsafe { raw_syscall(13, 13, 0, 0) != 0 }
3867 }
3868
3869 /// Claim exclusive console input (syscall 40, op = 1): after this, only THIS task's
3870 /// `try_console_read` returns bytes; every other task reads empty. The `chaos` service
3871 /// claims it for the duration of a run so a resurrected shell cannot swallow its
3872 /// `q`-to-quit. Pair with `release_console_foreground` on exit, after ensuring a live
3873 /// shell exists to hand the keyboard back to. Requires the CONSOLE_READ cap.
3874 pub fn claim_console_foreground(&self) {
3875 let data = Self::ctx();
3876 if data.magic != SERVICE_CTX_MAGIC { return; }
3877 let slot = data.console_read_slot;
3878 if slot == u32::MAX { return; }
3879 // SAFETY: syscall(40) = ConsoleForeground; op 1 = claim; slot is kernel-written cap index.
3880 let _ = unsafe { raw_syscall(40, slot as u64, 1, 0) };
3881 }
3882
3883 /// Release exclusive console input (syscall 40, op = 0) back to the unclaimed state, so
3884 /// any CONSOLE_READ holder (the shell) reads normally again. Idempotent.
3885 pub fn release_console_foreground(&self) {
3886 let data = Self::ctx();
3887 if data.magic != SERVICE_CTX_MAGIC { return; }
3888 let slot = data.console_read_slot;
3889 if slot == u32::MAX { return; }
3890 // SAFETY: syscall(40) = ConsoleForeground; op 0 = release; slot is kernel-written cap index.
3891 let _ = unsafe { raw_syscall(40, slot as u64, 0, 0) };
3892 }
3893
3894 /// Return the core this service was spawned on.
3895 pub fn core_id(&self) -> u32 { Self::ctx().core_id }
3896
3897 /// Safe MMIO handle for this service's xHCI controller, if one was granted
3898 /// (§12). The kernel mapped the controller's BAR into this driver's address
3899 /// space; the returned [`crate::Mmio`] reads/writes the uncached device
3900 /// registers directly. `None` for non-driver services.
3901 pub fn xhci_mmio(&self) -> Option<crate::mmio::Mmio> {
3902 let va = Self::ctx().xhci_mmio_va;
3903 if va == 0 {
3904 None
3905 } else {
3906 Some(crate::mmio::Mmio::new(va as *mut u8, Self::ctx().xhci_mmio_len as usize))
3907 }
3908 }
3909
3910 /// Safe MMIO handle for this service's EHCI controller, if one was granted
3911 /// (§12). Reads the same kernel-mapped controller-BAR window as
3912 /// [`xhci_mmio`](Self::xhci_mmio) - a driver service holds exactly one
3913 /// controller, so the field is shared and unambiguous. `None` for non-drivers.
3914 pub fn ehci_mmio(&self) -> Option<crate::mmio::Mmio> {
3915 let va = Self::ctx().xhci_mmio_va;
3916 if va == 0 {
3917 None
3918 } else {
3919 Some(crate::mmio::Mmio::new(va as *mut u8, Self::ctx().xhci_mmio_len as usize))
3920 }
3921 }
3922
3923 /// The framebuffer the kernel granted this service, or `None` if it holds no grant.
3924 ///
3925 /// Held only by `console`, which renders the terminal into it (`docs/console-service.md` §9). The
3926 /// grant carries PIXEL geometry only - character rows and columns are the terminal's own business,
3927 /// and the kernel deliberately does not compute or publish them (Commandment III).
3928 pub fn framebuffer(&self) -> Option<crate::mmio::Framebuffer> {
3929 let d = Self::ctx();
3930 if d.fb_va == 0 || d.fb_len == 0 {
3931 return None;
3932 }
3933 let sh = d.fb_shifts;
3934 Some(crate::mmio::Framebuffer::new(
3935 d.fb_va as *mut u8,
3936 d.fb_len as usize,
3937 d.fb_pitch as usize,
3938 d.fb_bpp as usize,
3939 d.fb_width as usize,
3940 d.fb_height as usize,
3941 sh & 0xFF,
3942 (sh >> 8) & 0xFF,
3943 (sh >> 16) & 0xFF,
3944 ))
3945 }
3946
3947 /// Safe MMIO handle to this service's device register window, if one was
3948 /// granted (§12) - the neutrally-named accessor for non-USB drivers (e.g. the
3949 /// AHCI `block-driver`, which maps its HBA ABAR here). Same kernel-mapped
3950 /// window as [`xhci_mmio`](Self::xhci_mmio). `None` for non-driver services.
3951 pub fn mmio(&self) -> Option<crate::mmio::Mmio> {
3952 let va = Self::ctx().xhci_mmio_va;
3953 if va == 0 {
3954 None
3955 } else {
3956 Some(crate::mmio::Mmio::new(va as *mut u8, Self::ctx().xhci_mmio_len as usize))
3957 }
3958 }
3959
3960 /// Safe handle to this service's DMA arena, if one was granted (§12). The
3961 /// kernel mapped a physically-contiguous region into this driver; the
3962 /// returned [`crate::Dma`] gives the CPU view (read/write) and the physical
3963 /// base to program into the controller. `None` for non-driver services.
3964 pub fn dma_region(&self) -> Option<crate::dma::Dma> {
3965 let d = Self::ctx();
3966 if d.xhci_dma_va == 0 {
3967 None
3968 } else {
3969 Some(crate::dma::Dma::new(
3970 d.xhci_dma_va as *mut u8,
3971 d.xhci_dma_phys,
3972 d.xhci_dma_len as usize,
3973 ))
3974 }
3975 }
3976
3977
3978 /// The interrupt vector the kernel granted this service, if any.
3979 ///
3980 /// A driver must NOT hardcode this. It used to be safe to: every vector was a kernel constant, so
3981 /// both sides knew the number statically. The MSI pool allocates one at spawn instead, and a
3982 /// driver holding a stale copy does not fail loudly - it keeps working (the interrupt still wakes
3983 /// it) while reporting every interrupt as an unknown message. That is exactly what `services/xhci`
3984 /// did on the T630: `0 MSI, 7799 msg`.
3985 ///
3986 /// Use it to recognise an interrupt notification: the kernel's wake payload is the vector number.
3987 ///
3988 /// ```ignore
3989 /// let irq = ctx.irq_vector();
3990 /// let is_irq = irq.is_some_and(|v| msg.payload_bytes() == [v]);
3991 /// ```
3992 ///
3993 /// `None` means no interrupt was granted - a non-driver service, or a driver whose device offered
3994 /// neither MSI nor MSI-X. A driver that needs interrupts should say so rather than poll silently.
3995 pub fn irq_vector(&self) -> Option<u8> {
3996 let d = Self::ctx();
3997 if d.irq_count == 0 { None } else { Some(d.irqs[0]) }
3998 }
3999
4000 /// Every interrupt vector granted to this service (at most four).
4001 ///
4002 /// [`Self::irq_vector`] is the common case; this is for a driver routed more than one line.
4003 pub fn irq_vectors(&self) -> &'static [u8] {
4004 let d = Self::ctx();
4005 let n = core::cmp::min(d.irq_count as usize, d.irqs.len());
4006 &d.irqs[..n]
4007 }
4008
4009 /// Read one 32-bit PCI configuration register. Requires the `PCI_CFG` privilege.
4010 ///
4011 /// `sel` is a bus/device/function selector in the platform's own encoding, which is the CALLER'S
4012 /// knowledge - the kernel performs the access and enforces which registers may be reached, but
4013 /// never interprets what the selector names. `offset` is the register within that function.
4014 ///
4015 /// ONE call, not a select-then-read pair, because the underlying hardware register pair is
4016 /// stateful: split across two calls, two callers would each read whichever register the other
4017 /// selected, and the KERNEL uses the same pair on its spawn and kill paths. Atomic here, that
4018 /// window does not exist - and this interface cannot write anything at all.
4019 ///
4020 /// `None` if the capability is missing, or the access is one this machine will not admit (a bus
4021 /// its bridge does not forward, say - reading past it is an abort on some hardware rather than a
4022 /// harmless all-ones). Both are reported by the kernel too (invariant 12), so a caller need not
4023 /// guess which it was. Note the distinction from `Some(0xFFFF_FFFF)`, which is a SUCCESSFUL read
4024 /// of an absent device - the bus floats high, and that is data, not an error.
4025 pub fn pci_cfg_read(&self, sel: u32, offset: u16) -> Option<u32> {
4026 // SAFETY: a plain syscall; the kernel validates the capability and the access before any I/O.
4027 let r = unsafe { crate::syscall::raw_syscall(53, sel as u64, offset as u64, 0) };
4028 if r < 0 { None } else { Some(r as u32) }
4029 }
4030
4031 /// Cut (`false`) or restore (`true`) the power of the device this service was granted a fixed
4032 /// peripheral window to (`DevicePower`, syscall 54). Needs `DEVICE_POWER`, which the kernel mints
4033 /// with the window wherever the machine can power the device; everywhere else, and for every
4034 /// service without such a window, this returns `false` and the kernel says why on the serial log.
4035 /// How long to hold the device off and how long to wait after restoring it are the caller's to know
4036 /// - they are properties of the device, not of the kernel (`docs/wifi.md` 47).
4037 pub fn device_power(&self, on: bool) -> bool {
4038 // SAFETY: a plain syscall; the kernel validates the capability before touching any pin.
4039 let r = unsafe { crate::syscall::raw_syscall(54, if on { 1 } else { 0 }, 0, 0) };
4040 r == 0
4041 }
4042
4043 /// Set the Arm cores to the platform's maximum (`true`) or minimum (`false`) clock, and return the rate
4044 /// they read back in Hz (`CpuClock`, syscall 55). Needs `CPU_CLOCK`, which only the `power` service is
4045 /// spawned with; everyone else asks `power` for a lease over IPC. `None` is a refusal or a machine with
4046 /// no control over its clock, and the kernel says which on the serial log.
4047 pub fn cpu_clock(&self, max: bool) -> Option<u32> {
4048 // SAFETY: a plain syscall; the kernel validates the capability before asking the firmware.
4049 let r = unsafe { crate::syscall::raw_syscall(55, if max { 1 } else { 0 }, 0, 0) };
4050 if r > 0 { Some(r as u32) } else { None }
4051 }
4052
4053 /// Turn the machine off NOW (`PowerOff`, syscall 56, op 0). Needs `POWER_OFF`, which the shell holds for the
4054 /// operator. With it, this does not return: the machine is off, or - where the hardware cannot cut its own
4055 /// power - every core is halted and the kernel's last line says it is safe to switch off. It returns only
4056 /// when refused, with the kernel's code; the kernel says why on the serial log.
4057 pub fn power_off(&self) -> i64 {
4058 self.power_off_call(0, 0)
4059 }
4060
4061 /// Set the machine's one pending power-off `secs` from now (1 ..= 24 h), held by the KERNEL so it survives
4062 /// every service (`docs/shutdown.md` 6). `0`: this is now the pending one. `1`: one due sooner was kept
4063 /// (earliest wins). `2`: a reboot is pending, and is kept. `-1`: out of range. `-2`: refused (no `POWER_OFF`).
4064 pub fn power_off_in(&self, secs: u64) -> i64 {
4065 self.power_off_call(1, secs)
4066 }
4067
4068 /// Cancel the pending power-off. `0`: there was one. `1`: none is pending. `2`: the pending one is a reboot,
4069 /// and is kept. `-2`: refused.
4070 pub fn power_off_cancel(&self) -> i64 {
4071 self.power_off_call(2, 0)
4072 }
4073
4074 /// Set the machine to reboot `secs` from now (1 ..= 24 h), on the same one kernel deadline as a power-off
4075 /// (`PowerOff` op 3). Needs `REBOOT`. The answers are `power_off_in`'s, with `2` meaning a power-off is
4076 /// pending. A reboot NOW is `reboot`.
4077 pub fn reboot_in(&self, secs: u64) -> i64 {
4078 self.power_off_call(3, secs)
4079 }
4080
4081 /// Cancel the pending reboot (`PowerOff` op 4). The answers are `power_off_cancel`'s, with `2` meaning the
4082 /// pending one is a power-off.
4083 pub fn reboot_cancel(&self) -> i64 {
4084 self.power_off_call(4, 0)
4085 }
4086
4087 fn power_off_call(&self, op: u64, secs: u64) -> i64 {
4088 // SAFETY: a plain syscall with two integers; the kernel validates the capability before acting.
4089 unsafe { crate::syscall::raw_syscall(56, op, secs, 0) }
4090 }
4091
4092 /// The machine's pending power action (InspectKernel query 28): whole seconds until it, rounded up, and
4093 /// whether it is a reboot - or `None` when none is pending. Readable by every task, deliberately: a pending
4094 /// shutdown or reboot must never be hidden.
4095 pub fn power_pending(&self) -> Option<PowerPending> {
4096 // SAFETY: syscall(13) = InspectKernel; query 28 takes no pointer and is ungated.
4097 let r = unsafe { raw_syscall(13, 28, 0, 0) };
4098 if r >= 0 { Some(PowerPending { secs: r as u64 / 2, reboot: r & 1 == 1 }) } else { None }
4099 }
4100
4101 /// Whole seconds until the pending power-off, or `None` when none is pending - a pending reboot is not one.
4102 pub fn power_off_pending(&self) -> Option<u64> {
4103 self.power_pending().filter(|p| !p.reboot).map(|p| p.secs)
4104 }
4105
4106 /// Whole seconds until the pending reboot, or `None` when none is pending - a pending power-off is not one.
4107 pub fn reboot_pending(&self) -> Option<u64> {
4108 self.power_pending().filter(|p| p.reboot).map(|p| p.secs)
4109 }
4110
4111 /// Allocate `size` bytes of read/write memory within this task's budget.
4112 ///
4113 /// Returns the virtual address of the mapping on success, or `AllocError`
4114 /// if the allocation would exceed the contract memory limit (AllocDenied)
4115 /// or physical memory is exhausted.
4116 pub fn alloc_mem(&self, size: usize) -> Result<u64, AllocError> {
4117 // SAFETY: syscall(6) = AllocMem; no user pointers passed.
4118 let ret = unsafe { raw_syscall(6, size as u64, 0, 0) };
4119 if ret >= 0 {
4120 Ok(ret as u64)
4121 } else if ret == -11 {
4122 Err(AllocError::Denied)
4123 } else {
4124 Err(AllocError::Failed)
4125 }
4126 }
4127
4128 // `abort()` (the kernel `Abort`/9 syscall) was removed: it let any task panic the kernel, an
4129 // ungated §3.1 hole, and its only caller (`init`) is gone (Phase 5). A service that hits a fatal
4130 // error now simply dies (and is restarted by the supervisor) rather than aborting the kernel.
4131
4132 /// Trigger a hardware reset via the kernel reboot syscall (18). Does not return.
4133 ///
4134 /// The kernel prints `reboot: hardware reset` to serial before resetting, so the operator sees
4135 /// confirmation before the line goes silent. Requires REBOOT (held by the shell); a refusal is
4136 /// logged and the caller then yields forever.
4137 pub fn reboot(&self) -> ! {
4138 // SAFETY: syscall(18) = Reboot; no arguments.
4139 let rc = unsafe { raw_syscall(18, 0, 0, 0) };
4140 // A REFUSED reboot must not look like a successful one.
4141 //
4142 // The kernel gates this on the REBOOT capability (§3.1) and returns `CapNotHeld` to anyone
4143 // without it. This used to fall into a bare `loop {}`, so a caller that lacked the cap simply
4144 // stopped - no message, no reset, a frozen prompt. "Reboot does nothing" is exactly how that
4145 // reads from the outside, and it is indistinguishable from a reset that failed in hardware.
4146 //
4147 // Say which it was. The kernel's own reset path is bounded and reports if the SoC does not come
4148 // down; this covers the other half, where the syscall never got that far. Invariant 12: failures
4149 // are loud, never silent.
4150 self.log_fmt(format_args!(
4151 "reboot REFUSED by the kernel (rc {}) - this service does not hold the REBOOT capability", rc));
4152 loop {
4153 self.yield_cpu();
4154 }
4155 }
4156
4157 /// Attempt a reboot but RETURN the syscall result instead of assuming it never comes back.
4158 ///
4159 /// A successful reset does not return; a denial returns a negative error code (CapNotHeld = -2
4160 /// when the caller lacks the REBOOT capability, §3.1). For tests/probes that must *observe* the
4161 /// denial without resetting the machine - ordinary rebooters use `reboot()`.
4162 pub fn try_reboot(&self) -> i64 {
4163 // SAFETY: syscall(18) = Reboot; no arguments. On success it never returns; on denial it
4164 // returns the error code, which we hand back to the caller.
4165 unsafe { raw_syscall(18, 0, 0, 0) }
4166 }
4167
4168 /// End this program, with `status`: 0 for success, anything else for a failure it has already said.
4169 ///
4170 /// The kernel ends the caller and no one else - it takes no capability - and tells the supervisor how
4171 /// it ended (`death::EXITED` and the status), which is how a program that FINISHED is told apart from one
4172 /// that crashed. A service the supervisor keeps running is restarted all the same: exiting is for a
4173 /// program that does one job (CLAUDE.md 14.5 amendment).
4174 pub fn exit(&self, status: u32) -> ! {
4175 // SAFETY: syscall(57) = Exit; one integer, no pointer. It does not return.
4176 unsafe { raw_syscall(57, status as u64, 0, 0); }
4177 // Unreachable unless the kernel refused, which it never does for the caller itself: park rather than
4178 // return into code that believes it has ended.
4179 self.park()
4180 }
4181
4182 /// Advisory yield (§9.3).
4183 pub fn yield_cpu(&self) {
4184 // SAFETY: syscall(4) = Yield; always valid from ring-3.
4185 unsafe { raw_syscall(4, 0, 0, 0); }
4186 }
4187
4188 /// Park this task forever: block with no waker. For idle services that have
4189 /// no further work - far better than `loop { yield_cpu() }`,
4190 /// which keeps the core busy and prevents it from halting (so it never runs
4191 /// cool). Nothing wakes a parked task in v1; the loop re-parks defensively.
4192 pub fn park(&self) -> ! {
4193 loop {
4194 // SAFETY: syscall(21) = Park; blocks this task indefinitely.
4195 unsafe { raw_syscall(21, 0, 0, 0); }
4196 }
4197 }
4198
4199 /// Offer a queryable COPY of one log line to `events`. Never the log itself.
4200 ///
4201 /// Called only AFTER `log`'s syscall has already written the kernel ring and serial, so the
4202 /// authoritative record exists before this runs and does not depend on it in any way. A service
4203 /// with no `events` cap does nothing here; `events` itself holds no cap to itself, so its own
4204 /// logging cannot recurse into the sink.
4205 ///
4206 /// NO REACQUIRE ON FAILURE, deliberately, unlike `metric` and `trace_emit`. The cap slot is shared
4207 /// between all three, and those two already refresh it after a sink restart - `count_recv`
4208 /// publishes every 64 messages, so the slot heals on its own. Reacquiring from inside the LOGGING
4209 /// path is the one place it could bite: a reacquire that itself wanted to report something would
4210 /// re-enter here. Dropping a copy costs a line of queryable scrollback and nothing else, because
4211 /// serial already has it.
4212 #[inline]
4213 fn log_copy(&self, text: &[u8]) {
4214 let slot = crate::trace::sink_slot();
4215 if slot == u32::MAX {
4216 return;
4217 }
4218 let mut buf = [0u8; 1 + crate::trace::PEER_LEN + crate::trace::LOG_TEXT_MAX];
4219 let n = crate::trace::encode_log(text, &mut buf);
4220 let _ = self.try_send_by_handle(CapHandle(slot), &crate::ipc::Message::from_bytes(&buf[..n]));
4221 }
4222
4223 /// Log a string via the kernel ring buffer and serial (syscall 5, requires the log_write cap).
4224 /// Over 256 bytes it is cut on a character boundary and followed by ` [TRUNCATED]`. Without the
4225 /// cap it does nothing, and a kernel refusal is not reported.
4226 pub fn log(&self, msg: &str) {
4227 let data = Self::ctx();
4228 if data.magic != SERVICE_CTX_MAGIC { return; }
4229 let slot = data.log_write_slot;
4230 if slot == u32::MAX { return; }
4231
4232 let bytes = msg.as_bytes();
4233 let len = bytes.len();
4234 if len == 0 { return; }
4235
4236 // TOO LONG IS TRUNCATED, NEVER DROPPED.
4237 //
4238 // This used to be `if len == 0 || len > 256 { return; }` - a message over the limit was
4239 // discarded whole, with no truncation, no marker and no error. The reporting channel itself
4240 // failed silently, which makes every carefully-worded warning in the system conditional on
4241 // its own length.
4242 //
4243 // It was not hypothetical. The two longest literal log lines in the tree are both in `fs`,
4244 // and both are messages the constitution leans on: the durability-not-attested warning (280
4245 // bytes), which CLAUDE.md 6.1's backend-conditional TCB claim describes as "`fs` says so once
4246 // per mount", and the journal-recovery refusal (356 bytes), whose own comment argues it must
4247 // stay as visible as the fault it reports. Neither has ever been printed on this board, and
4248 // the durability one is latched before the call, so it never would be.
4249 //
4250 // Truncating loses the tail of a sentence. Dropping loses the fact. The marker says which
4251 // happened, so a reader is never left believing they saw the whole message.
4252 const LOG_MAX: usize = 256;
4253 if len > LOG_MAX {
4254 const MARK: &str = " [TRUNCATED]";
4255 let keep = LOG_MAX - MARK.len();
4256 // Cut on a CHARACTER boundary: `as_bytes` is UTF-8 and slicing mid-codepoint would hand
4257 // the kernel an invalid string. Walk back at most 3 bytes to the start of a codepoint.
4258 let mut end = keep;
4259 while end > 0 && (bytes[end] & 0xC0) == 0x80 {
4260 end -= 1;
4261 }
4262 // SAFETY: syscall(5) = Log; both slices are valid, in-bounds and within user space. Sent
4263 // as two calls rather than staged through a buffer, because a fixed staging buffer here
4264 // is exactly the kind of hidden limit this change exists to remove.
4265 unsafe {
4266 raw_syscall(5, slot as u64, bytes.as_ptr() as u64, end as u64);
4267 raw_syscall(5, slot as u64, MARK.as_ptr() as u64, MARK.len() as u64);
4268 }
4269 self.log_copy(&bytes[..end]);
4270 return;
4271 }
4272
4273 // SAFETY: syscall(5) = Log; bytes is a valid slice within user space.
4274 unsafe {
4275 raw_syscall(5, slot as u64, bytes.as_ptr() as u64, len as u64);
4276 }
4277 self.log_copy(bytes);
4278 }
4279
4280 /// Write a string to serial WITHOUT a trailing newline (syscall 22, requires the
4281 /// log_write cap). For inline output such as the shell prompt, where `log`'s newline
4282 /// would push the user's typed echo to the next line. A string longer than the kernel's 256-byte
4283 /// limit is written in pieces, each ending on a character boundary - it was dropped whole, with
4284 /// no marker, until 2026-10-10 (`backlog/fixed/80` S5).
4285 pub fn print(&self, msg: &str) {
4286 let data = Self::ctx();
4287 if data.magic != SERVICE_CTX_MAGIC { return; }
4288 let slot = data.log_write_slot;
4289 if slot == u32::MAX { return; }
4290 for piece in Pieces::new(msg, CONSOLE_WRITE_MAX) {
4291 // SAFETY: syscall(22) = Print; `piece` is a valid slice within user space, at most
4292 // CONSOLE_WRITE_MAX bytes.
4293 unsafe {
4294 raw_syscall(22, slot as u64, piece.as_ptr() as u64, piece.len() as u64);
4295 }
4296 }
4297 }
4298
4299 /// Log a formatted message, rendered into a 256-byte stack buffer. A message longer than that is
4300 /// cut on a character boundary and ends `...`, so the cut is visible (`backlog/fixed/80` S5: it was cut
4301 /// with no marker, and a cut through a character logged `(fmt error)` instead of the message).
4302 pub fn log_fmt(&self, args: core::fmt::Arguments) {
4303 let mut buf = [0u8; FMT_MAX];
4304 let s = render(&mut buf, args);
4305 if !s.is_empty() {
4306 self.log(s);
4307 }
4308 }
4309
4310 /// Write a string to the **interactive console** (serial + framebuffer),
4311 /// WITHOUT a trailing newline (syscall 23, requires log_write cap in Stage 1).
4312 /// This is the user-facing path: the shell prompt, command results, and
4313 /// `observe` frames. Unlike `log`/`print` (now serial-only), this also reaches
4314 /// the framebuffer/TV - the interactive surface (see docs/console-service.md).
4315 pub fn console_write(&self, msg: &str) {
4316 let data = Self::ctx();
4317 if data.magic != SERVICE_CTX_MAGIC { return; }
4318 let slot = data.log_write_slot;
4319 if slot == u32::MAX { return; }
4320
4321 // Longer than the kernel takes in one call: written in pieces on character boundaries, never
4322 // dropped (`backlog/fixed/80` S5).
4323 for piece in Pieces::new(msg, CONSOLE_WRITE_MAX) {
4324 // SAFETY: syscall(23) = ConsoleWrite; `piece` is a valid slice within user space, at
4325 // most CONSOLE_WRITE_MAX bytes.
4326 unsafe {
4327 raw_syscall(23, slot as u64, piece.as_ptr() as u64, piece.len() as u64);
4328 }
4329 }
4330 }
4331
4332 /// Write a string to the interactive console followed by a newline.
4333 pub fn console_writeln(&self, msg: &str) {
4334 self.console_write(msg);
4335 self.console_write("\n");
4336 }
4337
4338 /// Write a formatted message to the interactive console, with **no** trailing
4339 /// newline (e.g. a pager status line the cursor should park on).
4340 pub fn console_write_fmt(&self, args: core::fmt::Arguments) {
4341 let mut buf = [0u8; FMT_MAX];
4342 self.console_write(render(&mut buf, args));
4343 }
4344
4345 /// Write a formatted message to the interactive console, followed by a newline.
4346 pub fn console_writeln_fmt(&self, args: core::fmt::Arguments) {
4347 let mut buf = [0u8; FMT_MAX];
4348 self.console_write(render(&mut buf, args));
4349 self.console_write("\n");
4350 }
4351
4352 /// Write one console line. When `clear_eol` is true the line ends with
4353 /// `ESC[K` (erase to end of line) before the newline - so a full-screen app
4354 /// repainting in place (cursor homed each frame) overwrites a previous,
4355 /// longer line without leaving stale characters, and without a full-screen
4356 /// clear (no flicker). When false, behaves exactly like `console_writeln`.
4357 pub fn console_line(&self, clear_eol: bool, msg: &str) {
4358 self.console_write(msg);
4359 self.console_write(if clear_eol { "\x1b[K\n" } else { "\n" });
4360 }
4361
4362 /// Formatted variant of [`Self::console_line`].
4363 pub fn console_line_fmt(&self, clear_eol: bool, args: core::fmt::Arguments) {
4364 let mut buf = [0u8; FMT_MAX];
4365 self.console_write(render(&mut buf, args));
4366 self.console_write(if clear_eol { "\x1b[K\n" } else { "\n" });
4367 }
4368
4369 /// Spawn a service by name on the kernel-selected core.
4370 pub fn spawn(&self, name: &str) -> Result<(), crate::Error> {
4371 self.spawn_on(name, 0xFFFF)
4372 }
4373
4374 /// Spawn a service by name on `core` (0xFFFF = kernel round-robin).
4375 ///
4376 /// **Asks the SUPERVISOR when this service can reach it**, and the kernel otherwise.
4377 ///
4378 /// Step C moves service images out of the kernel (`docs/service-ownership.md`), and a service the
4379 /// kernel does not hold cannot be spawned by name through the kernel. Seven separate callers did
4380 /// exactly that - control's RESTART, the supervisor's own respawn, the shell's `spawn`, the
4381 /// shell's PIPES, `spawncap`, `spawnwired`, and `chaos` - and each was found by a regression
4382 /// rather than by reading. Routing HERE fixes all of them at once, including the ones not yet
4383 /// found.
4384 ///
4385 /// With no supervisor peer there is nothing to ask: the kernel's by-name spawn (syscall 7) was
4386 /// retired, because its catalogue holds `supervisor` alone and it could only refuse
4387 /// (`backlog/fixed/80` K14). So that case is refused here, plainly, rather than sent to a syscall that
4388 /// would refuse it less clearly.
4389 pub fn spawn_on(&self, name: &str, core: u32) -> Result<(), crate::Error> {
4390 if self.find_send_slot("supervisor").is_some() {
4391 // The supervisor hands back a cap to the new service's endpoint. This path does not keep
4392 // it, so it is released here - `map(|_| ())` dropped the handle and leaked one table slot per
4393 // spawn (`backlog/fixed/80`, found during H20).
4394 return self.spawn_via_supervisor(name, core, &[]).map(|cap| {
4395 if let Some(c) = cap { self.remove_cap(c); }
4396 });
4397 }
4398 let _ = (name, core);
4399 Err(crate::Error::NotFound)
4400 }
4401
4402 /// Ask the supervisor to spawn `name`, returning the `SEND|GRANT` cap it hands back (`None` if
4403 /// the service has no recv endpoint, or the cap could not be taken).
4404 ///
4405 /// The supervisor is RESTARTABLE (6.2), so a cached cap to it goes stale on every respawn. This
4406 /// reacquires by name and retries ONCE when the send NEVER LEFT - a stale cap - and never when it
4407 /// was delivered: not on `Ok(None)`, where the deadline passed, and not on `ReplyDead`, where the
4408 /// supervisor died holding it. Either may have started the service, and retrying would start it
4409 /// twice. (`ReplyDead` was retried until 2026-10-10, `backlog/fixed/80` S1.) A full queue is not retried
4410 /// either: the supervisor is alive and busy, and the caller decides whether to ask again.
4411 pub fn spawn_via_supervisor(&self, name: &str, core: u32, peers: &[&str])
4412 -> Result<Option<CapHandle>, crate::Error>
4413 {
4414 let mut buf = [0u8; supcmd::MAX];
4415 let n = supcmd::encode(&mut buf, supcmd::SPAWN, core, name, peers)
4416 .ok_or(crate::Error::InvalidArgument)?;
4417 let msg = crate::ipc::Message::from_bytes(&buf[..n]);
4418 for attempt in 0..2 {
4419 match self.request_with_reply_call_err("supervisor", &msg, 2) {
4420 Ok(Some(reply)) => {
4421 let ok = reply.payload_bytes().first() == Some(&supcmd::OK);
4422 let cap = self.take_pending_cap();
4423 if ok { return Ok(cap); }
4424 // Answered "no". Reclaim any cap that rode along, or it leaks a table slot.
4425 if let Some(c) = cap { self.remove_cap(c); }
4426 return Err(crate::Error::InvalidArgument);
4427 }
4428 Ok(None) => return Err(crate::Error::InvalidArgument),
4429 Err(crate::ipc::IpcError::ReplyDead) | Err(crate::ipc::IpcError::QueueFull) =>
4430 return Err(crate::Error::InvalidArgument),
4431 Err(_) if attempt == 0 => { let _ = self.reacquire_by_name("supervisor"); }
4432 Err(_) => return Err(crate::Error::InvalidArgument),
4433 }
4434 }
4435 Err(crate::Error::InvalidArgument)
4436 }
4437
4438 /// Spawn a task from an image THIS service supplies (`SpawnImage`, syscall 52).
4439 ///
4440 /// The kernel loads what it is handed instead of looking a name up in its own catalogue - the
4441 /// step that ends that catalogue (`docs/service-ownership.md`). Requires the SPAWN capability,
4442 /// exactly as `spawn` does: supplying the image is not authority to start it - AND the caller
4443 /// must hold `IMAGE_SPAWN`, which only the supervisor has and cannot delegate (CLAUDE.md 14.1).
4444 /// A name already running comes back as `AlreadyRunning`; every other refusal as `InvalidArgument`,
4445 /// and the kernel's serial log says which it was.
4446 ///
4447 /// `peers` are NUL-joined into a caller-owned buffer; each name still resolves through the
4448 /// kernel name directory, so this grants nothing a contract's `send_peers` would not.
4449 /// Returns a `SEND|GRANT` cap to the new service's recv endpoint, or `None` if it has none -
4450 /// the spawner needs it to record `name -> cap` and re-wire dependents after a restart.
4451 pub fn spawn_image(&self, req: &mut SpawnRequest, peers_buf: &mut [u8], peers: &[&str])
4452 -> Result<Option<CapHandle>, crate::Error>
4453 {
4454 let data = Self::ctx();
4455 if data.magic != SERVICE_CTX_MAGIC { return Err(crate::Error::InvalidArgument); }
4456 let slot = data.spawn_slot;
4457 if slot == u32::MAX { return Err(crate::Error::Cap(CapError::CapNotHeld)); }
4458
4459 let mut n = 0usize;
4460 for (i, p) in peers.iter().enumerate() {
4461 if i > 0 {
4462 if n >= peers_buf.len() { return Err(crate::Error::InvalidArgument); }
4463 peers_buf[n] = 0; n += 1;
4464 }
4465 let b = p.as_bytes();
4466 if n + b.len() > peers_buf.len() { return Err(crate::Error::InvalidArgument); }
4467 peers_buf[n..n + b.len()].copy_from_slice(b);
4468 n += b.len();
4469 }
4470 req.peers_ptr = if n > 0 { peers_buf.as_ptr() as u64 } else { 0 };
4471 req.peers_len = n as u32;
4472
4473 // SAFETY: syscall(52) = SpawnImage; `req` is a live, fully initialised SpawnRequest whose
4474 // layout matches the kernel's, and the kernel copies it once before reading any field.
4475 let ret = unsafe {
4476 raw_syscall(52, req as *const SpawnRequest as u64,
4477 core::mem::size_of::<SpawnRequest>() as u64, slot as u64)
4478 };
4479 // slot + 1, so 0 means "spawned, no recv endpoint" without colliding with slot 0.
4480 // -22 is the kernel's SPAWN_ALREADY_RUNNING.
4481 if ret == -22 { Err(crate::Error::AlreadyRunning) }
4482 else if ret < 0 { Err(crate::Error::InvalidArgument) }
4483 else if ret == 0 { Ok(None) }
4484 else { Ok(Some(CapHandle(ret as u32 - 1))) }
4485 }
4486
4487
4488 /// Kill a named service. Requires SERVICE_CONTROL (the kernel refuses with `CapNotHeld`
4489 /// otherwise); every failure is returned as `InvalidArgument`.
4490 pub fn kill(&self, name: &str) -> Result<(), crate::Error> {
4491 let bytes = name.as_bytes();
4492 // SAFETY: syscall(8) = Kill; bytes is a valid slice within user space.
4493 let ret = unsafe {
4494 raw_syscall(8, bytes.as_ptr() as u64, bytes.len() as u64, 0)
4495 };
4496 // The kernel says which failure it was, and so does this (`backlog/fixed/80` S4): -2 is the
4497 // `SERVICE_CONTROL` refusal, anything else is no running service of that name (or a name
4498 // the kernel could not read). Every one used to come back as `InvalidArgument`.
4499 match ret {
4500 0 => Ok(()),
4501 -2 => Err(crate::Error::Cap(CapError::CapNotHeld)),
4502 _ => Err(crate::Error::NotFound),
4503 }
4504 }
4505
4506 /// Kill then respawn a service with optional core override (§14.4).
4507 ///
4508 /// A service that is not running is simply started - that is `NotFound` from the kill, and the
4509 /// one kill failure that is passed over. A refusal (no `SERVICE_CONTROL`) is returned, and
4510 /// nothing is spawned: until 2026-10-10 the kill's result was discarded, so a caller without the
4511 /// authority to kill went on to spawn a second copy (`backlog/fixed/80` S4, CLAUDE.md 13.6).
4512 pub fn restart(&self, name: &str, core_override: Option<u32>) -> Result<(), crate::Error> {
4513 match self.kill(name) {
4514 Ok(()) | Err(crate::Error::NotFound) => {}
4515 Err(e) => return Err(e),
4516 }
4517 let core = core_override.unwrap_or(0xFFFF);
4518 self.spawn_on(name, core)
4519 }
4520
4521 // ---------------------------------------------------------------------------
4522 // Private helpers.
4523 // ---------------------------------------------------------------------------
4524
4525 /// Find the cap slot for a named send peer.
4526 ///
4527 /// Search order: dynamic cache (post-restart reacquisitions), then the
4528 /// kernel-written ServiceContextData send_peers array.
4529 fn find_send_slot(&self, peer: &str) -> Option<u32> {
4530 let bytes = peer.as_bytes();
4531 let len = bytes.len();
4532
4533 // 1. Dynamic cache (updated after EndpointDead + reacquire).
4534 // SAFETY: single-threaded service process.
4535 unsafe {
4536 for entry in SEND_CAP_CACHE.iter() {
4537 if entry.name_len != 0
4538 && entry.name_len as usize == len
4539 && &entry.name[..len] == bytes
4540 {
4541 return Some(entry.slot);
4542 }
4543 }
4544 }
4545
4546 // 2. ServiceContextData send_peers (wired at spawn).
4547 let data = Self::ctx();
4548 let count = (data.send_peer_count as usize).min(MAX_SEND_PEERS);
4549 for i in 0..count {
4550 let entry = &data.send_peers[i];
4551 if entry.slot == u32::MAX { continue; }
4552 let nlen = entry.name_len as usize;
4553 if nlen == len && &entry.name[..len] == bytes {
4554 return Some(entry.slot);
4555 }
4556 }
4557
4558 None
4559 }
4560}
4561
4562// ---------------------------------------------------------------------------
4563// Stack-based fmt::Write helper for log_fmt.
4564// ---------------------------------------------------------------------------
4565
4566struct StackWriter<'a> {
4567 buf: &'a mut [u8],
4568 pos: &'a mut usize,
4569 /// Set when something did not fit, so the result can say it was cut.
4570 cut: bool,
4571}
4572
4573impl<'a> core::fmt::Write for StackWriter<'a> {
4574 fn write_str(&mut self, s: &str) -> core::fmt::Result {
4575 let space = self.buf.len().saturating_sub(*self.pos);
4576 // Never split a character: what is kept is always valid UTF-8.
4577 let mut n = s.len().min(space);
4578 while n > 0 && !s.is_char_boundary(n) {
4579 n -= 1;
4580 }
4581 if n < s.len() {
4582 self.cut = true;
4583 }
4584 self.buf[*self.pos .. *self.pos + n].copy_from_slice(&s.as_bytes()[..n]);
4585 *self.pos += n;
4586 Ok(())
4587 }
4588}
4589
4590/// The most one formatted line renders to, on the stack.
4591const FMT_MAX: usize = 256;
4592/// The most the kernel takes in one `Print` or `ConsoleWrite` call.
4593const CONSOLE_WRITE_MAX: usize = 256;
4594
4595/// Render `args` into `buf`. A rendering that did not fit ends `...` in place of its last bytes, on a
4596/// character boundary, so a reader can see it was cut.
4597fn render<'b>(buf: &'b mut [u8; FMT_MAX], args: core::fmt::Arguments) -> &'b str {
4598 let mut pos = 0usize;
4599 let cut = {
4600 let mut w = StackWriter { buf: &mut buf[..], pos: &mut pos, cut: false };
4601 let _ = core::fmt::write(&mut w, args);
4602 w.cut
4603 };
4604 if cut {
4605 let mut end = FMT_MAX - 3;
4606 while end > 0 && core::str::from_utf8(&buf[..end]).is_err() {
4607 end -= 1;
4608 }
4609 buf[end..end + 3].copy_from_slice(b"...");
4610 pos = end + 3;
4611 }
4612 core::str::from_utf8(&buf[..pos]).unwrap_or("")
4613}
4614
4615/// A string in pieces of at most `max` bytes, each ending on a character boundary.
4616struct Pieces<'s> {
4617 rest: &'s str,
4618 max: usize,
4619}
4620
4621impl<'s> Pieces<'s> {
4622 fn new(s: &'s str, max: usize) -> Self {
4623 Pieces { rest: s, max }
4624 }
4625}
4626
4627impl<'s> Iterator for Pieces<'s> {
4628 type Item = &'s [u8];
4629 fn next(&mut self) -> Option<&'s [u8]> {
4630 if self.rest.is_empty() {
4631 return None;
4632 }
4633 let mut n = self.rest.len().min(self.max);
4634 while n > 0 && !self.rest.is_char_boundary(n) {
4635 n -= 1;
4636 }
4637 if n == 0 {
4638 return None;
4639 }
4640 let (head, tail) = self.rest.split_at(n);
4641 self.rest = tail;
4642 Some(head.as_bytes())
4643 }
4644}
4645
4646/// The seconds a `CallDeadline` is given for a caller's `max_secs`. Never 0, which the kernel reads
4647/// as NO deadline: a non-positive bound is the shortest real one, not the absence of one.
4648fn deadline_secs(max_secs: i64) -> u64 {
4649 if max_secs <= 0 { 1 } else { max_secs as u64 }
4650}
4651
4652// ---------------------------------------------------------------------------
4653// Placeholder types retained for compatibility.
4654// ---------------------------------------------------------------------------
4655
4656pub struct ServiceDescriptor;
4657impl ServiceDescriptor {
4658 pub fn name(&self) -> &str { todo!() }
4659}
4660
4661pub struct BootManifest;
4662impl BootManifest {
4663 pub fn services(&self) -> &[ServiceDescriptor] { todo!() }
4664}