461 lines
25 KiB
Zig
461 lines
25 KiB
Zig
//! protocol-registry-test — the registrar's own test fixture
|
||
//! (docs/os-development/protocol-namespace.md). One binary, four roles picked by
|
||
//! argv, driven by the `protocol-registry` kernel case:
|
||
//!
|
||
//! - `protocol-registry-test provider <mark>` — binds `/protocol/test/registry`
|
||
//! and answers every request with its one-byte `<mark>`, so a client can tell
|
||
//! *which instance* it reached.
|
||
//! - `protocol-registry-test deputy` / `... launder` — the two halves of the
|
||
//! laundering-deputy probe (assertion 7 below).
|
||
//! - `protocol-registry-test run` — the driver, and the assertions:
|
||
//! 1. a bind of a name this binary is not granted is refused, `-EPERM`;
|
||
//! 2. the kernel's reserved prefix holds: nothing may mount at or under
|
||
//! `/protocol`, and nobody may unmount it;
|
||
//! 3. a **forged power event** posted straight into the registry endpoint
|
||
//! does not shut the machine down — the registrar answers privileged
|
||
//! control traffic on the sender's kernel-stamped identity, never on two
|
||
//! bytes of payload. The system surviving to the next line is the
|
||
//! assertion, and it is the strongest one available: a regression takes
|
||
//! the whole boot down;
|
||
//! 4. zero-length calls carrying a capability do not consume PID 1's handle
|
||
//! table — after sixty-four of them a legitimate bind still succeeds;
|
||
//! 5. a bind colliding with a **live** owner is refused, `-EBUSY`;
|
||
//! 6. killing the provider makes the cached channel fail (`-EPEER`), and a
|
||
//! client that re-resolves reaches the **restarted** instance — the
|
||
//! restart story falling out of the naming layer, with no reconnect verb
|
||
//! anywhere;
|
||
//! 7. the laundering deputy is refused: the *same binary* under the *same
|
||
//! supervisor name* binds when its supervisor is a task the kernel
|
||
//! started, and is refused one hop further down, where the supervisor is
|
||
//! a task nobody authorized. The pair is the point — one alone would pass
|
||
//! for reasons that have nothing to do with the rule under test;
|
||
//! 8. a **forged terminate signal** does not reach PID 1 either: the kernel
|
||
//! refuses to bind this process's signal delivery to an endpoint it does
|
||
//! not own, so "bind init's endpoint, then signal yourself" — which needs
|
||
//! no forgery at all, only two ungated syscalls — cannot happen;
|
||
//! 9. the same for the other kernel notifications: a timer and an exit
|
||
//! subscription may only be armed on one's own endpoint;
|
||
//! 10. the handle-table storm of assertion 4, aimed at a **harness-run**
|
||
//! service instead of PID 1 — the loop every other service in the system
|
||
//! runs — after which a capability-passing operation still works.
|
||
//!
|
||
//! Assertions 8–10 each pair a refusal with a control (the same operation on an
|
||
//! endpoint this process created, which must succeed), because a refusal alone
|
||
//! would pass just as happily against a kernel that refused everything.
|
||
//!
|
||
//! Prints `protocol-registry: ok` on success, or a `protocol-registry: FAIL`
|
||
//! line naming the step. Spawned bare (the initial-ramdisk sweep starts every
|
||
//! bundled binary), it exits silently so it cannot derange other tests.
|
||
|
||
const std = @import("std");
|
||
const channel = @import("channel");
|
||
const envelope = @import("envelope");
|
||
const file_system = @import("file-system");
|
||
const ipc = @import("ipc");
|
||
const logging = @import("logging");
|
||
const power_protocol = @import("power-protocol");
|
||
const process = @import("process");
|
||
const service = @import("service");
|
||
const time = @import("time");
|
||
|
||
/// The contract this fixture's provider role claims — under `/protocol/test`,
|
||
/// the subtree every `/test/` binary is granted.
|
||
const contract = "test/registry";
|
||
|
||
/// Where the deputy and the laundered grandchild report their bind verdicts. The
|
||
/// driver binds it and listens; both children reach it by name like any client.
|
||
const verdict_contract = "test/verdict";
|
||
|
||
/// The name the deputy claims (it may — its supervisor is the kernel-started
|
||
/// driver) and the one its own child attempts (it may not — its supervisor is
|
||
/// the deputy, a task neither init nor the kernel started).
|
||
const deputy_contract = "test/deputy";
|
||
const laundered_contract = "test/laundered";
|
||
|
||
/// A contract this fixture is deliberately NOT granted. It belongs to the
|
||
/// display service, and no manifest row names this binary against it.
|
||
const forbidden = "display";
|
||
|
||
fn fail(step: []const u8) noreturn {
|
||
_ = logging.write("protocol-registry: FAIL ");
|
||
_ = logging.write(step);
|
||
_ = logging.write("\n");
|
||
process.exit(1);
|
||
}
|
||
|
||
// --- the provider role ------------------------------------------------------
|
||
|
||
/// Which instance this is, echoed to every caller. The point of the whole
|
||
/// exercise: a client cannot tell instances apart by the name it opened, so the
|
||
/// provider says so itself.
|
||
var mark: u8 = '?';
|
||
|
||
/// The provider's one operation, plus `handover_operation` — the capability-passing
|
||
/// verb the harness-exhaustion probe needs. Claiming the arriving capability with
|
||
/// `take` and *using* it is the point: it proves the harness handed over a working
|
||
/// handle, which a service whose table is full could not have been given.
|
||
fn answer(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
||
_ = sender;
|
||
if (message.len != 0 and message[0] == handover_operation) {
|
||
if (arrived.take()) |handed| {
|
||
_ = ipc.send(handed, &[_]u8{mark}); // speak on it, then it is ours to close
|
||
_ = ipc.close(handed);
|
||
}
|
||
}
|
||
if (reply.len == 0) return 0;
|
||
reply[0] = mark;
|
||
return 1;
|
||
}
|
||
|
||
/// The one request byte the provider treats as "here is an endpoint, use it".
|
||
const handover_operation: u8 = 's';
|
||
|
||
// --- the laundering-deputy roles --------------------------------------------
|
||
|
||
/// One child's bind verdict, posted to the driver over `test/verdict`. Sent
|
||
/// asynchronously (no reply owed) so a child can report and exit without the
|
||
/// driver having to be waiting at that instant.
|
||
const Verdict = extern struct {
|
||
/// `deputy` or `launder` — which of the two spoke.
|
||
role: u8,
|
||
_reserved: [3]u8 = .{0} ** 3,
|
||
/// The registry's answer: 0 bound, or a negative errno.
|
||
status: i32,
|
||
};
|
||
|
||
const role_deputy: u8 = 'd';
|
||
const role_launder: u8 = 'l';
|
||
|
||
/// Report a verdict to the driver, best-effort: the driver has a deadline, and a
|
||
/// child that cannot reach it must not hang the boot.
|
||
fn report(role: u8, status: i32) void {
|
||
const verdict = Verdict{ .role = role, .status = status };
|
||
var attempts: u32 = 0;
|
||
while (attempts < 200) : (attempts += 1) {
|
||
if (channel.openEndpoint(verdict_contract)) |endpoint| {
|
||
defer _ = ipc.close(endpoint);
|
||
if (ipc.send(endpoint, std.mem.asBytes(&verdict))) return;
|
||
}
|
||
time.sleepMillis(20);
|
||
}
|
||
}
|
||
|
||
/// The middle of the laundered chain: a granted binary whose own supervisor is
|
||
/// the kernel-started driver. It binds — that is the control, and it is what
|
||
/// makes the refusal below mean something — then starts an instance of *itself*
|
||
/// and stays alive while that instance asks for a name, so the registry judges a
|
||
/// live, fully attestable chain rather than an orphan.
|
||
fn deputy() void {
|
||
const spare = ipc.createIpcEndpoint() orelse return;
|
||
report(role_deputy, verdictOf(deputy_contract, spare));
|
||
|
||
const exits = ipc.createIpcEndpoint() orelse return;
|
||
const child = process.spawnSupervised("protocol-registry-test", &.{"launder"}, exits) orelse return;
|
||
awaitExit(exits, child);
|
||
}
|
||
|
||
/// The laundered grandchild. Its binary is granted, and its supervisor's binary
|
||
/// is granted as a supervisor — every *name* in the row matches. The one thing
|
||
/// that does not is which task its supervisor is: nobody authorized this deputy,
|
||
/// so nothing it spawns inherits a name.
|
||
fn launder() void {
|
||
const spare = ipc.createIpcEndpoint() orelse return;
|
||
report(role_launder, verdictOf(laundered_contract, spare));
|
||
}
|
||
|
||
// --- the driver role --------------------------------------------------------
|
||
|
||
/// Ask whoever is on the other end of `endpoint` which instance they are. Null
|
||
/// if the call failed — which, after a provider dies, is `-EPEER`.
|
||
fn instanceOf(endpoint: ipc.Handle) ?u8 {
|
||
var reply: [8]u8 = undefined;
|
||
const n = ipc.call(endpoint, "?", &reply) catch return null;
|
||
if (n != 1) return null;
|
||
return reply[0];
|
||
}
|
||
|
||
/// Open `/protocol/<contract>` until the instance marked `wanted` answers,
|
||
/// re-resolving each time — exactly the recovery a client performs when its
|
||
/// provider dies. Returns the live endpoint. Handles from a failed attempt are
|
||
/// closed: a 32-slot table does not survive four hundred leaks.
|
||
fn reach(wanted: u8) ?ipc.Handle {
|
||
var attempts: u32 = 0;
|
||
while (attempts < 400) : (attempts += 1) {
|
||
if (channel.openEndpoint(contract)) |endpoint| {
|
||
if (instanceOf(endpoint)) |seen| {
|
||
if (seen == wanted) return endpoint;
|
||
}
|
||
_ = ipc.close(endpoint);
|
||
}
|
||
time.sleepMillis(20);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// Block until child `id`'s exit notification lands on `endpoint`.
|
||
fn awaitExit(endpoint: ipc.Handle, id: u32) void {
|
||
var scratch: [8]u8 = undefined;
|
||
var guard: u32 = 0;
|
||
while (guard < 1000) : (guard += 1) {
|
||
const got = ipc.replyWait(endpoint, scratch[0..0], &scratch, null);
|
||
if (got.isChildExit() and got.childProcessId() == id) return;
|
||
}
|
||
fail("the provider's exit notification never arrived");
|
||
}
|
||
|
||
/// One bind, retried only while the registry is *unreachable* — a registry that
|
||
/// answered has decided. Null means it never answered, which the caller reports
|
||
/// in its own words: "not mounted yet" and "stopped serving mid-test" are the
|
||
/// same silence and very different findings.
|
||
fn verdictWithin(name: []const u8, endpoint: ipc.Handle) ?i32 {
|
||
var attempts: u32 = 0;
|
||
while (attempts < 200) : (attempts += 1) {
|
||
if (channel.bind(name, endpoint)) |status| return status;
|
||
time.sleepMillis(20);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// The registry answers, whatever it answers — the first bind also waits out a
|
||
/// registry that has not mounted `/protocol` yet.
|
||
fn verdictOf(name: []const u8, endpoint: ipc.Handle) i32 {
|
||
return verdictWithin(name, endpoint) orelse fail("the registry never came up");
|
||
}
|
||
|
||
/// The registry's endpoint, obtained the way every process obtains it: resolve
|
||
/// `/protocol`. `fs_resolve` installs the backend's capability in ANY caller's
|
||
/// table, and the registrar's backend endpoint is PID 1's *supervision* endpoint
|
||
/// — the same mailbox its signals, timers, child deaths and power events land in.
|
||
/// That is precisely why what arrives there may not be believed on its content.
|
||
fn registryEndpoint() ?ipc.Handle {
|
||
var relative: [channel.path_maximum]u8 = undefined;
|
||
const route = file_system.fsResolve(channel.root, 0, &relative) orelse return null;
|
||
return switch (route) {
|
||
.kernel => null,
|
||
.backend => |backend| backend.handle,
|
||
};
|
||
}
|
||
|
||
/// Wait for both children of the laundering probe to report. Taken together
|
||
/// rather than one at a time because the two are independent processes and the
|
||
/// order they reach the driver is not the driver's business.
|
||
fn awaitVerdicts(endpoint: ipc.Handle) struct { deputy: i32, launder: i32 } {
|
||
var scratch: [64]u8 = undefined;
|
||
var from_deputy: ?i32 = null;
|
||
var from_launder: ?i32 = null;
|
||
var guard: u32 = 0;
|
||
while (guard < 2000) : (guard += 1) {
|
||
const got = ipc.replyWait(endpoint, scratch[0..0], &scratch, null);
|
||
if (got.isMessage() and got.len >= @sizeOf(Verdict)) {
|
||
const verdict = std.mem.bytesToValue(Verdict, scratch[0..@sizeOf(Verdict)]);
|
||
if (verdict.role == role_deputy) from_deputy = verdict.status;
|
||
if (verdict.role == role_launder) from_launder = verdict.status;
|
||
}
|
||
if (from_deputy) |d| {
|
||
if (from_launder) |l| return .{ .deputy = d, .launder = l };
|
||
}
|
||
}
|
||
fail("the deputy chain never reported its bind verdicts");
|
||
}
|
||
|
||
fn run() void {
|
||
// A spare endpoint to offer the registry. Every bind below is meant to be
|
||
// refused, so it is never actually claimed by anyone.
|
||
const spare = ipc.createIpcEndpoint() orelse fail("create an endpoint to offer");
|
||
|
||
// 1. A name this binary is not granted. Refusal is `-EPERM` at the bind, not
|
||
// silence: the caller *is* the provider, and telling it its manifest is
|
||
// wrong is not a leak.
|
||
const ungranted = verdictOf(forbidden, spare);
|
||
if (ungranted != -envelope.EPERM) fail("an ungranted bind was not refused with -EPERM");
|
||
_ = logging.write("protocol-registry: ungranted bind refused\n");
|
||
|
||
// 2. The kernel's residual rule: `/protocol` is claimed once, by PID 1, and
|
||
// the prefix is closed for the boot. Shadowing one contract by mounting
|
||
// under it is refused too, and so is deleting the namespace outright.
|
||
if (file_system.mount("/protocol", spare)) fail("a second mount at /protocol was allowed");
|
||
if (file_system.mount("/protocol/display", spare)) fail("a mount under /protocol was allowed");
|
||
if (file_system.fsUnmount("/protocol")) fail("unmounting /protocol was allowed");
|
||
_ = logging.write("protocol-registry: /protocol reserved\n");
|
||
|
||
// 3. A forged power event. Resolving `/protocol` hands this process — any
|
||
// process — a sendable handle to PID 1's mailbox, and the bytes below are
|
||
// byte-for-byte what the real power service publishes when the button is
|
||
// pressed. If the registrar believed payloads, this single `send` would
|
||
// run the whole stop sequence and park PID 1 in its final sleep: every
|
||
// service terminated, the registry gone for the rest of the boot, and no
|
||
// way back. So the assertion is that the machine is still here afterwards
|
||
// — the strongest one available, because a regression does not fail this
|
||
// line, it takes the entire boot down with it.
|
||
const registry = registryEndpoint() orelse fail("resolve /protocol");
|
||
const forged = power_protocol.EventMessage{ .event = @intFromEnum(power_protocol.Event.power_button) };
|
||
if (!ipc.send(registry, std.mem.asBytes(&forged))) fail("post a forged power event");
|
||
const still_serving = verdictWithin(forbidden, spare) orelse
|
||
fail("the registrar went silent after a forged power event — it acted on it");
|
||
if (still_serving != -envelope.EPERM) fail("the registry misanswered after a forged power event");
|
||
_ = logging.write("protocol-registry: forged power event ignored\n");
|
||
|
||
// 4. Sixty-four zero-length calls, each carrying a capability. The kernel
|
||
// installs a sent capability in the receiver's table whatever the message
|
||
// length, so every one of these hands PID 1 a handle — and PID 1 has
|
||
// thirty-two slots. A registrar whose ping path returns without closing
|
||
// runs out before this loop is half done, after which nothing can ever
|
||
// bind again and the machine cannot recover for the rest of the boot.
|
||
var storm: u32 = 0;
|
||
while (storm < 64) : (storm += 1) {
|
||
var scratch: [8]u8 = undefined;
|
||
_ = ipc.callCap(registry, "", &scratch, spare) catch {};
|
||
}
|
||
// The proof is a legitimate bind landing afterwards. This one doubles as
|
||
// the driver's listening post for step 7.
|
||
const verdicts = ipc.createIpcEndpoint() orelse fail("create the verdict endpoint");
|
||
const after_storm = verdictWithin(verdict_contract, verdicts) orelse
|
||
fail("the registrar went silent after capability-carrying pings");
|
||
if (after_storm != 0) fail("a legitimate bind failed after capability-carrying pings");
|
||
_ = logging.write("protocol-registry: capability-carrying pings did not exhaust the registrar\n");
|
||
|
||
// 5. A live owner's name is not for the taking. Collision is an error, never
|
||
// last-writer-wins — otherwise any process could impersonate any service.
|
||
const exits = ipc.createIpcEndpoint() orelse fail("create the exit endpoint");
|
||
const first = process.spawnSupervised("protocol-registry-test", &.{ "provider", "1" }, exits) orelse
|
||
fail("spawn the first provider");
|
||
const one = reach('1') orelse fail("the first provider never bound its contract");
|
||
const collision = verdictOf(contract, spare);
|
||
if (collision != -envelope.EBUSY) fail("a collision with a live owner was not refused with -EBUSY");
|
||
_ = logging.write("protocol-registry: collision refused\n");
|
||
|
||
// 6. The restart story. Kill the provider: the cached channel dies with it
|
||
// (the kernel fails calls on a dead endpoint rather than blocking
|
||
// forever), and re-resolving the same name reaches whatever instance the
|
||
// registry now points at. No reconnect verb, no client-side repair.
|
||
if (!process.kill(first)) fail("kill the first provider");
|
||
awaitExit(exits, first);
|
||
if (instanceOf(one) != null) fail("a call on a dead provider's channel succeeded");
|
||
_ = logging.write("protocol-registry: dead channel refused\n");
|
||
|
||
_ = process.spawnSupervised("protocol-registry-test", &.{ "provider", "2" }, exits) orelse
|
||
fail("spawn the replacement provider");
|
||
const two = reach('2') orelse fail("re-resolving never reached the restarted provider");
|
||
_ = logging.write("protocol-registry: restarted provider reached\n");
|
||
|
||
// 7. The laundering deputy. Two binds by the *same binary*, whose grant rows
|
||
// read identically — same claimant path, same supervisor path — differing
|
||
// only in which task the supervisor is:
|
||
//
|
||
// kernel -> this driver -> deputy the deputy binds (control)
|
||
// kernel -> this driver -> deputy -> ... the grandchild does not
|
||
//
|
||
// Nobody authorized the deputy to be a supervisor, so nothing it spawns
|
||
// inherits a name — which is the whole reason a supervisor is attested by
|
||
// task id and not by the string the kernel stamped on it. The control
|
||
// matters as much as the refusal: without it this step would pass just as
|
||
// happily against a registrar that refused everything.
|
||
const deputy_exits = ipc.createIpcEndpoint() orelse fail("create the deputy's exit endpoint");
|
||
_ = process.spawnSupervised("protocol-registry-test", &.{"deputy"}, deputy_exits) orelse
|
||
fail("spawn the deputy");
|
||
const chain = awaitVerdicts(verdicts);
|
||
if (chain.deputy != 0) fail("a granted binary spawned by an AUTHORIZED task was refused");
|
||
if (chain.launder != -envelope.EPERM) fail("a granted binary spawned by an UNAUTHORIZED task was not refused");
|
||
_ = logging.write("protocol-registry: laundering deputy refused\n");
|
||
|
||
// 8. A forged terminate SIGNAL. Step 3 proved a *payload* cannot reach PID 1's
|
||
// shutdown path; this is the same destination by the other road, and the
|
||
// interesting one, because nothing here is forged at all. `signal_bind`
|
||
// nominates where a process's own signals are delivered, and `process_signal`
|
||
// lets any task signal itself — both correct in isolation, and together a
|
||
// shutdown primitive the moment the delivery point may be somebody else's
|
||
// endpoint. `fs_resolve` hands every process a handle to PID 1's, so all it
|
||
// would take is: bind, then signal yourself. The badge the kernel stamps is
|
||
// genuine, which is exactly why init cannot filter it and the kernel has to.
|
||
//
|
||
// The control comes first and matters as much as the refusal: an endpoint we
|
||
// created is accepted, so what the second line refuses is *foreignness* and
|
||
// not signals-in-general. If the second line ever succeeds again it also
|
||
// steals our own binding, so the terminate below lands in PID 1 and takes the
|
||
// boot with it — the assertion is the machine still being here.
|
||
const signals = ipc.createIpcEndpoint() orelse fail("create the signal endpoint");
|
||
if (!process.bindSignals(signals)) fail("binding signals to an endpoint we created was refused");
|
||
if (process.bindSignals(registry)) fail("binding signals to the registry's endpoint was allowed");
|
||
if (!process.sendSignal(process.taskId(), .terminate)) fail("post ourselves a terminate signal");
|
||
const after_signal = verdictWithin(forbidden, spare) orelse
|
||
fail("the registrar went silent after a redirected terminate signal — it acted on it");
|
||
if (after_signal != -envelope.EPERM) fail("the registry misanswered after a redirected terminate signal");
|
||
_ = logging.write("protocol-registry: foreign signal binding refused\n");
|
||
|
||
// 9. The same rule for the other two kernel notifications. A timer landing
|
||
// carries no identity — no sender, no cookie — so a service cannot tell a
|
||
// timer it armed from one a stranger armed on its endpoint; init re-arms its
|
||
// heartbeat on every landing, so N smuggled timers leave N+1 beats running
|
||
// forever. Exit subscriptions are the same shape with a smaller blast radius
|
||
// (an eight-slot table, and a firehose of deaths aimed at a stranger).
|
||
// Control first again, and for the timer the control's *landing* is the proof
|
||
// the refusal is about ownership rather than a timer syscall that just fails.
|
||
const ticker = ipc.createIpcEndpoint() orelse fail("create the timer endpoint");
|
||
if (!time.timerOnce(ticker, 1)) fail("arming a timer on an endpoint we created was refused");
|
||
if (time.timerOnce(registry, 1)) fail("arming a timer on the registry's endpoint was allowed");
|
||
var tick: [8]u8 = undefined;
|
||
if (!ipc.replyWait(ticker, tick[0..0], &tick, null).isTimer()) fail("our own timer did not land");
|
||
if (process.subscribeExits(registry)) fail("subscribing the registry's endpoint to exit events was allowed");
|
||
if (!process.subscribeExits(ticker)) fail("subscribing an endpoint we created to exit events was refused");
|
||
_ = logging.write("protocol-registry: foreign timer and exit binding refused\n");
|
||
|
||
// 9b. Receiving is the same privilege, and it was the one member of the family
|
||
// left unguarded. Every process holds a sendable handle to the registrar's
|
||
// mailbox — `fs_resolve` installs one for anyone who asks — and a stranger
|
||
// that could *dequeue* there would not merely evade the grants this fixture
|
||
// checks: it would take the provider endpoints that ride `bind` requests
|
||
// straight out of the queue, and answer other clients' opens in the
|
||
// registrar's name. Sending to it stays legal; receiving on it must not be.
|
||
// A refusal comes back as a negative errno in the length register, which
|
||
// is the whole point: the call returns instead of parking us on someone
|
||
// else's queue, where a success would have blocked until a request it was
|
||
// never ours to see arrived.
|
||
var stolen: [8]u8 = undefined;
|
||
const theft = ipc.replyWait(registry, stolen[0..0], &stolen, null);
|
||
if (theft.len <= ~@as(usize, 0) - 4095) fail("receiving on the registry's endpoint was allowed");
|
||
_ = logging.write("protocol-registry: foreign receive refused\n");
|
||
|
||
// 10. The handle-table storm again, aimed at a **harness-run service** this
|
||
// time. Step 4 covers PID 1, which runs its own hand-written loop; every
|
||
// other service in the system — the VFS, the display compositor, the device
|
||
// manager, every driver — runs `library/kernel/service.zig`, and that loop
|
||
// had the identical hole: the zero-length ping answered without closing what
|
||
// the ping carried, and callbacks handed a bare handle they had no use for.
|
||
// The provider role below runs that same harness, so it stands in for all of
|
||
// them. Sixty-four capability-carrying pings is twice its thirty-two slots.
|
||
var storm2: u32 = 0;
|
||
while (storm2 < 64) : (storm2 += 1) {
|
||
var scratch: [8]u8 = undefined;
|
||
_ = ipc.callCap(two, "", &scratch, spare) catch {};
|
||
}
|
||
// The proof is a *capability-passing* operation still working: the kernel
|
||
// installs the sent handle in the receiver's table before the service sees
|
||
// the request, so against an exhausted service the call fails outright. And
|
||
// the provider answers on the handed-over endpoint, so the handle it claimed
|
||
// is a working one and not merely a number.
|
||
const handover = ipc.createIpcEndpoint() orelse fail("create the handover endpoint");
|
||
var handover_reply: [8]u8 = undefined;
|
||
const served = ipc.callCap(two, &[_]u8{handover_operation}, &handover_reply, handover) catch
|
||
fail("a capability-passing call failed after capability-carrying pings — the harness leaked them");
|
||
if (served.len != 1 or handover_reply[0] != '2') fail("the harness-run provider misanswered after capability-carrying pings");
|
||
var echoed: [8]u8 = undefined;
|
||
const back = ipc.replyWait(handover, echoed[0..0], &echoed, null);
|
||
if (!back.isMessage() or back.len != 1 or echoed[0] != '2') fail("the provider never spoke on the endpoint it was handed");
|
||
_ = logging.write("protocol-registry: capability-carrying pings did not exhaust the harness\n");
|
||
|
||
_ = logging.write("protocol-registry: ok\n");
|
||
}
|
||
|
||
pub fn main(startup: process.Init) void {
|
||
const role = startup.arguments.get(1) orelse return; // bare (ramdisk sweep): stay silent
|
||
if (std.mem.eql(u8, role, "provider")) {
|
||
const label = startup.arguments.get(2) orelse "?";
|
||
if (label.len != 0) mark = label[0];
|
||
service.run(64, .{ .service = contract, .on_message = answer });
|
||
return; // terminate arrived, or the bind was refused; either way we are done
|
||
}
|
||
if (std.mem.eql(u8, role, "deputy")) return deputy();
|
||
if (std.mem.eql(u8, role, "launder")) return launder();
|
||
if (std.mem.eql(u8, role, "run")) run();
|
||
}
|