//! 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 ` — binds `/protocol/test/registry` //! and answers every request with its one-byte ``, 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/` 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(); }