library: five protocols speak the envelope

The folded header stops being a rule in a document and becomes the layout
on the wire. Verbs number from sixteen, leaving describe, enumerate,
subscribe and unsubscribe reserved and answered the same way by every
provider — none of them writes a line to do it. What each protocol used to
carry in a field of its own now travels in the header: a vfs node and a
display layer are the packet's target, and a reply opens with a status the
envelope stamps rather than one each protocol spelled for itself.

Display gains the most. One forty-byte request had served eleven verbs, so
attach_scanout smuggled stride through x, refresh through y and format
through colour, and every coordinate crossed as a bitcast. Per-operation
structs end all three: the fields have their own names and their own signs,
and the tile payload grows to 224 bytes because the prefix shrank. Scanout
loses a message maximum of 64 it had no business declaring — it answers
calls, and the floor for a call is 256 — and virtio-gpu stops hard-coding
that number at its harness.

Two changes are semantic rather than notational. A directory now ends at an
entry with no name, because the fixed part of a reply always travels and a
zero-length reply no longer exists to mean anything. And input joins the
service harness, the last loop in the tree that answered no ping and heard
no terminate; its subscriber table, its pruning and its fan-out are the
same code, and a shutdown now asks it to stop instead of killing it.

A new conformance case reads the registry's own listing and asks every
protocol it finds for its name, its version and its verb count, then offers
a verb nobody defines and requires -ENOSYS — the envelope's promise,
checked against providers rather than against itself. What it cannot reach
in that boot it names on the serial line instead of passing quietly.

Suite 110/110.
This commit is contained in:
Daniel Samson
2026-08-01 06:15:25 +01:00
parent b004b9c3eb
commit d2dfbcabf8
40 changed files with 1882 additions and 1008 deletions
+19
View File
@@ -889,6 +889,25 @@ CASES = [
r"(?=.*protocol-denied: ok)"
r"(?=.*DANOS-TEST-RESULT: PASS)",
"fail": r"DANOS-TEST-RESULT: FAIL|protocol-denied: FAIL"},
# The reserved verbs, asked of live providers (P4a). Every protocol built on
# envelope.Define answers `describe` out of its specification and `-ENOSYS`
# for a verb it does not define, without its provider implementing either —
# so a fixture that walks /protocol's own listing and asks both of whatever
# it finds is the proof that `Define` hands those verbs to everyone alike.
# The scenario boots the registry, the input service and the compositor: two
# protocols of different sizes and verb counts, both reached through the real
# registry under the manifest's own grants. vfs, block and scanout are the
# P4a protocols whose providers need hardware chains this case does not boot;
# the fixture names them as unchecked rather than skipping them quietly, and
# the existing fat-mount / usb-storage / virtio-gpu scenarios are their proof.
{"name": "protocol-conformance",
"expect": r"(?s)(?=.*protocol-conformance: input v\d+ describes itself)"
r"(?=.*protocol-conformance: display v\d+ describes itself)"
r"(?=.*-> -ENOSYS)"
r"(?=.*protocol-conformance: 2 provider\(s\) answered the reserved verbs identically)"
r"(?=.*protocol-conformance: ok)"
r"(?=.*DANOS-TEST-RESULT: PASS)",
"fail": r"DANOS-TEST-RESULT: FAIL|protocol-conformance: FAIL"},
# Device manager: a ring-3 service enumerates /system/devices, matches the PCI host
# bridge to pci-bus, and spawns it — end-to-end proof of discover -> match -> spawn
# -> driver-up (the spawned pci-bus logs "<N> functions found").
@@ -0,0 +1,28 @@
//! The protocol-conformance-test fixture as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! build-support resolves each name from the domains this zon declares.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "protocol-conformance-test",
.root_source_file = b.path("protocol-conformance-test.zig"),
.imports = &.{
"block-protocol",
"channel",
"display-protocol",
"envelope",
"file-system",
"input-protocol",
"ipc",
"logging",
"process",
"scanout-protocol",
"time",
"vfs-protocol",
},
});
b.installArtifact(exe);
}
@@ -0,0 +1,20 @@
.{
.name = .protocol_conformance_test,
.version = "0.0.0",
.fingerprint = 0xea942427118ba350, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../../build-support" },
.kernel = .{ .path = "../../../../library/kernel" },
// envelope: the reserved verbs and the errno a refused one answers;
// vfs-protocol: the registry's own readdir, which is where the set
// under test comes from; the four -protocol modules: the name, the
// version and the verb count each contract's `describe` must report,
// read off the contract itself rather than copied beside it.
.protocol = .{ .path = "../../../../library/protocol" },
},
.paths = .{""},
}
@@ -0,0 +1,332 @@
//! 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();
}
@@ -95,9 +95,13 @@ const Answer = struct {
/// Whether a capability rode the reply. The one field that actually matters
/// to a client: the capability IS the channel.
capability: bool = false,
/// The reply header, decoded — compared field by field as well as byte for
/// byte, so a failure says *which* field diverged.
reply: vfs_protocol.Reply = .{ .status = 0, .node = 0, .len = 0 },
/// The reply's envelope `Status`, decoded — compared field by field as well
/// as byte for byte, so a failure says *which* field diverged.
status: envelope.Status = .{ .status = 0, .len = 0 },
/// The node id the reply carried, or null when it carried no reply body at
/// all. A refusal has none; the open of a contract carries a zero, because
/// the capability is the whole answer.
node: ?u64 = null,
fn bytes(self: *const Answer) []const u8 {
return self.packet[0..self.length];
@@ -129,33 +133,30 @@ fn registryEndpoint() ?ipc.Handle {
const resolve_attempts: u32 = 200;
const resolve_retry_ms: u64 = 20;
/// One vfs-protocol request at the registry: the fixed header, then the contract
/// name inline. Names go bare (`input`, not `/input`) — the registrar normalises
/// both, and bare is what `bind` sends.
fn transact(registry: ipc.Handle, operation: vfs_protocol.Operation, name: []const u8, cursor: u64) ?Answer {
var request: [vfs_protocol.message_maximum]u8 = undefined;
if (vfs_protocol.request_size + name.len > request.len) return null;
const header = vfs_protocol.Request{
.operation = operation,
.node = 0,
.offset = cursor,
.len = @intCast(name.len),
.flags = 0,
};
@memcpy(request[0..vfs_protocol.request_size], std.mem.asBytes(&header));
@memcpy(request[vfs_protocol.request_size..][0..name.len], name);
/// One vfs-protocol request at the registry: the folded header, the verb's own
/// fixed part, then the contract name as the packet's tail. Names go bare
/// (`input`, not `/input`) — the registrar normalises both, and bare is what
/// `bind` sends.
fn transact(
registry: ipc.Handle,
comptime operation: vfs_protocol.Operation,
request: vfs_protocol.Protocol.RequestOf(operation),
name: []const u8,
) ?Answer {
var packet: [vfs_protocol.message_maximum]u8 = undefined;
const framed = vfs_protocol.Protocol.encodeRequest(operation, 0, request, name, &packet) orelse return null;
var answer: Answer = .{};
const got = ipc.callCap(
registry,
request[0 .. vfs_protocol.request_size + name.len],
&answer.packet,
null,
) catch return null;
if (got.len < vfs_protocol.reply_size) return null;
const got = ipc.callCap(registry, framed, &answer.packet, null) catch return null;
answer.length = got.len;
answer.capability = got.cap != null;
answer.reply = std.mem.bytesToValue(vfs_protocol.Reply, answer.packet[0..vfs_protocol.reply_size]);
answer.status = envelope.statusOf(answer.bytes()) orelse return null;
answer.node = if (operation == .open) blk: {
const opened = vfs_protocol.Protocol.decodeReply(.open, answer.bytes()) orelse break :blk null;
// A short reply decodes as garbage rather than absence, so the promised
// length is what says whether a body is there at all.
break :blk if (answer.status.len < @sizeOf(vfs_protocol.Opened)) null else opened.node;
} else null;
// A capability we did not ask to keep is a handle slot spent; the assertions
// below only care that one arrived.
if (got.cap) |handle| _ = ipc.close(handle);
@@ -165,7 +166,7 @@ fn transact(registry: ipc.Handle, operation: vfs_protocol.Operation, name: []con
/// `open(name)`, kept whole. Null only if the registry could not be reached at
/// all — a registrar that answered has decided, and its decision is the subject.
fn openContract(registry: ipc.Handle, name: []const u8) Answer {
return transact(registry, .open, name, 0) orelse fail("the registry stopped answering");
return transact(registry, .open, .{ .flags = 0 }, name) orelse fail("the registry stopped answering");
}
/// Whether `/protocol` currently lists `name`. The namespace is browsable on
@@ -177,12 +178,11 @@ fn openContract(registry: ipc.Handle, name: []const u8) Answer {
fn listed(registry: ipc.Handle, name: []const u8) bool {
var cursor: u64 = 0;
while (cursor < 64) : (cursor += 1) {
const answer = transact(registry, .readdir, "", cursor) orelse return false;
if (answer.reply.status != 0 or answer.reply.len == 0) return false; // end of directory
const payload = answer.packet[vfs_protocol.reply_size..answer.length];
if (payload.len < vfs_protocol.directory_entry_size) return false;
const entry = std.mem.bytesToValue(vfs_protocol.DirectoryEntry, payload[0..vfs_protocol.directory_entry_size]);
const text = payload[vfs_protocol.directory_entry_size..];
const answer = transact(registry, .readdir, .{ .cursor = cursor }, "") orelse return false;
if (answer.status.status != 0) return false;
const entry = vfs_protocol.Protocol.decodeReply(.readdir, answer.bytes()) orelse return false;
if (entry.name_len == 0) return false; // end of directory
const text = vfs_protocol.Protocol.replyTail(.readdir, answer.bytes());
const length = @min(@as(usize, entry.name_len), text.len);
if (std.mem.eql(u8, text[0..length], name)) return true;
}
@@ -207,14 +207,24 @@ fn awaitListed(registry: ipc.Handle, name: []const u8) void {
/// Every caller-visible field of two answers, compared. `step` names the pair so
/// a failure says which comparison broke and in which field.
fn expectIdentical(step: []const u8, refused: Answer, absent: Answer) void {
if (refused.reply.status != absent.reply.status) fail(step); // the errno
if (refused.reply.node != absent.reply.node) fail(step); // the node id an open would return
if (refused.reply.len != absent.reply.len) fail(step); // payload bytes promised
if (refused.status.status != absent.status.status) fail(step); // the errno
if (!nodesMatch(refused.node, absent.node)) fail(step); // the node id an open would return
if (refused.status.len != absent.status.len) fail(step); // payload bytes promised
if (refused.length != absent.length) fail(step); // reply packet length
if (refused.capability != absent.capability) fail(step); // the channel itself
if (!std.mem.eql(u8, refused.bytes(), absent.bytes())) fail(step); // and every byte of it
}
/// Two node ids agree when both are absent or both are the same value. A refusal
/// carries none at all now — the envelope sends a bare `Status` — so "no node"
/// is itself one of the observations that has to match.
fn nodesMatch(one: ?u64, other: ?u64) bool {
if (one) |a| {
return if (other) |b| a == b else false;
}
return other == null;
}
fn run() void {
const registry = registryEndpoint() orelse fail("resolve /protocol");
@@ -232,14 +242,14 @@ fn run() void {
// 1. The control. A granted, bound contract opens: success, and the
// capability that IS the channel.
const allowed = openContract(registry, granted_contract);
if (allowed.reply.status != 0) fail("a granted open was refused");
if (allowed.status.status != 0) fail("a granted open was refused");
if (!allowed.capability) fail("a granted open carried no channel");
_ = logging.write("protocol-denied: granted open succeeded\n");
// 2. The refusal. `input` is bound — the listing above proved it — and no
// manifest row names this binary against it.
const refused = openContract(registry, forbidden_contract);
if (refused.reply.status != -envelope.ENOENT) fail("an ungranted open did not answer -ENOENT");
if (refused.status.status != -envelope.ENOENT) fail("an ungranted open did not answer -ENOENT");
if (refused.capability) fail("an ungranted open carried a channel");
_ = logging.write("protocol-denied: ungranted open refused as absent\n");
@@ -259,7 +269,7 @@ fn run() void {
// what it should, so what steps 2-4 saw was policy and not a registry
// that had wedged.
const again = openContract(registry, granted_contract);
if (again.reply.status != 0 or !again.capability) fail("the granted contract stopped opening");
if (again.status.status != 0 or !again.capability) fail("the granted contract stopped opening");
_ = logging.write("protocol-denied: ok\n");
}