establishment: block stops being a name, and enumerate learns to page

P2 of docs/establishment-planes-plan.md. usb-storage serves nameless — one
process per stick cannot share an exclusive bind, and a second stick used to
die silently on -EBUSY before ever helloing. Its one hello now moves both
directions at once: the block-serving endpoint up, its controller channel
down. fat finds its volume through the manager — a new `.consumer` role asks
for the channel of the driver BOUND TO a device (distinct from the device's
reporter), found by enumerating the tree for the mass-storage identity.
fat stays single-volume; the boot-volume-by-content choice is M21.

The conversion immediately caught a live truncation of exactly the audit's
shape: ChildEntry grew to 32 bytes, one enumerate reply holds ~7, and a real
tree carries a dozen ACPI nodes before the first USB child — the storage
entry silently never fit (the protocol comment already said "paging joins
the protocol if a tree ever outgrows one packet"). enumerate is now paged:
Header.target is the start cursor, a short page is the end; device-list's
page-0 read is unchanged.

Grant rows move with the code: the block bind and fat's block open die, fat
gains open device-manager. Gate: 18 cases green including the registry
trio, device-list, and both IOMMU storage variants.
This commit is contained in:
Daniel Samson
2026-08-09 12:24:25 +01:00
parent d603d40b5c
commit 1d7850239d
10 changed files with 155 additions and 61 deletions
@@ -99,10 +99,19 @@ fn identityFromReport(report: device_manager_protocol.ChildAdded) registry.Ident
/// Whether some driver entry already serves registered device `device_id` —
/// a re-report after a bus restart must not spawn a second instance.
fn driverForDevice(device_id: u64) bool {
return driverEntryForDevice(device_id) != null;
}
/// The driver entry BOUND TO a device — matched and spawned for it. Distinct
/// from the device's reporter: a mouse's provider is the bus that reported it
/// (lineage via `children[].reporter`), while a volume's provider is the
/// storage driver spawned FOR it — which is what a `.consumer` hello asks for.
fn driverEntryForDevice(device_id: u64) ?*Driver {
if (device_id == device_manager_protocol.no_device) return null;
for (&drivers) |*driver| {
if (driver.used and driver.device_id == device_id) return true;
if (driver.used and driver.device_id == device_id) return driver;
}
return false;
return null;
}
// --- supervision -------------------------------------------------------------
@@ -509,6 +518,24 @@ fn onHello(_: void, invocation: Invocation(device_manager_protocol.Hello), _: An
std.log.info("refused hello (version {d}) from process {d}", .{ invocation.request.version, invocation.sender });
return -envelope.EPROTO;
}
// A consumer is not a spawned driver: no entry, no deadline, no state —
// just establishment. It asks for the channel of the driver BOUND TO its
// target (fat asking for its volume's block provider). No channel is a
// retryable ack, exactly as for a device-role consumer.
//
// The residual, stated plainly: any process granted `open device-manager`
// can ask. The grant rows are the gate today, as they were when the block
// name was open-granted; a finer per-channel policy belongs to the same
// future as the spawn capability (device-authority.md).
if (invocation.request.role == @intFromEnum(device_manager_protocol.Role.consumer)) {
if (invocation.request.wants_channel != 0) {
if (driverEntryForDevice(invocation.target)) |provider| {
if (provider.endpoint) |serving| service.replyWithCapability(serving);
}
}
return 0;
}
const driver = driverByProcess(invocation.sender) orelse {
std.log.info("hello from unknown process {d}", .{invocation.sender});
return -envelope.EPERM;
@@ -664,14 +691,24 @@ fn onChildRemoved(_: void, invocation: Invocation(device_manager_protocol.ChildR
/// The reserved `enumerate` verb: the mirror, one `ChildEntry` per known child,
/// packed into the reply's tail. How many arrived is the reply's own length —
/// `Status.len` — so no count header is spent saying it twice.
fn onEnumerate(_: void, _: Invocation(void), answer: Answer(void)) isize {
fn onEnumerate(_: void, invocation: Invocation(void), answer: Answer(void)) isize {
const entry_size = @sizeOf(device_manager_protocol.ChildEntry);
const tail = answer.tail();
// `target` is the page cursor: skip that many known children first. One
// reply holds only a handful of entries, and a real tree (a dozen ACPI
// nodes before the first USB child) outgrew one packet — the storage
// child silently never fit, which is precisely the truncation shape the
// bounds audit exists to forbid. A caller pages until a short page.
var skip = invocation.target;
var written: usize = 0;
for (&children) |*child| {
if (!child.used) continue;
if (skip > 0) {
skip -= 1;
continue;
}
if (written + entry_size > tail.len) break;
const entry = device_manager_protocol.ChildEntry{ .parent = child.parent, .bus_address = child.bus_address, .identity = child.identity };
const entry = device_manager_protocol.ChildEntry{ .parent = child.parent, .bus_address = child.bus_address, .identity = child.identity, .device_id = child.device_id };
@memcpy(tail[written..][0..entry_size], std.mem.asBytes(&entry));
written += entry_size;
}
+4 -2
View File
@@ -10,8 +10,10 @@ pub fn build(b: *std.Build) void {
.name = "fat",
.root_source_file = b.path("fat.zig"),
.imports = &.{
"block", "envelope", "file-system", "ipc", "logging", "memory", "process",
"service", "time", "vfs-protocol",
"block", "channel", "device-manager-protocol", "driver",
"envelope", "file-system", "ipc", "logging",
"memory", "process", "service", "time",
"vfs-protocol",
},
});
b.installArtifact(exe);
+54 -1
View File
@@ -10,6 +10,9 @@
//! it.
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 process = @import("process");
const service = @import("service");
@@ -116,6 +119,56 @@ const mount_retry_ms = 500;
var mounted = false;
var service_endpoint: ipc.Handle = 0;
/// The one channel to the device manager, opened on first need and kept — the
/// poll retries on it, never spending a handle-table slot per attempt.
var manager_handle: ?ipc.Handle = null;
/// Find the volume's provider through the device manager (establishment by
/// lineage, communication.md "Establishment: two planes" — `block` is not a
/// registry name; one storage process serves each stick): enumerate the
/// manager's tree, take the FIRST usb mass-storage child by enumeration order
/// (deterministic within a boot; single-volume by construction, and choosing
/// the BOOT volume by content when two sticks are present is the M21 remount
/// track), and consumer-hello for the channel of the driver bound to it.
/// Null until the chain is up — the caller's poll retries.
fn acquireVolume() ?block.Device {
const manager = manager_handle orelse opened: {
const handle = channel.openEndpoint("device-manager") orelse return null;
manager_handle = handle;
break :opened handle;
};
// The envelope's reserved `enumerate` verb, PAGED: one reply carries only
// a handful of entries and a real tree (a dozen ACPI nodes before the
// first USB child) is bigger, so `Header.target` is the start cursor and
// a short page is the end. Identity is the bus's native triple, for USB
// (base << 16) | (class << 8) | protocol — mass storage is base 0x08,
// subclass 0x06 (SCSI transparent), the same key devices.csv matches on.
const Entry = device_manager_protocol.ChildEntry;
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 return null;
const status = envelope.statusOf(reply[0..length]) orelse return null;
if (status.status != 0) return null;
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) return null; // the tree is exhausted; no volume yet
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 return null;
const provider = exchanged.channel orelse continue; // its driver not up yet — next tick
return .{ .endpoint = provider };
}
start += count;
}
}
fn initialise(endpoint: ipc.Handle) bool {
service_endpoint = endpoint;
@@ -133,7 +186,7 @@ fn initialise(endpoint: ipc.Handle) bool {
/// `mounted` on success; a failure leaves everything untouched for the next tick.
fn tryBringUp() void {
if (mounted) return;
const device = block.tryOpen() orelse return;
const device = acquireVolume() orelse return;
const geometry = device.geometry() orelse {
_ = logging.write("/system/services/fat: block geometry unavailable\n");
return;