init: a protocol you were not granted does not exist

The registry consults the open rows it has been parsing since P2, so
reaching a contract now takes a grant as well as a binding. A caller
without one is answered exactly as it would be for a name nobody ever
bound: same status, same empty reply, same absent capability, byte for
byte, and no log line on either path — klog_read is ungated, so a line on
one and not the other would be the oracle the design set out to remove.
Refusal and absence being one answer is what lets a supervisor later
narrow, fake or park a child's namespace without the child learning what
it was denied.

The manifest gains a third permission for a shape the plan did not
foresee: attestation is one hop, but the driver tree is three deep — the
PS/2 keyboard and mouse are spawned by ps2-bus, which the device manager
spawned — so no row could name them and PS/2 input would simply stop.
 lets a delegate vouch for what its children *reach*, never for
what they claim; the bind path is untouched, and the laundering deputy is
still refused.

The review found the receive side of a rule this track had already
written down. Every process holds a sendable handle to the registrar —
resolve installs one for anyone who asks — and ipc_reply_wait never asked
who owned the endpoint, so a stranger could dequeue there: take the
provider endpoints riding bind requests, and answer other clients' opens
in the registrar's name. Receiving is the owner's privilege, like binding
a signal or a timer; sending remains anyone's.

Suite 109/109.
This commit is contained in:
Daniel Samson
2026-08-01 04:44:15 +01:00
parent 1379b699f3
commit 7f3eb848f0
13 changed files with 670 additions and 24 deletions
+55
View File
@@ -248,6 +248,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
deviceManagerTest(boot_information);
} else if (eql(case, "protocol-registry")) {
protocolRegistryTest(boot_information);
} else if (eql(case, "protocol-denied")) {
protocolDeniedTest(boot_information);
} else if (eql(case, "reboot")) {
rebootTest();
} else {
@@ -3843,6 +3845,59 @@ fn protocolRegistryTest(boot_information: *const BootInformation) void {
result();
}
/// P3 — restriction stage one (docs/os-development/protocol-namespace.md). The
/// registrar now checks `open` against `/system/configuration/protocol.csv`, and
/// a caller with no grant is told exactly what a caller asking for a name nobody
/// bound is told.
///
/// The scenario is the assertion's scaffolding: `/protocol` (init in its registry
/// role), the **input service** — which binds a real contract the fixture is
/// deliberately not granted — and the fixture. Without a live provider on the
/// forbidden name, "refused" and "not bound yet" would be the same observation
/// and the case would prove nothing; the fixture reads `/protocol`'s own listing
/// to confirm the name is there before it asks for it.
///
/// The fixture's `protocol-denied: ok` is the marker; each step prints its own
/// line, which the harness's ordered regex reads.
fn protocolDeniedTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: protocol-denied\n", .{});
if (boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over an initial_ramdisk", false);
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
process.setInitialRamdisk(image);
check("registry (init) spawned", spawnRegistry(rd));
// The provider of the contract the fixture may NOT reach. It needs no
// hardware: it binds /protocol/input and waits for subscribers.
check("input service spawned", spawnNamed(rd, "input"));
check("protocol-denied-test spawned", spawnNamedWithArg(rd, "protocol-denied-test", "run"));
const pass_marker = "protocol-denied: ok";
const fail_marker = "protocol-denied: FAIL";
scheduler.setPriority(1);
const deadline = architecture.millis() + 20000;
var saw_pass = false;
var saw_fail = false;
while (architecture.millis() < deadline and !saw_pass and !saw_fail) {
if (bufferHas(pass_marker)) saw_pass = true;
if (bufferHas(fail_marker)) saw_fail = true;
scheduler.yield();
}
scheduler.setPriority(4);
check("no step of the restriction contract failed", !saw_fail);
check("the fixture completed every restriction assertion", saw_pass);
result();
}
fn deviceManagerTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: device-manager\n", .{});
if (boot_information.initial_ramdisk_len == 0) {