//! protocol-conformance-test — P4a's evidence that `envelope.Define` gives every //! provider the reserved verbs, uniformly and without the provider writing a line //! for them (docs/security-track-plan.md P4a; //! docs/os-development/protocol-namespace.md). One binary, one role, driven by //! the `protocol-conformance` kernel case: //! //! - `protocol-conformance-test run` — for each contract it can reach: //! 1. `describe` — the reserved verb 0 — is answered, and the answer names //! *that* protocol: the name it was opened under, the version its module //! declares, and the number of verbs its module declares. No provider in //! the system implements `describe`; the generated dispatch answers it out //! of the specification, which is exactly the claim being checked; //! 2. a verb number no protocol in the system defines answers `-ENOSYS`, and //! carries no capability. That is the other half of the same generated //! dispatch: a provider does not have to reject strangers, it gets the //! rejection for free and every provider gives the same one; //! 3. `describe` again, after the refusal — a refused verb is an *answer*, //! not a wedged service, so the channel is still good afterwards. //! //! **The set it checks is read, never hardcoded.** The fixture asks `/protocol` //! for its own listing (`readdir`, which the namespace publishes on purpose) and //! walks what it finds, so the case cannot drift from what this boot actually //! bound. What it opens is bounded by P3: the manifest names this binary against //! exactly the two contracts its scenario boots, and an ungranted name is absent //! for it like any other client's. //! //! **What it covers, and what it cannot — the honest list at P4a.** The scenario //! boots the registry, the input service, and the compositor, so `input` and //! `display` are checked end to end over real IPC. The other three protocols P4a //! rebased are not asked here, and the reason is the provider, not the protocol: //! //! - `vfs` — the FAT server, which needs a mounted volume behind the whole USB //! storage chain (the `fat-mount` scenario); //! - `block` — the usb-storage driver, which the device manager spawns after //! enumerating an xHCI bus (the `usb-storage` scenario); //! - `scanout` — the virtio-gpu driver, which needs an emulated virtio-gpu the //! default harness does not attach (the `virtio-gpu` scenario). //! //! Booting any of those chains here would buy conformance for a third and fourth //! provider at the price of a case that boots half the system to send two //! packets; their rebase is proven instead by the scenarios that already drive //! them. All three sit in the table below anyway, so if a future scenario binds //! one, this fixture checks it without being edited — and prints, every run, the //! ones it found no provider for. //! //! **And the ones it must not ask.** `device-manager`, `power` and //! `usb-transfer` are still hand-numbered (P4b): to them, operation 0 is a verb //! of their own, not `describe`. So the table is not "every contract" but "every //! contract already built on `Define`" — anything listed that is not in it is //! reported as skipped by name, never silently. P4b adds three rows here and the //! coverage follows. //! //! The registry itself — PID 1 serving `/protocol` — is the one vfs backend //! deliberately NOT dispatched through the generated table (it reads a //! stranger's packet by hand, `system/services/init/init.zig`), so `describe` is //! not asked of it and nothing here claims it. //! //! Prints `protocol-conformance: ok` on success, or a `protocol-conformance: //! 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 process = @import("process"); const time = @import("time"); const block_protocol = @import("block-protocol"); const display_protocol = @import("display-protocol"); const input_protocol = @import("input-protocol"); const scanout_protocol = @import("scanout-protocol"); const vfs_protocol = @import("vfs-protocol"); // --- what conformance means, per contract ----------------------------------- /// One contract this fixture knows how to check, and what the answer must say. /// Every field is read off the protocol module itself, so the expectation is the /// contract's own definition rather than a number copied beside it — a version /// bump or a new verb updates this table by recompiling. const Contract = struct { name: []const u8, version: u32, /// How many verbs the module declares — `describe` reports it, so it is /// checked. The reserved verbs are not counted: they are the envelope's. operations: u32, /// Whether this scenario boots a provider for it. A required contract that /// is missing, unreachable or non-conforming fails the case; the rest are /// checked when some other scenario happens to bind them. required: bool, }; fn contractOf(comptime Protocol: type, required: bool) Contract { return .{ .name = Protocol.protocol_name, .version = Protocol.version, .operations = @typeInfo(Protocol.Operation).@"enum".fields.len, .required = required, }; } /// The protocols built on `envelope.Define`, and nothing else. A name listed by /// `/protocol` that is absent from here is reported and left alone — see the /// header: asking a hand-numbered provider for operation 0 would name one of its /// own verbs. const contracts = [_]Contract{ contractOf(input_protocol.Protocol, true), // the input fan-out service contractOf(display_protocol.Protocol, true), // the compositor contractOf(vfs_protocol.Protocol, false), // the FAT server — needs a volume contractOf(block_protocol.Protocol, false), // usb-storage — needs the xHCI chain contractOf(scanout_protocol.Protocol, false), // virtio-gpu — needs the device }; /// A verb number no protocol in the system defines, and none plausibly will: far /// above the reserved range, so it is unambiguously a protocol verb, and far /// above any protocol's verb count, so the generated dispatch has nothing to /// match it against. The answer must be `-ENOSYS` at every provider. const stranger_operation: u32 = envelope.first_protocol_operation + 4096; fn fail(step: []const u8) noreturn { _ = logging.write("protocol-conformance: FAIL "); _ = logging.write(step); _ = logging.write("\n"); process.exit(1); } fn report(comptime format: []const u8, arguments: anytype) void { var line: [192]u8 = undefined; _ = logging.write(std.fmt.bufPrint(&line, format, arguments) catch return); } // --- reading the namespace --------------------------------------------------- /// The cadence every client in the tree spends finding a service. const resolve_attempts: u32 = 200; const resolve_retry_ms: u64 = 20; /// The registry's endpoint, obtained the way every process obtains it: resolve /// `/protocol`. The handle is the kernel's, shared with every other user of the /// mount, so it is never ours to close. Patient, because the harness starts the /// registrar and this fixture together and a first resolve can land before init /// has mounted `/protocol` at all. fn registryEndpoint() ?ipc.Handle { var attempt: u32 = 0; while (attempt < resolve_attempts) : (attempt += 1) { var relative: [channel.path_maximum]u8 = undefined; if (file_system.fsResolve(channel.root, 0, &relative)) |route| switch (route) { .kernel => return null, // a kernel route means something other than the registry owns the name .backend => |backend| return backend.handle, }; time.sleepMillis(resolve_retry_ms); } return null; } /// One `readdir(cursor)` at the registry, into `into`. Null at end of directory /// or on any failure — the caller is walking a listing, and both mean "stop". /// /// The listing is what makes this test un-driftable: `/protocol` publishes what /// is bound (protocol-namespace.md — the tree stays diagnosable), so the set /// under test is the set this boot actually produced. fn entryAt(registry: ipc.Handle, cursor: u64, into: []u8) ?[]u8 { var packet: [vfs_protocol.message_maximum]u8 = undefined; const framed = vfs_protocol.Protocol.encodeRequest(.readdir, 0, .{ .cursor = cursor }, &.{}, &packet) orelse return null; var reply: [vfs_protocol.message_maximum]u8 = undefined; const got = ipc.callCap(registry, framed, &reply, null) catch return null; // A readdir owes no capability; one that arrived anyway is a handle slot. if (got.cap) |handle| _ = ipc.close(handle); const answer = reply[0..got.len]; const status = envelope.statusOf(answer) orelse return null; if (status.status != 0) return null; const entry = vfs_protocol.Protocol.decodeReply(.readdir, answer) orelse return null; if (entry.name_len == 0) return null; // end of directory const text = vfs_protocol.Protocol.replyTail(.readdir, answer); const length = @min(@as(usize, entry.name_len), @min(text.len, into.len)); @memcpy(into[0..length], text[0..length]); return into[0..length]; } /// The listing, taken once so every later question is asked of one observation /// rather than of a namespace that may have moved underneath it. const maximum_listed: usize = 32; var listed_names: [maximum_listed][channel.name_maximum]u8 = undefined; var listed_lengths: [maximum_listed]usize = undefined; var listed_count: usize = 0; fn listedName(index: usize) []const u8 { return listed_names[index][0..listed_lengths[index]]; } fn takeListing(registry: ipc.Handle) void { listed_count = 0; var cursor: u64 = 0; while (cursor < maximum_listed) : (cursor += 1) { const name = entryAt(registry, cursor, &listed_names[listed_count]) orelse return; listed_lengths[listed_count] = name.len; listed_count += 1; } } /// Whether `/protocol` currently lists `name`. fn listed(registry: ipc.Handle, name: []const u8) bool { var cursor: u64 = 0; while (cursor < maximum_listed) : (cursor += 1) { var scratch: [channel.name_maximum]u8 = undefined; const entry = entryAt(registry, cursor, &scratch) orelse return false; if (std.mem.eql(u8, entry, name)) return true; } return false; } /// Wait until `/protocol` lists `name` — the providers this case needs come up /// alongside the fixture, and racing them would make the listing a boot race /// rather than an observation. fn awaitListed(registry: ipc.Handle, name: []const u8) void { var attempts: u32 = 0; while (attempts < 400) : (attempts += 1) { if (listed(registry, name)) return; time.sleepMillis(20); } report("protocol-conformance: FAIL /protocol never listed {s}\n", .{name}); process.exit(1); } // --- the assertions ---------------------------------------------------------- /// The three checks, against one open channel. Every failure is fatal: the point /// of the case is that these hold at *every* provider, so one that does not is /// not a degraded result, it is the regression. fn conform(link: channel.Channel, contract: Contract) void { var buffer: [envelope.packet_maximum]u8 = undefined; // 1. The reserved verb no provider implements. `describe` is answered from // the specification by the generated dispatch, so what comes back is the // contract's own identity — checked field by field against the module // this fixture compiled against. const described = link.describe(&buffer) orelse fail("describe was not answered"); if (!std.mem.eql(u8, described.name, contract.name)) fail("describe named a different protocol"); if (described.description.version != contract.version) fail("describe answered the wrong version"); if (described.description.operation_count != contract.operations) fail("describe counted the wrong number of verbs"); // 2. A number no protocol wears. Nothing in the provider looks at it; the // dispatch table finds no handler and refuses, identically everywhere. var into: [envelope.packet_maximum]u8 = undefined; const answered = link.call(.{ .operation = stranger_operation }, &.{}, &into) orelse fail("a stranger verb was not answered at all"); if (answered.status.status != -envelope.ENOSYS) fail("a stranger verb did not answer -ENOSYS"); if (answered.status.len != 0) fail("a refused verb promised a payload"); if (answered.capability) |handle| { _ = ipc.close(handle); fail("a refused verb handed back a capability"); } // 3. A refusal is an answer, not a wedge — so the same channel still works. const again = link.describe(&buffer) orelse fail("the provider stopped answering after a refused verb"); if (!std.mem.eql(u8, again.name, contract.name)) fail("describe changed its answer after a refused verb"); report("protocol-conformance: {s} v{d} describes itself ({d} verbs), verb {d} -> -ENOSYS\n", .{ contract.name, contract.version, contract.operations, stranger_operation, }); } fn contractIndex(name: []const u8) ?usize { for (contracts, 0..) |contract, index| { if (std.mem.eql(u8, contract.name, name)) return index; } return null; } fn run() void { const registry = registryEndpoint() orelse fail("resolve /protocol"); // Every contract this scenario is supposed to be able to check must be bound // before the listing is taken, or the case would assert nothing on a slow // boot instead of failing on a broken one. for (contracts) |contract| { if (contract.required) awaitListed(registry, contract.name); } takeListing(registry); if (listed_count == 0) fail("/protocol listed nothing at all"); report("protocol-conformance: /protocol lists {d} contract(s)\n", .{listed_count}); var checked = [_]bool{false} ** contracts.len; for (0..listed_count) |index| { const name = listedName(index); const found = contractIndex(name) orelse { // Not a lie of omission: named on serial, with the reason. report("protocol-conformance: {s} skipped — not built on envelope.Define yet\n", .{name}); continue; }; const contract = contracts[found]; const link = channel.Channel.connect(name) orelse { // P3 is in force: an ungranted name is absent for this binary, and // that is a manifest fact, not a failure — unless the scenario is // supposed to have granted it. if (contract.required) fail("a contract this fixture is granted would not open"); report("protocol-conformance: {s} skipped — not granted to this fixture\n", .{name}); continue; }; conform(link, contract); link.close(); checked[found] = true; } // The vacuity guard, and the honest tail: a required contract that went // unchecked fails the case, and every other one this fixture knows how to // check but found no provider for is named, so the coverage is legible on // serial rather than inferred from what is absent. var count: usize = 0; for (contracts, 0..) |contract, index| { if (checked[index]) { count += 1; continue; } if (contract.required) fail("a contract this scenario boots was never conformance-checked"); report("protocol-conformance: {s} not bound in this scenario — no provider to ask\n", .{contract.name}); } report("protocol-conformance: {d} provider(s) answered the reserved verbs identically\n", .{count}); _ = logging.write("protocol-conformance: 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, "run")) run(); }