217 lines
9.4 KiB
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;
|
|
}
|