//! test/system/services/badge-scope-test — the guessable-id probe. Two providers //! hand small integers to a client and then take them back from anyone who says //! the number: the FAT server's node ids (a table of thirty-two) and the //! compositor's layer ids (a table of sixteen). P4c scoped both to the badge that //! opened them (docs/os-development/protocol-namespace.md — *handles must be //! scoped per client, validated against the badge*), and this fixture is the //! proof. //! //! It runs as two processes of the same binary, because the hole is a //! *cross-client* one and a single process cannot demonstrate it: //! //! - the **owner** (no arguments — the scenario spawns this one) opens a file //! on the volume and creates a layer, keeps both, and spawns //! - the **intruder** (the two ids as argv), which opens a file and a layer of //! its own and then names the owner's. //! //! Every refusal is paired with the same operation on the intruder's own id, so //! the case cannot pass against a provider that refuses everything; and the owner //! re-uses both ids after the intruder has finished — including after its attempt //! to *close* the file node — so the case cannot pass against a provider that //! honoured the intruder and merely reported failure. The pass marker is //! `badge-scope-test: ok` from the intruder plus `badge-scope-test: owner intact` //! from the owner. //! //! What it cannot check: which id the intruder was refused *for*. A refusal is //! deliberately identical to "no such id" — that is the discipline being tested — //! so the fixture proves the boundary by construction (an id it was told about by //! its parent, which is holding it) rather than by reading anything back. const std = @import("std"); const display = @import("display-client"); const fs = @import("file-system"); const ipc = @import("ipc"); const logging = @import("logging"); const process = @import("process"); const time = @import("time"); /// A scratch file on the volume, so the node the intruder tries to write through /// is one nothing else reads. (A foreign write that *succeeded* would prove the /// bug — it must not also damage the boot volume proving it.) const held_path = "/volumes/usb/BADGE.TXT"; const held_contents = "held"; fn line(comptime format: []const u8, arguments: anytype) void { var buffer: [192]u8 = undefined; _ = logging.write(std.fmt.bufPrint(&buffer, format, arguments) catch return); } /// The fat server mounts /volumes/usb only after the whole USB storage chain is /// up, and both instances race it. fn waitForVolume() bool { var tries: u32 = 0; while (tries < 1400) : (tries += 1) { if (fs.openDirectory("/volumes/usb")) |opened| { var directory = opened; directory.close(); return true; } time.sleepMillis(50); } return false; } pub fn main(init: process.Init) void { if (init.arguments.get(1)) |node_text| { const layer_text = init.arguments.get(2) orelse { _ = logging.write("badge-scope-test: FAILED (intruder without a layer id)\n"); return; }; const node = std.fmt.parseInt(u64, node_text, 10) catch return; const layer = std.fmt.parseInt(u32, layer_text, 10) catch return; intrude(node, layer); return; } own(); } // --- the owner --------------------------------------------------------------- fn own() void { if (!waitForVolume()) { _ = logging.write("badge-scope-test: FAILED (/volumes/usb never became available)\n"); return; } var held = fs.open(held_path, .{ .create = true, .truncate = true }) orelse { _ = logging.write("badge-scope-test: FAILED (owner could not create its file)\n"); return; }; if ((held.writeAll(held_contents) orelse 0) != held_contents.len) { _ = logging.write("badge-scope-test: FAILED (owner could not write its file)\n"); return; } const layer = display.createLayer(240, 40, 32, 32, 3) orelse { _ = logging.write("badge-scope-test: FAILED (owner could not create a layer)\n"); return; }; _ = layer.fill(0, 0, 32, 32, display.color(0x20, 0x80, 0xC0)); line("badge-scope-test: owner holds node {d} and layer {d}\n", .{ held.node, layer.id }); // The intruder is told both ids outright: guessing them is not what is being // tested (they are small integers — a prober would simply enumerate), and a // fixture that had to search would be asserting on a race instead of on the // rule. var node_text: [24]u8 = undefined; var layer_text: [12]u8 = undefined; const arguments = [_][]const u8{ std.fmt.bufPrint(&node_text, "{d}", .{held.node}) catch return, std.fmt.bufPrint(&layer_text, "{d}", .{layer.id}) catch return, }; const endpoint = ipc.createIpcEndpoint() orelse { _ = logging.write("badge-scope-test: FAILED (owner has no exit endpoint)\n"); return; }; const intruder = process.spawnSupervised("badge-scope-test", &arguments, endpoint) orelse { _ = logging.write("badge-scope-test: FAILED (could not spawn the intruder)\n"); return; }; var receive: [8]u8 = undefined; while (true) { const got = ipc.replyWait(endpoint, &.{}, &receive, null); if (got.isChildExit() and got.childProcessId() == intruder) break; } // Both ids must still be the owner's, and still work — the half of the proof // the intruder cannot give, because a provider that had honoured its close or // its destroy would have answered it exactly as one that refused. held.seekTo(0); var buffer: [16]u8 = undefined; const read = held.read(&buffer) orelse 0; const node_intact = read == held_contents.len and std.mem.eql(u8, buffer[0..read], held_contents); const layer_intact = layer.fill(0, 0, 32, 32, display.color(0x20, 0xC0, 0x80)); if (node_intact and layer_intact) { _ = logging.write("badge-scope-test: owner intact\n"); } else { line("badge-scope-test: FAILED (owner lost its ids: node={} layer={})\n", .{ node_intact, layer_intact }); } _ = layer.destroy(); held.close(); } // --- the intruder ------------------------------------------------------------- fn intrude(foreign_node: u64, foreign_layer: u32) void { if (!waitForVolume()) { _ = logging.write("badge-scope-test: FAILED (/volumes/usb never became available)\n"); return; } const node_verdict = probeNode(foreign_node); const layer_verdict = probeLayer(foreign_layer); if (node_verdict and layer_verdict) { _ = logging.write("badge-scope-test: ok\n"); } else { _ = logging.write("badge-scope-test: FAILED\n"); } } /// The FAT half: the intruder's own node works, the owner's does not. fn probeNode(foreign: u64) bool { var mine = fs.open(held_path, .{}) orelse { _ = logging.write("badge-scope-test: FAILED (intruder could not open its own file)\n"); return false; }; defer mine.close(); if (mine.backend == null or mine.node == foreign) { line("badge-scope-test: FAILED (intruder's node {d} is not distinct from {d})\n", .{ mine.node, foreign }); return false; } var buffer: [16]u8 = undefined; const own_read = (mine.read(&buffer) orelse 0) == held_contents.len; // The same backend channel, the same verbs, one different integer. Every one // of these worked before the owner check went in. var forged = fs.File{ .node = foreign, .backend = mine.backend }; const read_refused = forged.read(&buffer) == null; const status_refused = forged.attributes() == null; const write_refused = forged.write("intruded") == null; forged.close(); // must not release the owner's node — the owner proves that after we exit mine.seekTo(0); const own_read_again = (mine.read(&buffer) orelse 0) == held_contents.len; const ok = own_read and own_read_again and read_refused and status_refused and write_refused; line("badge-scope-test: node own={} read_refused={} status_refused={} write_refused={} own_again={}\n", .{ own_read, read_refused, status_refused, write_refused, own_read_again, }); return ok; } /// The display half: the intruder's own layer obeys it, the owner's ignores it. fn probeLayer(foreign: u32) bool { const mine = display.createLayer(200, 40, 32, 32, 4) orelse { _ = logging.write("badge-scope-test: FAILED (intruder could not create a layer)\n"); return false; }; if (mine.id == foreign) { line("badge-scope-test: FAILED (intruder's layer {d} is not distinct)\n", .{mine.id}); return false; } const own_fill = mine.fill(0, 0, 32, 32, display.color(0xC0, 0x40, 0x40)); const own_configure = mine.configure(200, 80, 4, true); const forged = display.Layer{ .id = foreign }; const fill_refused = !forged.fill(0, 0, 32, 32, display.color(0xFF, 0, 0)); const configure_refused = !forged.configure(0, 0, 9, false); const destroy_refused = !forged.destroy(); const own_still = mine.fill(0, 0, 32, 32, display.color(0x40, 0xC0, 0x40)); _ = mine.destroy(); const ok = own_fill and own_configure and own_still and fill_refused and configure_refused and destroy_refused; line("badge-scope-test: layer own={} fill_refused={} configure_refused={} destroy_refused={} own_again={}\n", .{ own_fill, fill_refused, configure_refused, destroy_refused, own_still, }); return ok; }