diff --git a/library/device/block/block.zig b/library/device/block/block.zig index 47c17f5..b03e4e9 100644 --- a/library/device/block/block.zig +++ b/library/device/block/block.zig @@ -7,10 +7,8 @@ //! `runtime.dma.alloc`), so whole sectors move without crossing the IPC size //! limit — the same handoff usb-storage uses toward the controller. -const channel = @import("channel"); const envelope = @import("envelope"); const ipc = @import("ipc"); -const time = @import("time"); const block_protocol = @import("block-protocol"); const Protocol = block_protocol.Protocol; @@ -75,26 +73,7 @@ pub const Device = struct { } }; -/// One open attempt, no waiting — for a server that retries on its own -/// timer (the fat service) instead of blocking its harness in here. -pub fn tryOpen() ?Device { - if (channel.openEndpoint("block")) |handle| return .{ .endpoint = handle }; - return null; -} - -/// Open `/protocol/block`, retrying generously while the USB storage chain -/// (controller reset, enumeration, mass-storage bring-up) comes up. -pub fn open() ?Device { - // Patient: the whole USB storage chain (firmware discovery, xHCI reset and - // enumeration, mass-storage bring-up) must complete first, which can take - // tens of seconds under emulation. - var attempts: usize = 0; - // 30 s covers the slowest observed healthy chain (a flaky QEMU enumeration - // completed at ~24 s); a machine whose stick genuinely failed setup should - // not sit a further minute pretending otherwise. - while (attempts < 600) : (attempts += 1) { - if (channel.openEndpoint("block")) |handle| return .{ .endpoint = handle }; - time.sleepMillis(50); - } - return null; -} +// There is deliberately no open-by-name here: `block` is not a registry name. +// One storage process serves each volume, and a consumer receives its volume's +// channel from the device manager (establishment by lineage, communication.md +// "Establishment: two planes"), then wraps it: `block.Device{ .endpoint = c }`. diff --git a/library/device/driver/driver.zig b/library/device/driver/driver.zig index 45c6c37..6628606 100644 --- a/library/device/driver/driver.zig +++ b/library/device/driver/driver.zig @@ -270,18 +270,21 @@ pub const Exchange = struct { }; /// A consumer's whole establishment step: hello until the channel to this -/// device's provider arrives. The manager acks a hello whose provider is not -/// there yet (mid-restart, re-report on the way) with no channel — retryable -/// by design — so this re-hellos on the ONE manager handle, on the same -/// cadence the old name lookup used, and gives up on a refusal or a vanished -/// manager. Re-hello is benign: the manager just re-marks the entry running. -pub fn helloForChannel(role: Role, device_id: u64) ?ipc.Handle { - const first = helloExchange(role, device_id, null, true) orelse return null; +/// device's provider arrives. `serving` (a provider-and-consumer like +/// usb-storage: block endpoint up, bus channel down) rides the FIRST exchange +/// only — the manager keeps it, so retries need not resend it. The manager +/// acks a hello whose provider is not there yet (mid-restart, re-report on +/// the way) with no channel — retryable by design — so this re-hellos on the +/// ONE manager handle, on the same cadence the old name lookup used, and +/// gives up on a refusal or a vanished manager. Re-hello is benign: the +/// manager just re-marks the entry running. +pub fn helloForChannel(role: Role, device_id: u64, serving: ?ipc.Handle) ?ipc.Handle { + const first = helloExchange(role, device_id, serving, true) orelse return null; if (first.channel) |bus| return bus; var attempts: u32 = 0; while (attempts < lookup_attempts) : (attempts += 1) { time.sleepMillis(lookup_pause_ms); - const again = exchangeOn(first.manager, role, device_id, null, true) orelse return null; + const again = helloOn(first.manager, role, device_id, null, true) orelse return null; if (again.channel) |bus| return bus; } std.log.info("no provider channel for device {d}", .{device_id}); @@ -303,12 +306,14 @@ pub fn helloExchange(role: Role, device_id: u64, serving: ?ipc.Handle, want_chan std.log.info("no device manager to hello", .{}); return null; }; - return exchangeOn(manager, role, device_id, serving, want_channel); + return helloOn(manager, role, device_id, serving, want_channel); } /// One hello on an already-open manager handle — the exchange without the /// lookup, so a retry loop never spends a handle-table slot per attempt. -fn exchangeOn(manager: ipc.Handle, role: Role, device_id: u64, serving: ?ipc.Handle, want_channel: bool) ?Exchange { +/// Public for parties that keep their own manager handle across a long retry +/// cadence (fat polls for its volume on a timer). +pub fn helloOn(manager: ipc.Handle, role: Role, device_id: u64, serving: ?ipc.Handle, want_channel: bool) ?Exchange { var packet: [device_manager_protocol.message_maximum]u8 = undefined; const framed = device_manager_protocol.Protocol.encodeRequest( .hello, diff --git a/library/protocol/device-manager/device-manager-protocol.zig b/library/protocol/device-manager/device-manager-protocol.zig index 2ab1aeb..279513d 100644 --- a/library/protocol/device-manager/device-manager-protocol.zig +++ b/library/protocol/device-manager/device-manager-protocol.zig @@ -63,6 +63,11 @@ pub const Role = enum(u8) { bus = 1, /// Serves one device, reached through a bus's transfer protocol. device = 2, + /// Not a spawned driver at all: a party asking for the channel of the + /// driver BOUND TO the target device (a service consuming a driver-layer + /// contract — fat asking for its volume's block provider). No deadline, no + /// state, no driver entry; just establishment by lineage. + consumer = 3, }; // --- the per-operation request parts ---------------------------------------- @@ -160,10 +165,18 @@ pub const ChildEntry = extern struct { parent: u64, bus_address: u64, identity: u64, + /// The kernel device id this child was registered as (`no_device` for an + /// unregistered leaf) — what a `.consumer` hello names as its target to be + /// routed to the driver bound to this child. + device_id: u64, }; -/// How many `ChildEntry` records one `enumerate` reply can carry. Paging joins -/// the protocol if a tree ever outgrows one packet. +/// How many `ChildEntry` records one `enumerate` reply can carry. The verb is +/// PAGED: the request's `Header.target` is the start index (skip that many +/// known children), and a short or empty page means the tree is exhausted — a +/// real tree outgrew one packet the day ACPI reported a dozen nodes before +/// the first USB child, and an unpaged reply silently truncated exactly the +/// entries a consumer was looking for. pub const entries_per_reply: usize = (envelope.packet_maximum - envelope.prefix_size) / @sizeOf(ChildEntry); pub const Protocol = envelope.Define(.{ diff --git a/system/configuration/protocol.csv b/system/configuration/protocol.csv index 13123e5..c2cbd0b 100644 --- a/system/configuration/protocol.csv +++ b/system/configuration/protocol.csv @@ -63,12 +63,11 @@ /system/services/discovery, /system/services/device-manager, bind, power # --- the drivers, which the device manager spawns --------------------------- -# usb-transfer has NO bind row: several bus processes provide it (one per -# controller), so it is never a registry name — consumers get their controller's -# channel from the device manager's hello, routed by lineage (communication.md -# "Establishment: two planes, one namespace"). +# usb-transfer and block have NO bind rows: several processes provide each (one +# per controller, one per volume), so neither is ever a registry name — +# consumers get their provider's channel from the device manager's hello, +# routed by lineage (communication.md "Establishment: two planes"). /system/drivers/ps2-bus, /system/services/device-manager, bind, ps2-bus -/system/drivers/usb-storage, /system/services/device-manager, bind, block /system/drivers/virtio-gpu, /system/services/device-manager, bind, scanout # --- the same providers when the kernel test harness starts them directly --- @@ -95,11 +94,12 @@ # ============================================================================ # --- init's own services ---------------------------------------------------- -# fat reaches the block device behind the volume it mounts; the compositor -# reaches the scanout its driver announced, its own endpoint (the mouse-listener -# thread opens /protocol/display like any other client — threads share no -# handles), and the input stream that moves the cursor. -/system/services/fat, /system/services/init, open, block +# fat reaches the device manager to be routed to its volume's block provider +# (block is not a name — see the bind section); the compositor reaches the +# scanout its driver announced, its own endpoint (the mouse-listener thread +# opens /protocol/display like any other client — threads share no handles), +# and the input stream that moves the cursor. +/system/services/fat, /system/services/init, open, device-manager /system/services/display, /system/services/init, open, scanout /system/services/display, /system/services/init, open, display /system/services/display, /system/services/init, open, input diff --git a/system/drivers/usb-hid/keyboard.zig b/system/drivers/usb-hid/keyboard.zig index 30b295e..4887fce 100644 --- a/system/drivers/usb-hid/keyboard.zig +++ b/system/drivers/usb-hid/keyboard.zig @@ -77,7 +77,7 @@ pub fn main(init: process.Init) void { // The hello both meets the spawn deadline and brings back the channel to // THIS device's controller — routed by lineage, never found by name. - const bus = device_manager.helloForChannel(.device, device_id) orelse return; + const bus = device_manager.helloForChannel(.device, device_id, null) orelse return; var device = usb.open(bus, device_id) orelse { std.log.info("could not open device {d}", .{device_id}); return; diff --git a/system/drivers/usb-hid/mouse.zig b/system/drivers/usb-hid/mouse.zig index a214f76..aefcce4 100644 --- a/system/drivers/usb-hid/mouse.zig +++ b/system/drivers/usb-hid/mouse.zig @@ -43,7 +43,7 @@ pub fn main(init: process.Init) void { // The hello both meets the spawn deadline and brings back the channel to // THIS device's controller — routed by lineage, never found by name. - const bus = device_manager.helloForChannel(.device, device_id) orelse return; + const bus = device_manager.helloForChannel(.device, device_id, null) orelse return; var device = usb.open(bus, device_id) orelse { std.log.info("could not open device {d}", .{device_id}); return; diff --git a/system/drivers/usb-storage/usb-storage.zig b/system/drivers/usb-storage/usb-storage.zig index 2a1810f..efa49b3 100644 --- a/system/drivers/usb-storage/usb-storage.zig +++ b/system/drivers/usb-storage/usb-storage.zig @@ -82,10 +82,13 @@ fn transact(cdb: []const u8, direction_in: bool, data_physical: u64, data_length var bring_up_failed = false; fn initialise(endpoint: ipc.Handle) bool { - _ = endpoint; - // The hello both meets the spawn deadline and brings back the channel to - // THIS device's controller — routed by lineage, never found by name. - const bus = device_manager.helloForChannel(.device, device_id) orelse return false; + // One hello, both directions: the block-serving endpoint goes UP (the + // manager routes fat's consumer hello here — this driver serves one + // volume, one process per stick, so `block` is never a registry name), + // and the channel to THIS device's controller comes DOWN, routed by + // lineage. A second stick used to die silently on the exclusive bind; + // now every instance is reachable through its lineage. + const bus = device_manager.helloForChannel(.device, device_id, endpoint) orelse return false; device = usb.open(bus, device_id) orelse { std.log.info("could not open device {d}", .{device_id}); return false; @@ -224,8 +227,10 @@ pub fn main(init: process.Init) void { std.log.info("malformed device id '{s}'", .{argument}); return; }; + // No `.service` name: several instances provide the block contract (one + // per stick), so consumers are routed here by the device manager's + // lineage, never by a registry bind — the endpoint goes up in the hello. service.run(block_protocol.message_maximum, .{ - .service = "block", .init = initialise, .on_message = onMessage, }); diff --git a/system/services/device-manager/device-manager.zig b/system/services/device-manager/device-manager.zig index 412e276..b4c2cf9 100644 --- a/system/services/device-manager/device-manager.zig +++ b/system/services/device-manager/device-manager.zig @@ -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; } diff --git a/system/services/fat/build.zig b/system/services/fat/build.zig index 1df42bb..039af65 100644 --- a/system/services/fat/build.zig +++ b/system/services/fat/build.zig @@ -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); diff --git a/system/services/fat/fat.zig b/system/services/fat/fat.zig index e62633c..6b81fd6 100644 --- a/system/services/fat/fat.zig +++ b/system/services/fat/fat.zig @@ -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;