init: /protocol replaces the ServiceId registry
A protocol is reached by name now, not by a compile-time integer. Init is PID 1 and already knows which binary it started, so init serves /protocol as a vfs backend: bind claims a contract with the provider's endpoint attached, open answers with that endpoint as the reply's capability, and readdir lists what is bound with the task and binary behind it. The kernel reserves the prefix — nothing may mount over it, under it, or unmount it — and ServiceId, ipc_register and ipc_lookup are gone, their syscall numbers left vacant. A bind is authorized by who the caller *is*: the kernel-stamped binary together with the supervising task's identity, matched against /system/configuration/protocol.csv. Identity, not spelling — spawn is ungated, so an attacker can run any bundled binary, and a name-only rule would have let it launder grants through an init of its own making. A name a live process holds is refused to everyone else; a dead one's is released. Three review rounds against a hostile ring-3 process found what 108 green tests could not, because the suite contains no attacker. Publishing init's supervision endpoint as the registry put PID 1's mailbox in every process's hands, where two forged bytes reached the shutdown path: privileged traffic is now believed only from the task that holds the contract it speaks for. A capability arriving on a request outlived every path that ignored it, one handle per call until the table was full — in init, and in the harness ten services share — so the arriving capability is owned by the turn and released unless a handler says otherwise. And the kernel let anyone holding an endpoint handle aim signals, timers, exit notices and interrupts at it: binding now requires having created it. Suite 108/108. The new protocol-registry case asserts eleven properties, each one an attack that must fail.
This commit is contained in:
+116
-17
@@ -246,6 +246,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
||||
containmentTest();
|
||||
} else if (eql(case, "device-manager")) {
|
||||
deviceManagerTest(boot_information);
|
||||
} else if (eql(case, "protocol-registry")) {
|
||||
protocolRegistryTest(boot_information);
|
||||
} else if (eql(case, "reboot")) {
|
||||
rebootTest();
|
||||
} else {
|
||||
@@ -2209,7 +2211,7 @@ fn processKillTest(boot_information: *const BootInformation) void {
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "process-test")) continue;
|
||||
spinner = process.spawnProcessSupervised(item.blob, 4, &.{ "process-test", "spinner" }, me, endpoint) catch 0;
|
||||
spinner = process.spawnProcessSupervised(item.blob, 4, &.{ item.name, "spinner" }, me, endpoint) catch 0;
|
||||
break;
|
||||
}
|
||||
check("process-test spawned as the supervised spinner victim", spinner != 0);
|
||||
@@ -2395,13 +2397,14 @@ fn signalsTest(boot_information: *const BootInformation) void {
|
||||
};
|
||||
|
||||
process.setInitialRamdisk(image); // the parent system_spawns its children by name
|
||||
_ = spawnRegistry(rd); // the service child binds /protocol/test/process
|
||||
process.write_count = 0;
|
||||
var runner: u32 = 0;
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "process-test")) continue;
|
||||
runner = process.spawnProcessSupervised(item.blob, 4, &.{ "process-test", "signal-run" }, scheduler.currentId(), null) catch 0;
|
||||
runner = process.spawnProcessSupervised(item.blob, 4, &.{ item.name, "signal-run" }, scheduler.currentId(), null) catch 0;
|
||||
break;
|
||||
}
|
||||
check("signal-run parent spawned", runner != 0);
|
||||
@@ -2443,13 +2446,14 @@ fn driverRestartTest(boot_information: *const BootInformation) void {
|
||||
};
|
||||
|
||||
process.setInitialRamdisk(image); // the manager system_spawns drivers by name
|
||||
_ = spawnRegistry(rd); // the drivers bind their contracts
|
||||
process.write_count = 0;
|
||||
var manager: u32 = 0;
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-restart" }, scheduler.currentId(), null) catch 0;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{ item.name, "test-restart" }, scheduler.currentId(), null) catch 0;
|
||||
break;
|
||||
}
|
||||
check("device-manager spawned in test-restart mode", manager != 0);
|
||||
@@ -2482,12 +2486,13 @@ fn usbReportTest(boot_information: *const BootInformation) void {
|
||||
};
|
||||
|
||||
process.setInitialRamdisk(image);
|
||||
_ = spawnRegistry(rd); // the xhci driver binds /protocol/usb-transfer
|
||||
var manager: u32 = 0;
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-usb-restart" }, scheduler.currentId(), null) catch 0;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{ item.name, "test-usb-restart" }, scheduler.currentId(), null) catch 0;
|
||||
break;
|
||||
}
|
||||
check("device-manager spawned in test-usb-restart mode", manager != 0);
|
||||
@@ -2514,12 +2519,13 @@ fn deviceListTest(boot_information: *const BootInformation) void {
|
||||
};
|
||||
|
||||
process.setInitialRamdisk(image);
|
||||
_ = spawnRegistry(rd); // the fixture opens /protocol/device-manager
|
||||
var manager: u32 = 0;
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-usb-restart" }, scheduler.currentId(), null) catch 0;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{ item.name, "test-usb-restart" }, scheduler.currentId(), null) catch 0;
|
||||
break;
|
||||
}
|
||||
check("device-manager spawned in test-usb-restart mode", manager != 0);
|
||||
@@ -2548,13 +2554,14 @@ fn pciCapsTest(boot_information: *const BootInformation) void {
|
||||
};
|
||||
|
||||
process.setInitialRamdisk(image);
|
||||
_ = spawnRegistry(rd); // the manager binds /protocol/device-manager
|
||||
// Plain mode — no restart drill, whose kill would race the fixture's claim.
|
||||
var manager: u32 = 0;
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{"device-manager"}, scheduler.currentId(), null) catch 0;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{item.name}, scheduler.currentId(), null) catch 0;
|
||||
break;
|
||||
}
|
||||
check("device-manager spawned", manager != 0);
|
||||
@@ -2582,12 +2589,13 @@ fn iommuFaultTest(boot_information: *const BootInformation) void {
|
||||
|
||||
check("IOMMU enabled for the enforcement test", iommu.enabled());
|
||||
process.setInitialRamdisk(image);
|
||||
_ = spawnRegistry(rd); // the manager binds /protocol/device-manager
|
||||
var manager: u32 = 0;
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{"device-manager"}, scheduler.currentId(), null) catch 0;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{item.name}, scheduler.currentId(), null) catch 0;
|
||||
break;
|
||||
}
|
||||
check("device-manager spawned", manager != 0);
|
||||
@@ -2623,12 +2631,13 @@ fn pciScanTest(boot_information: *const BootInformation) void {
|
||||
check("the kernel seeded no PCI functions (the walk retired)", brokerPciCount(&buffer) == 0);
|
||||
|
||||
process.setInitialRamdisk(image);
|
||||
_ = spawnRegistry(rd); // the manager binds /protocol/device-manager
|
||||
var manager: u32 = 0;
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-pci-restart" }, scheduler.currentId(), null) catch 0;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{ item.name, "test-pci-restart" }, scheduler.currentId(), null) catch 0;
|
||||
break;
|
||||
}
|
||||
check("device-manager spawned (test-pci-restart mode)", manager != 0);
|
||||
@@ -2776,12 +2785,13 @@ fn acpiReportTest(boot_information: *const BootInformation) void {
|
||||
return;
|
||||
};
|
||||
process.setInitialRamdisk(image);
|
||||
_ = spawnRegistry(rd); // the manager and the acpi service bind theirs
|
||||
var spawned = false;
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue;
|
||||
_ = process.spawnProcessSupervised(item.blob, 4, &.{"device-manager"}, scheduler.currentId(), null) catch 0;
|
||||
_ = process.spawnProcessSupervised(item.blob, 4, &.{item.name}, scheduler.currentId(), null) catch 0;
|
||||
spawned = true;
|
||||
break;
|
||||
}
|
||||
@@ -2819,7 +2829,7 @@ fn acpiParseTest(boot_information: *const BootInformation) void {
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "discovery")) continue;
|
||||
_ = process.spawnProcessSupervised(item.blob, 4, &.{ "discovery", "1" }, scheduler.currentId(), null) catch 0;
|
||||
_ = process.spawnProcessSupervised(item.blob, 4, &.{ item.name, "1" }, scheduler.currentId(), null) catch 0;
|
||||
spawned = true;
|
||||
break;
|
||||
}
|
||||
@@ -2854,7 +2864,7 @@ fn supervisionTest(boot_information: *const BootInformation) void {
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "process-test")) continue;
|
||||
started = if (process.spawnProcess(item.blob, 4, &.{ "process-test", "run" })) true else |_| false;
|
||||
started = if (process.spawnProcess(item.blob, 4, &.{ item.name, "run" })) true else |_| false;
|
||||
break;
|
||||
}
|
||||
check("process-test spawned as the user-space supervisor", started);
|
||||
@@ -2996,6 +3006,9 @@ fn inputTest(boot_information: *const BootInformation) void {
|
||||
|
||||
process.write_count = 0;
|
||||
process.write_from_user = false;
|
||||
// init (the registry, below) reads its manifests through the kernel VFS.
|
||||
process.setInitialRamdisk(image);
|
||||
_ = spawnRegistry(rd); // the input service binds /protocol/input
|
||||
_ = spawnNamed(rd, "input"); // the fan-out service
|
||||
_ = spawnNamed(rd, "input-source"); // a synthetic keyboard publishing events
|
||||
_ = spawnNamed(rd, "input-test"); // the subscriber whose "ok" line is the marker
|
||||
@@ -3037,6 +3050,10 @@ fn displayServiceTest(boot_information: *const BootInformation) void {
|
||||
return;
|
||||
};
|
||||
|
||||
// init (the registry) reads its manifests through the kernel VFS.
|
||||
process.setInitialRamdisk(image);
|
||||
_ = spawnRegistry(rd); // the compositor binds /protocol/display
|
||||
|
||||
// Spawn the compositor and hand it the core. Its own serial heartbeats — `display:
|
||||
// online WxH` and `display: presented frame 0` — are what the harness matches (it
|
||||
// reads serial directly, like the fault cases). We don't poll for them in-kernel: a
|
||||
@@ -3073,6 +3090,9 @@ fn displayCursorTest(boot_information: *const BootInformation) void {
|
||||
return;
|
||||
};
|
||||
|
||||
// init (the registry, below) reads its manifests through the kernel VFS.
|
||||
process.setInitialRamdisk(image);
|
||||
_ = spawnRegistry(rd); // input and display bind theirs
|
||||
if (!spawnNamed(rd, "input")) {
|
||||
log("display-cursor: could not spawn the input service\n", .{});
|
||||
result();
|
||||
@@ -3113,6 +3133,9 @@ fn displayDemoTest(boot_information: *const BootInformation) void {
|
||||
return;
|
||||
};
|
||||
|
||||
// init (the registry, below) reads its manifests through the kernel VFS.
|
||||
process.setInitialRamdisk(image);
|
||||
_ = spawnRegistry(rd); // the compositor binds /protocol/display
|
||||
if (!spawnNamed(rd, "display")) {
|
||||
log("display-demo: could not spawn the display service\n", .{});
|
||||
result();
|
||||
@@ -3148,6 +3171,9 @@ fn sharedMemoryTest(boot_information: *const BootInformation) void {
|
||||
return;
|
||||
};
|
||||
|
||||
// init (the registry, below) reads its manifests through the kernel VFS.
|
||||
process.setInitialRamdisk(image);
|
||||
_ = spawnRegistry(rd); // the server binds /protocol/test/shared-memory
|
||||
if (!spawnNamed(rd, "shared-memory-server")) {
|
||||
log("shared-memory: could not spawn shared-memory-server\n", .{});
|
||||
result();
|
||||
@@ -3185,12 +3211,13 @@ fn virtioGpuTest(boot_information: *const BootInformation) void {
|
||||
// from the kernel device tree, spawns pci-bus, and matches the virtio-gpu class triple to
|
||||
// spawn our driver with the function's device id as argv[1].
|
||||
process.setInitialRamdisk(image);
|
||||
_ = spawnRegistry(rd); // the driver binds /protocol/scanout
|
||||
var manager: u32 = 0;
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{"device-manager"}, scheduler.currentId(), null) catch 0;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{item.name}, scheduler.currentId(), null) catch 0;
|
||||
break;
|
||||
}
|
||||
if (manager == 0) {
|
||||
@@ -3226,12 +3253,13 @@ fn displayNativeTest(boot_information: *const BootInformation) void {
|
||||
};
|
||||
|
||||
process.setInitialRamdisk(image);
|
||||
_ = spawnRegistry(rd); // display, the manager, and the driver bind theirs
|
||||
var manager: u32 = 0;
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{"device-manager"}, scheduler.currentId(), null) catch 0;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{item.name}, scheduler.currentId(), null) catch 0;
|
||||
break;
|
||||
}
|
||||
if (manager == 0) {
|
||||
@@ -3270,12 +3298,13 @@ fn displayReattachTest(boot_information: *const BootInformation) void {
|
||||
};
|
||||
|
||||
process.setInitialRamdisk(image);
|
||||
_ = spawnRegistry(rd); // display and the restarted driver bind theirs
|
||||
var manager: u32 = 0;
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-scanout-restart" }, scheduler.currentId(), null) catch 0;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{ item.name, "test-scanout-restart" }, scheduler.currentId(), null) catch 0;
|
||||
break;
|
||||
}
|
||||
if (manager == 0) {
|
||||
@@ -3625,6 +3654,21 @@ fn threadTestMarkerCase(boot_information: *const BootInformation, case_name: []c
|
||||
result();
|
||||
}
|
||||
|
||||
/// Bring up the protocol namespace for a scenario that spawns its providers
|
||||
/// itself. `/protocol` is served by init, PID 1 — but a scenario case wants the
|
||||
/// naming layer without init's whole service list underneath it, so init is
|
||||
/// started in its `registry` role: it mounts `/protocol`, reads the grants, and
|
||||
/// spawns nothing (docs/os-development/protocol-namespace.md; the plan's
|
||||
/// decision 9). Providers retry their bind, so racing the mount is survivable —
|
||||
/// but calling this first makes the race rare.
|
||||
///
|
||||
/// The caller must have published the initial ramdisk already
|
||||
/// (`process.setInitialRamdisk`): init reads its manifests out of it, and every
|
||||
/// `/protocol` resolve goes through the same kernel VFS.
|
||||
fn spawnRegistry(rd: initial_ramdisk.Reader) bool {
|
||||
return spawnNamedWithArg(rd, "init", "registry");
|
||||
}
|
||||
|
||||
fn spawnNamed(rd: initial_ramdisk.Reader, name: []const u8) bool {
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
@@ -3745,6 +3789,60 @@ fn childDescriptor(hid: []const u8, start: u64, len: u64) device_abi.DeviceDescr
|
||||
/// match `pci-bus`, and spawn it (with the bridge id as its argument) — and the spawned
|
||||
/// pci-bus must reach its own live marker. It uses no special privilege — the same
|
||||
/// `device_enumerate` any process could call.
|
||||
/// P2 — the registrar (docs/os-development/protocol-namespace.md). Bring up
|
||||
/// `/protocol` (init in its registry role) and hand the fixture the core: it
|
||||
/// asserts that an ungranted bind is refused, that the kernel's reserved prefix
|
||||
/// holds, that a name a live provider holds cannot be taken, and that killing a
|
||||
/// provider makes its channel fail while re-resolving the same name reaches the
|
||||
/// restarted instance.
|
||||
///
|
||||
/// It doubles as the security case for PID 1's shared mailbox, since resolving
|
||||
/// `/protocol` hands every process a sendable handle to it: a forged power
|
||||
/// payload, a redirected terminate signal, a timer or exit subscription armed on
|
||||
/// a foreign endpoint, and capability-carrying ping storms against both PID 1 and
|
||||
/// a harness-run service. Those assertions kill the boot when they regress rather
|
||||
/// than printing anything, which is the strongest form available here.
|
||||
///
|
||||
/// The fixture's `protocol-registry: ok` is the marker; each step also prints its
|
||||
/// own line, which the harness's ordered regex reads.
|
||||
fn protocolRegistryTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: protocol-registry\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;
|
||||
};
|
||||
|
||||
// The fixture spawns its own providers by name, so the ramdisk must be
|
||||
// published; init then mounts /protocol over the same kernel VFS.
|
||||
process.setInitialRamdisk(image);
|
||||
check("registry (init) spawned", spawnRegistry(rd));
|
||||
check("protocol-registry-test spawned", spawnNamedWithArg(rd, "protocol-registry-test", "run"));
|
||||
|
||||
const pass_marker = "protocol-registry: ok";
|
||||
const fail_marker = "protocol-registry: 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 registry contract failed", !saw_fail);
|
||||
check("the fixture completed every registry 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) {
|
||||
@@ -3764,6 +3862,7 @@ fn deviceManagerTest(boot_information: *const BootInformation) void {
|
||||
// all, it's because the manager discovered the PCI host bridge, matched, and
|
||||
// spawned it.
|
||||
process.setInitialRamdisk(image);
|
||||
_ = spawnRegistry(rd); // the manager binds /protocol/device-manager
|
||||
|
||||
process.write_count = 0;
|
||||
process.write_from_user = false;
|
||||
@@ -3845,9 +3944,9 @@ fn hpetDeviceId() ?u64 {
|
||||
/// 2. After `releaseOwner` for the binding's owner, that same entry is masked again.
|
||||
///
|
||||
/// And one property that can only be checked from kernel state: a *different* owner's
|
||||
/// binding on the same endpoint survives. Endpoints are shared (ipc_register hands out
|
||||
/// references), so teardown keyed on the endpoint pointer rather than the owning task
|
||||
/// would mask a live sibling driver's device line.
|
||||
/// binding on the same endpoint survives. Endpoints are shared (a capability passed in a
|
||||
/// message hands out extra references), so teardown keyed on the endpoint pointer rather
|
||||
/// than the owning task would mask a live sibling driver's device line.
|
||||
fn irqFreeTest() void {
|
||||
log("DANOS-TEST-BEGIN: irqfree\n", .{});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user