A protocol is reached by name now, not by a compile-time integer. Init is PID 1 and already knows which binary it started, so init serves /protocol as a vfs backend: bind claims a contract with the provider's endpoint attached, open answers with that endpoint as the reply's capability, and readdir lists what is bound with the task and binary behind it. The kernel reserves the prefix — nothing may mount over it, under it, or unmount it — and ServiceId, ipc_register and ipc_lookup are gone, their syscall numbers left vacant. A bind is authorized by who the caller *is*: the kernel-stamped binary together with the supervising task's identity, matched against /system/configuration/protocol.csv. Identity, not spelling — spawn is ungated, so an attacker can run any bundled binary, and a name-only rule would have let it launder grants through an init of its own making. A name a live process holds is refused to everyone else; a dead one's is released. Three review rounds against a hostile ring-3 process found what 108 green tests could not, because the suite contains no attacker. Publishing init's supervision endpoint as the registry put PID 1's mailbox in every process's hands, where two forged bytes reached the shutdown path: privileged traffic is now believed only from the task that holds the contract it speaks for. A capability arriving on a request outlived every path that ignored it, one handle per call until the table was full — in init, and in the harness ten services share — so the arriving capability is owned by the turn and released unless a handler says otherwise. And the kernel let anyone holding an endpoint handle aim signals, timers, exit notices and interrupts at it: binding now requires having created it. Suite 108/108. The new protocol-registry case asserts eleven properties, each one an attack that must fail.
246 lines
11 KiB
Zig
246 lines
11 KiB
Zig
//! User-space IPC helpers over the kernel's synchronous IPC syscalls. A client
|
|
//! `call`s an endpoint (send + block for reply); the VFS server and drivers are
|
|
//! reached this way. The server side (`replyWait`, which returns two values) is
|
|
//! added with the first server binary.
|
|
|
|
const abi = @import("abi");
|
|
const sc = @import("system-call");
|
|
|
|
/// A small-int handle into the calling process's handle table.
|
|
pub const Handle = usize;
|
|
|
|
/// A fixed-size, register-friendly message payload. Server protocols (VFS, driver)
|
|
/// layer their own wire format on top of the bytes a call carries.
|
|
pub const Message = extern struct {
|
|
tag: u64 = 0,
|
|
a: u64 = 0,
|
|
b: u64 = 0,
|
|
c: u64 = 0,
|
|
};
|
|
|
|
/// Whether a system_call return value is a wrapped -errno (lands in the top page).
|
|
inline fn failed(r: usize) bool {
|
|
return r > ~@as(usize, 0) - 4095;
|
|
}
|
|
|
|
/// Create a new endpoint owned by this process; returns its handle.
|
|
pub fn createIpcEndpoint() ?Handle {
|
|
const r = sc.systemCall0(.create_ipc_endpoint);
|
|
return if (failed(r)) null else r;
|
|
}
|
|
|
|
// `register`/`lookup` lived here — the two wrappers over the flat ServiceId
|
|
// registry. Naming is not a system call any more: a provider binds its contract
|
|
// name at the registry and a client resolves and opens `/protocol/<name>`, both
|
|
// through `channel` (docs/os-development/protocol-namespace.md).
|
|
|
|
/// Drop a capability handle (endpoint, shared-memory, or DMA-region) and free its table
|
|
/// slot. A forwarding hop closes a cap it passed on; a binder closes a DMA-region cap
|
|
/// once the binding holds its own reference — the 32-slot table is otherwise consumed by
|
|
/// repeated cap-passing.
|
|
pub fn close(h: Handle) bool {
|
|
return !failed(sc.systemCall1(.handle_close, h));
|
|
}
|
|
|
|
pub const CallError = error{Failed};
|
|
|
|
/// The result of a capability-passing `callCap`: the reply length, and the handle of
|
|
/// an endpoint the server sent back (e.g. a per-device channel), or null.
|
|
pub const Reply = struct {
|
|
len: usize,
|
|
cap: ?Handle,
|
|
};
|
|
|
|
/// Send `message` to endpoint `h` and block until the server replies into `reply`,
|
|
/// optionally handing the server a capability (`send_cap`) and receiving one back.
|
|
/// This is the class-driver "open" primitive: call a bus with `send_cap = null`, get a
|
|
/// private per-device endpoint back in `.cap`. Two return values (reply length in rax,
|
|
/// received handle in r8) need a hand-written stub — r8 is read-write (in: reply
|
|
/// capacity, arg #4; out: the received handle).
|
|
pub fn callCap(h: Handle, message: []const u8, reply: []u8, send_cap: ?Handle) CallError!Reply {
|
|
var rax: usize = undefined;
|
|
var r8: usize = reply.len; // in: reply capacity (arg #4); out: received capability handle
|
|
asm volatile ("syscall"
|
|
: [rax] "={rax}" (rax),
|
|
[r8] "+{r8}" (r8),
|
|
: [n] "{rax}" (@intFromEnum(abi.SystemCall.ipc_call)),
|
|
[a0] "{rdi}" (h),
|
|
[a1] "{rsi}" (@intFromPtr(message.ptr)),
|
|
[a2] "{rdx}" (message.len),
|
|
[a3] "{r10}" (@intFromPtr(reply.ptr)),
|
|
[a5] "{r9}" (send_cap orelse abi.no_cap),
|
|
: .{ .rcx = true, .r11 = true, .memory = true });
|
|
if (failed(rax)) return error.Failed;
|
|
return .{ .len = rax, .cap = if (r8 == abi.no_cap) null else r8 };
|
|
}
|
|
|
|
/// Send `message` to endpoint `h` and block until the server replies into `reply`.
|
|
/// Returns the reply length. The common case: no capability passed either way.
|
|
pub fn call(h: Handle, message: []const u8, reply: []u8) CallError!usize {
|
|
return (try callCap(h, message, reply, null)).len;
|
|
}
|
|
|
|
/// Post `message` to endpoint `h`'s asynchronous queue and return immediately — no
|
|
/// rendezvous, no reply, no blocking. The receiver picks it up through `replyWait` as a
|
|
/// buffered message (`Received.isMessage`). Unlike `call`, this **cannot hang on a dead
|
|
/// or slow peer**, which is why a broadcaster (the input service) delivers events this
|
|
/// way. The payload must fit an endpoint slot (64 bytes); a full queue drops the oldest
|
|
/// message. Returns false on failure (bad handle, oversized payload, bad buffer).
|
|
pub fn send(h: Handle, message: []const u8) bool {
|
|
return !failed(sc.systemCall3(.ipc_send, h, @intFromPtr(message.ptr), message.len));
|
|
}
|
|
|
|
/// Set in `Received.badge` when what arrived is an asynchronous notification — a
|
|
/// bound device interrupt — rather than a client's message. The low bits carry the
|
|
/// GSI. See `isNotification`.
|
|
pub const notify_badge_bit: u64 = abi.notify_badge_bit;
|
|
|
|
/// Set alongside `notify_badge_bit` when the notification is a **signal** — the
|
|
/// lifecycle vocabulary of docs/process-lifecycle.md, delivered to the endpoint
|
|
/// nominated with `process.bindSignals`. Decode with `process.signalsFrom`.
|
|
pub const notify_signal_bit: u64 = abi.notify_signal_bit;
|
|
|
|
/// Set alongside `notify_badge_bit` when the notification is a **one-shot timer**
|
|
/// landing (`system.timerOnce`).
|
|
pub const notify_timer_bit: u64 = abi.notify_timer_bit;
|
|
|
|
/// Set alongside `notify_badge_bit` when the notification is a **child-exit
|
|
/// notice** — a process this one spawned (with an exit endpoint) has ended —
|
|
/// rather than a device interrupt. The low bits carry the child's process id.
|
|
pub const notify_exit_bit: u64 = abi.notify_exit_bit;
|
|
|
|
/// Set alongside `notify_badge_bit` when the wake-up is a **buffered message** — a payload
|
|
/// posted with `send` (`ipc_send`) — rather than a bare device interrupt or child-exit
|
|
/// notice. The payload is in the `replyWait` receive buffer (`Received.len` bytes); the
|
|
/// low bits of the badge carry the sender's task id. See `Received.isMessage`.
|
|
pub const notify_message_bit: u64 = abi.notify_message_bit;
|
|
|
|
/// The result of a `replyWait`: the request length, the sender's badge (a task id, or
|
|
/// an IRQ notification if the high bit is set), and any capability the request carried.
|
|
pub const Received = struct {
|
|
len: usize,
|
|
badge: u64,
|
|
cap: ?Handle,
|
|
|
|
/// True if this wake-up was an asynchronous notification (a device interrupt
|
|
/// or a child-exit notice), not a client request. An event loop branches on
|
|
/// this; there is no reply owed on the notification path.
|
|
pub fn isNotification(self: Received) bool {
|
|
return self.badge & notify_badge_bit != 0;
|
|
}
|
|
|
|
/// True if this wake-up tells of a supervised child's end — the notification
|
|
/// requested by passing an exit endpoint to `system.spawnSupervised`.
|
|
pub fn isChildExit(self: Received) bool {
|
|
return self.isNotification() and self.badge & notify_exit_bit != 0;
|
|
}
|
|
|
|
/// True if this wake-up is a **buffered message** posted with `send` (`ipc_send`):
|
|
/// there is a payload in the receive buffer (`self.len` bytes) and no reply is owed.
|
|
/// The subscriber side of a broadcast branches on this.
|
|
pub fn isMessage(self: Received) bool {
|
|
return self.isNotification() and self.badge & notify_message_bit != 0;
|
|
}
|
|
|
|
/// The task id of whoever posted a buffered message, meaningful only when
|
|
/// Whether this arrival is a signal notification — decode the set with
|
|
/// `process.signalsFrom(badge)`.
|
|
pub fn isSignal(self: Received) bool {
|
|
return self.isNotification() and self.badge & notify_signal_bit != 0;
|
|
}
|
|
|
|
/// Whether this arrival is a one-shot timer landing (`system.timerOnce`).
|
|
pub fn isTimer(self: Received) bool {
|
|
return self.isNotification() and self.badge & notify_timer_bit != 0;
|
|
}
|
|
|
|
/// `isMessage`. (The badge's low bits, with the three high marker bits masked off.)
|
|
pub fn senderTaskId(self: Received) u32 {
|
|
return @intCast(self.badge & ~(notify_badge_bit | notify_exit_bit | notify_message_bit));
|
|
}
|
|
|
|
/// The interrupt source (a GSI), meaningful only when `isNotification` and
|
|
/// not `isChildExit`.
|
|
pub fn source(self: Received) u64 {
|
|
return self.badge & ~notify_badge_bit;
|
|
}
|
|
|
|
/// The ended child's process id, meaningful only when `isChildExit`.
|
|
pub fn childProcessId(self: Received) u32 {
|
|
return @intCast(self.badge & ~(notify_badge_bit | notify_exit_bit));
|
|
}
|
|
};
|
|
|
|
/// A capability that arrived with one turn of a receive loop, and the ownership
|
|
/// rule for it: **the turn owns it until a handler takes it, and closes whatever
|
|
/// is left.**
|
|
///
|
|
/// The kernel installs a sent capability in the receiver's handle table whenever
|
|
/// the caller attached one, *independent of the message's length or kind*
|
|
/// (system/kernel/ipc-synchronous.zig `replyWait`), so every path out of a loop
|
|
/// has to dispose of one — including the paths that never look at the message.
|
|
/// The table is thirty-two slots, and `ipc_call` does not dedupe, so a client
|
|
/// looping on `callCap(server, &.{}, endpoint)` spends one slot per call: about
|
|
/// thirty-two zero-length pings and the service can never accept another
|
|
/// capability, which means no subscribe and no shared-memory handover, for the
|
|
/// rest of the boot. It is unauthenticated and it is two lines to write.
|
|
///
|
|
/// So ownership is structural rather than a close per branch — the per-branch
|
|
/// version has already failed twice in this tree, in PID 1's ping path and in
|
|
/// every `service.run` callback that simply ignored its capability argument.
|
|
/// Written this way, forgetting **closes**, and *keeping* a capability is the
|
|
/// thing a handler has to say out loud:
|
|
///
|
|
/// ```zig
|
|
/// var arrived: ipc.Arrival = .{ .handle = got.cap };
|
|
/// defer arrived.release(); // every exit path, including `continue`
|
|
/// ...
|
|
/// const kept = arrived.take().?; // claimed: mine to hold or close
|
|
/// ```
|
|
pub const Arrival = struct {
|
|
handle: ?Handle = null,
|
|
|
|
/// Look without claiming — a handler that may still refuse wants no close of
|
|
/// its own on the refusal paths.
|
|
pub fn peek(self: *const Arrival) ?Handle {
|
|
return self.handle;
|
|
}
|
|
|
|
/// Claim ownership: from here the capability is the taker's to keep or close,
|
|
/// and the turn will not touch it.
|
|
pub fn take(self: *Arrival) ?Handle {
|
|
defer self.handle = null;
|
|
return self.handle;
|
|
}
|
|
|
|
/// Close whatever nobody claimed. Idempotent, so it is safe as a `defer` next
|
|
/// to any number of `take`s.
|
|
pub fn release(self: *Arrival) void {
|
|
if (self.handle) |handle| _ = close(handle);
|
|
self.handle = null;
|
|
}
|
|
};
|
|
|
|
/// Server side of IPC_ReplyWait: deliver `reply` to the client last received (if any,
|
|
/// optionally handing it `send_cap`), then block until the next request arrives in
|
|
/// `receive`. Returns its length, the sender badge, and any capability the request
|
|
/// carried (in `.cap`). Three return values — length in rax, badge in rdx, received
|
|
/// handle in r8 — so it needs a hand-written stub: rdx is read-write (in: reply length,
|
|
/// arg #3; out: badge) and r8 is read-write (in: receive capacity, arg #4; out: handle).
|
|
pub fn replyWait(h: Handle, reply: []const u8, receive: []u8, send_cap: ?Handle) Received {
|
|
var rax: usize = undefined;
|
|
var rdx: usize = reply.len; // in: reply_len (arg #3); out: badge
|
|
var r8: usize = receive.len; // in: receive capacity (arg #4); out: received capability handle
|
|
asm volatile ("syscall"
|
|
: [rax] "={rax}" (rax),
|
|
[rdx] "+{rdx}" (rdx),
|
|
[r8] "+{r8}" (r8),
|
|
: [n] "{rax}" (@intFromEnum(abi.SystemCall.ipc_reply_wait)),
|
|
[a0] "{rdi}" (h),
|
|
[a1] "{rsi}" (@intFromPtr(reply.ptr)),
|
|
[a3] "{r10}" (@intFromPtr(receive.ptr)),
|
|
[a5] "{r9}" (send_cap orelse abi.no_cap),
|
|
: .{ .rcx = true, .r11 = true, .memory = true });
|
|
return .{ .len = rax, .badge = rdx, .cap = if (r8 == abi.no_cap) null else r8 };
|
|
}
|