//! block-range-test — the discrimination fixture for per-sender range //! confinement (V2a, docs/volume-manager-plan.md). It gets a block channel the //! way a filesystem does (consumer-hello the device manager for the mass-storage //! provider), then proves the two properties the clamp exists for: //! //! 1. an UNCONFINED caller may define a range on its own badge (the volume //! manager is unconfined — this stands in for it); //! 2. once confined, a transfer PAST the range is refused, and the volume //! relative LBA 0 maps inside the range (the clamp translates + bounds); //! 3. a CONFINED caller may NOT call define_range again (the gate — a //! filesystem cannot widen its own range or escape). //! //! Against pre-clamp usb-storage the verb does not exist, so (1) already fails — //! which is exactly the discrimination: the fixture cannot even arm confinement, //! let alone see a transfer refused for crossing it. //! //! It coexists with the FAT service in the same boot: ranges are per-badge, so //! confining THIS process touches nothing fat does on its own channel. const std = @import("std"); const channel = @import("channel"); const device_manager_protocol = @import("device-manager-protocol"); const driver = @import("driver"); const ipc = @import("ipc"); const block = @import("block"); const memory = @import("memory"); const logging = @import("logging"); const process = @import("process"); const time = @import("time"); const envelope = @import("envelope"); fn verdict(ok: bool, name: []const u8) void { _ = logging.write("block-range: "); _ = logging.write(if (ok) "ok " else "FAILED "); _ = logging.write(name); _ = logging.write("\n"); } /// The mass-storage provider's block channel, via the device manager's tree — /// the same lineage acquisition the FAT service uses (block is not a name). fn acquireBlock() ?block.Device { var tries: u32 = 0; const manager = while (tries < 200) : (tries += 1) { if (channel.openEndpoint("device-manager")) |h| break h; time.sleepMillis(20); } else return null; // The whole USB storage chain (enumeration, bring-up) takes a few seconds to // appear in the manager's tree, so retry the enumerate-and-hello with a pause // between rounds — 500 x 20 ms ~ 10 s, well within the case timeout. const Entry = device_manager_protocol.ChildEntry; var attempt: u32 = 0; while (attempt < 500) : (attempt += 1) { var start: u64 = 0; while (true) { const enumerate = envelope.Header{ .operation = envelope.operation_enumerate, .target = start }; var reply: [device_manager_protocol.message_maximum]u8 = undefined; const length = ipc.call(manager, std.mem.asBytes(&enumerate), &reply) catch break; const status = envelope.statusOf(reply[0..length]) orelse break; if (status.status != 0) break; const carried = @min(@as(usize, status.len), length -| envelope.prefix_size); const tail = reply[envelope.prefix_size..][0..carried]; const count = tail.len / @sizeOf(Entry); if (count == 0) break; var index: usize = 0; while (index < count) : (index += 1) { const entry = std.mem.bytesToValue(Entry, tail[index * @sizeOf(Entry) ..][0..@sizeOf(Entry)]); if (entry.device_id == device_manager_protocol.no_device) continue; if ((entry.identity >> 16) & 0xff != 0x08 or (entry.identity >> 8) & 0xff != 0x06) continue; const exchanged = driver.helloOn(manager, .consumer, entry.device_id, null, true) orelse break; const provider = exchanged.channel orelse continue; return .{ .endpoint = provider }; } start += count; } time.sleepMillis(20); } return null; } pub fn main(init: process.Init) void { // Bundled fixtures are swept up and spawned bare on every boot; stay silent // unless the kernel test explicitly runs us, or we would contend for the // block channel and print markers into unrelated cases. const arg = init.arguments.get(1) orelse return; if (!std.mem.eql(u8, arg, "run")) return; const device = acquireBlock() orelse { verdict(false, "acquire-block"); return; }; const geometry = device.geometry() orelse { verdict(false, "geometry"); return; }; // Need at least a few blocks to carve a range out of; every FAT image is far // larger, so this only guards a nonsense device. if (geometry.block_count < 4) { verdict(false, "device-too-small"); return; } // A one-block DMA buffer for the positive-control read. Shareable so it can be // attached under an enforcing IOMMU (a no-op success otherwise). const bounce = memory.dmaAlloc(512, memory.dma_coherent | memory.dma_shareable) orelse { verdict(false, "dma-alloc"); return; }; if (bounce.handle) |handle| { if (!device.attach(handle)) { verdict(false, "attach"); return; } _ = ipc.close(handle); } // Baseline: an unconfined read of block 0 succeeds — so a later refusal is // the clamp, not a broken read path. verdict(device.read(0, 1, bounce.physical), "unconfined-read"); const me = process.taskId(); // (1) An unconfined caller confines itself to blocks [1, 3). Against pre-clamp // usb-storage this verb does not exist and the call fails here. if (!device.defineRange(me, 1, 2)) { verdict(false, "define-range"); return; } verdict(true, "define-range"); // (2) Confined now: volume-relative LBA 0 maps to device block 1 (inside the // range) and succeeds; LBA 2 would reach device block 3, past the 2-block // range, and must be refused. verdict(device.read(0, 1, bounce.physical), "in-range-read"); verdict(!device.read(2, 1, bounce.physical), "out-of-range-refused"); // Geometry now reports the CONFINED size, not the device's. const confined = device.geometry() orelse { verdict(false, "confined-geometry"); return; }; verdict(confined.block_count == 2, "geometry-is-confined"); // (3) The gate: a confined caller cannot define_range — no widening, no escape. verdict(!device.defineRange(me, 0, geometry.block_count), "confined-cannot-redefine"); _ = logging.write("block-range: VERDICT done\n"); }