danos/test/system/services/badge-scope-test/badge-scope-test.zig

217 lines
9.4 KiB
Zig

//! 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;
}