establishment: usb-transfer stops being a name

P1 of docs/establishment-planes-plan.md. The bus no longer binds
/protocol/usb-transfer — the bind race made whichever instance came second
unreachable, which on a real three-controller Ryzen meant a mouse no class
driver could reach ("could not open device 50"). Each instance hands its
serving endpoint up in the hello that already delegates its controller, and
class drivers receive their OWN controller's channel from helloForChannel —
routed by the manager's lineage, retried while a provider is mid-restart,
on one manager handle so retries never spend handle-table slots.

usb.open(bus, id) now takes the channel it used to look up; the name rows
leave protocol.csv with the code (bind 67, opens 119/120/122); the
conformance fixture's prose stops claiming the bus binds; and the stale
input-client import leaves the bus with the channel one.

Gate: 19 QEMU cases green (usb family, hubs, both IOMMU variants, fat chain,
boot-from-USB, orderly shutdown, conformance).
This commit is contained in:
Daniel Samson
2026-08-09 11:48:31 +01:00
parent 77fe4d220e
commit d603d40b5c
10 changed files with 84 additions and 67 deletions
+24
View File
@@ -269,6 +269,25 @@ pub const Exchange = struct {
channel: ?ipc.Handle, channel: ?ipc.Handle,
}; };
/// 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;
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;
if (again.channel) |bus| return bus;
}
std.log.info("no provider channel for device {d}", .{device_id});
return null;
}
/// The full handshake (communication.md "Establishment: two planes, one /// The full handshake (communication.md "Establishment: two planes, one
/// namespace"): a provider hands `serving` — the endpoint its consumers will /// namespace"): a provider hands `serving` — the endpoint its consumers will
/// be routed to — up with the request; a consumer sets `want_channel` and /// be routed to — up with the request; a consumer sets `want_channel` and
@@ -284,7 +303,12 @@ pub fn helloExchange(role: Role, device_id: u64, serving: ?ipc.Handle, want_chan
std.log.info("no device manager to hello", .{}); std.log.info("no device manager to hello", .{});
return null; return null;
}; };
return exchangeOn(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 {
var packet: [device_manager_protocol.message_maximum]u8 = undefined; var packet: [device_manager_protocol.message_maximum]u8 = undefined;
const framed = device_manager_protocol.Protocol.encodeRequest( const framed = device_manager_protocol.Protocol.encodeRequest(
.hello, .hello,
+14 -16
View File
@@ -5,12 +5,16 @@
//! service and `device.zig` over the raw device calls. //! service and `device.zig` over the raw device calls.
//! //!
//! A class driver, spawned with its interface's assigned device id as argv[1]: //! A class driver, spawned with its interface's assigned device id as argv[1]:
//! if (device_manager.hello(.device, id) == null) return; // meet the spawn deadline //! const bus = device_manager.helloForChannel(.device, id) orelse return;
//! var device = usb.open(id) orelse return; // open + get its endpoints //! var device = usb.open(bus, id) orelse return; // open + get its endpoints
//! _ = device.controlOut(usb_abi.setProtocol(...));// class requests, descriptors //! _ = device.controlOut(usb_abi.setProtocol(...));// class requests, descriptors
//! _ = device.subscribeInterrupt(address, length); // reports arrive asynchronously //! _ = device.subscribeInterrupt(address, length); // reports arrive asynchronously
//! while (true) { ... ipc.replyWait(device.endpoint, ...) ... } // its own loop //! while (true) { ... ipc.replyWait(device.endpoint, ...) ... } // its own loop
//! //!
//! The bus channel arrives from the device manager's hello — routed to THIS
//! device's controller by lineage — never from a registry name; several bus
//! processes provide this contract on a multi-controller machine.
//!
//! Reports are delivered to `device.endpoint` as asynchronous `interrupt_report` //! Reports are delivered to `device.endpoint` as asynchronous `interrupt_report`
//! event packets, decoded with `reportOf` (the class driver runs a bare `replyWait` //! event packets, decoded with `reportOf` (the class driver runs a bare `replyWait`
//! loop to read them, because the service harness drops buffered-message payloads //! loop to read them, because the service harness drops buffered-message payloads
@@ -21,10 +25,8 @@
//! a control transfer's data stage in the tail. //! a control transfer's data stage in the tail.
const std = @import("std"); const std = @import("std");
const channel = @import("channel");
const envelope = @import("envelope"); const envelope = @import("envelope");
const ipc = @import("ipc"); const ipc = @import("ipc");
const time = @import("time");
const usb_transfer_protocol = @import("usb-transfer-protocol"); const usb_transfer_protocol = @import("usb-transfer-protocol");
const Protocol = usb_transfer_protocol.Protocol; const Protocol = usb_transfer_protocol.Protocol;
@@ -157,18 +159,14 @@ pub fn reportOf(packet: []const u8) ?InterruptReport {
return Protocol.decodeEvent(.interrupt_report, packet); return Protocol.decodeEvent(.interrupt_report, packet);
} }
/// Open `/protocol/usb-transfer` and, on that channel, open the device with the /// On `bus` — the channel to this device's own controller, handed to the class
/// assigned id, handing over a freshly created endpoint for asynchronous interrupt /// driver by the device manager's hello (establishment by lineage,
/// reports. Retries while the bus is still coming up (a class driver races the bus /// communication.md "Establishment: two planes") — open the device with the
/// driver at boot). Two opens, deliberately: the first names the contract, the /// assigned id, handing over a freshly created endpoint for asynchronous
/// second names an object within it. /// interrupt reports. The channel is never found by name: one machine carries
pub fn open(device_id: u64) ?Device { /// several controllers, several processes provide this contract, and only the
var attempts: usize = 0; /// manager knows which one reported this device.
const bus = while (attempts < 100) : (attempts += 1) { pub fn open(bus: ipc.Handle, device_id: u64) ?Device {
if (channel.openEndpoint("usb-transfer")) |handle| break handle;
time.sleepMillis(20);
} else return null;
const endpoint = ipc.createIpcEndpoint() orelse return null; const endpoint = ipc.createIpcEndpoint() orelse return null;
// The assigned device id is the target: it is what the caller has before a // The assigned device id is the target: it is what the caller has before a
// token exists, and the token the reply hands back addresses every packet // token exists, and the token the reply hands back addresses every packet
+4 -4
View File
@@ -63,8 +63,11 @@
/system/services/discovery, /system/services/device-manager, bind, power /system/services/discovery, /system/services/device-manager, bind, power
# --- the drivers, which the device manager spawns --------------------------- # --- 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").
/system/drivers/ps2-bus, /system/services/device-manager, bind, ps2-bus /system/drivers/ps2-bus, /system/services/device-manager, bind, ps2-bus
/system/drivers/usb-xhci-bus, /system/services/device-manager, bind, usb-transfer
/system/drivers/usb-storage, /system/services/device-manager, bind, block /system/drivers/usb-storage, /system/services/device-manager, bind, block
/system/drivers/virtio-gpu, /system/services/device-manager, bind, scanout /system/drivers/virtio-gpu, /system/services/device-manager, bind, scanout
@@ -116,10 +119,7 @@
# to the compositor. # to the compositor.
/system/drivers/*, /system/services/device-manager, open, device-manager /system/drivers/*, /system/services/device-manager, open, device-manager
/system/services/discovery, /system/services/device-manager, open, device-manager /system/services/discovery, /system/services/device-manager, open, device-manager
/system/drivers/usb-storage, /system/services/device-manager, open, usb-transfer
/system/drivers/usb-hid-keyboard, /system/services/device-manager, open, usb-transfer
/system/drivers/usb-hid-keyboard, /system/services/device-manager, open, input /system/drivers/usb-hid-keyboard, /system/services/device-manager, open, input
/system/drivers/usb-hid-mouse, /system/services/device-manager, open, usb-transfer
/system/drivers/usb-hid-mouse, /system/services/device-manager, open, input /system/drivers/usb-hid-mouse, /system/services/device-manager, open, input
/system/drivers/virtio-gpu, /system/services/device-manager, open, display /system/drivers/virtio-gpu, /system/services/device-manager, open, display
Can't render this file because it contains an unexpected character in line 12 and column 15.
+4 -3
View File
@@ -75,9 +75,10 @@ pub fn main(init: process.Init) void {
}; };
const layout = xkb.byName(init.arguments.get(2) orelse "us") orelse xkb.us; const layout = xkb.byName(init.arguments.get(2) orelse "us") orelse xkb.us;
// Hello the manager first (meet the spawn deadline), then open the device. // The hello both meets the spawn deadline and brings back the channel to
if (device_manager.hello(.device, device_id) == null) return; // THIS device's controller — routed by lineage, never found by name.
var device = usb.open(device_id) orelse { const bus = device_manager.helloForChannel(.device, device_id) orelse return;
var device = usb.open(bus, device_id) orelse {
std.log.info("could not open device {d}", .{device_id}); std.log.info("could not open device {d}", .{device_id});
return; return;
}; };
+4 -2
View File
@@ -41,8 +41,10 @@ pub fn main(init: process.Init) void {
return; return;
}; };
if (device_manager.hello(.device, device_id) == null) return; // The hello both meets the spawn deadline and brings back the channel to
var device = usb.open(device_id) orelse { // THIS device's controller — routed by lineage, never found by name.
const bus = device_manager.helloForChannel(.device, device_id) orelse return;
var device = usb.open(bus, device_id) orelse {
std.log.info("could not open device {d}", .{device_id}); std.log.info("could not open device {d}", .{device_id});
return; return;
}; };
+4 -2
View File
@@ -83,8 +83,10 @@ var bring_up_failed = false;
fn initialise(endpoint: ipc.Handle) bool { fn initialise(endpoint: ipc.Handle) bool {
_ = endpoint; _ = endpoint;
if (device_manager.hello(.device, device_id) == null) return false; // The hello both meets the spawn deadline and brings back the channel to
device = usb.open(device_id) orelse { // THIS device's controller — routed by lineage, never found by name.
const bus = device_manager.helloForChannel(.device, device_id) orelse return false;
device = usb.open(bus, device_id) orelse {
std.log.info("could not open device {d}", .{device_id}); std.log.info("could not open device {d}", .{device_id});
return false; return false;
}; };
+4 -4
View File
@@ -10,10 +10,10 @@ pub fn build(b: *std.Build) void {
.name = "usb-xhci-bus", .name = "usb-xhci-bus",
.root_source_file = b.path("usb-xhci-bus.zig"), .root_source_file = b.path("usb-xhci-bus.zig"),
.imports = &.{ .imports = &.{
"channel", "device-manager-protocol", "driver", "envelope", "device-manager-protocol", "driver", "envelope", "ipc",
"input-client", "ipc", "logging", "memory", "logging", "memory", "mmio", "pci",
"mmio", "pci", "process", "service", "process", "service", "time", "usb-abi",
"time", "usb-abi", "usb-ids", "usb-transfer-protocol", "usb-ids", "usb-transfer-protocol",
}, },
}); });
b.installArtifact(exe); b.installArtifact(exe);
+14 -26
View File
@@ -15,12 +15,10 @@
const std = @import("std"); const std = @import("std");
const device = @import("driver"); const device = @import("driver");
const channel = @import("channel");
const ipc = @import("ipc"); const ipc = @import("ipc");
const process = @import("process"); const process = @import("process");
const service = @import("service"); const service = @import("service");
const time = @import("time"); const time = @import("time");
const input = @import("input-client");
const device_manager = @import("driver"); const device_manager = @import("driver");
const memory = @import("memory"); const memory = @import("memory");
const logging = @import("logging"); const logging = @import("logging");
@@ -158,37 +156,27 @@ fn initialise(endpoint: ipc.Handle) bool {
// its device open, or its restarted instance could never claim it back. // its device open, or its restarted instance could never claim it back.
_ = process.subscribeExits(endpoint); _ = process.subscribeExits(endpoint);
// The transfer contract, bound by hand rather than through the harness's // **The handshake comes first, because it is where everything moves.** The
// `.service`, because **losing it is not fatal here**. One machine can carry // manager holds the controller and transfers it in `onHello`, so by the time
// several xHCI controllers and the driver model spawns one process per // this call returns the device is ours and nothing else could have taken it
// controller, so several processes provide the same contract for different // (docs/os-development/device-authority.md). And the serving endpoint rides
// hardware — and `/protocol` holds exactly one name, deliberately (addressing // up with the same call: one machine can carry several xHCI controllers, one
// lives inside the protocol, never in the path). Whoever binds first is the // process per controller, so the transfer contract is never a registry name
// one clients reach by name; a later instance still owns its controller, // — `/protocol` holds no instances, deliberately. Class drivers reach *this*
// enumerates its bus, and reports its children to the device manager, so it // controller because the manager answers their hellos with this endpoint,
// keeps running. **Known gap:** a class driver behind a second controller // routed by lineage (communication.md "Establishment: two planes"). The bind
// cannot reach it — the transfer protocol has no controller field for // race this replaces left every device behind a losing controller
// `target`, and the fix is either one process multiplexing every controller // unreachable — a real machine's mouse, "could not open device 50".
// or the spawner wiring the child's channel (P5), not a second name.
if (!channel.bindPatiently("usb-transfer", endpoint))
_ = logging.write("/system/drivers/usb-xhci-bus: /protocol/usb-transfer is another controller's; serving mine unnamed\n");
// **The handshake comes first, because it is where the device arrives.** This
// driver used to claim `controller_id` here — first-come-first-served, so the
// manager's matching was advisory and any process could have claimed it by
// passing the same integer. Now the manager holds the controller and transfers
// it in `onHello`, so by the time this call returns the device is ours and
// nothing else could have taken it (docs/os-development/device-authority.md).
// //
// `hello` is synchronous, so the transfer has completed before the reply lands — // `hello` is synchronous, so the transfer has completed before the reply lands —
// there is no window between being told yes and holding the thing. // there is no window between being told yes and holding the thing.
// //
// Keep the handle: the tick's hot-plug dispatch reports through it. // Keep the handle: the tick's hot-plug dispatch reports through it.
const handle = device_manager.hello(.bus, controller_id) orelse { const exchanged = device_manager.helloExchange(.bus, controller_id, endpoint, false) orelse {
std.log.warn("no hello with the device manager; controller {d} not delegated", .{controller_id}); std.log.warn("no hello with the device manager; controller {d} not delegated", .{controller_id});
return false; return false;
}; };
manager_handle = handle; manager_handle = exchanged.manager;
// Fetch our own descriptor back for the controller's resources. // Fetch our own descriptor back for the controller's resources.
const buffer = memory.allocator().alloc(device.DeviceDescriptor, 64) catch { const buffer = memory.allocator().alloc(device.DeviceDescriptor, 64) catch {
@@ -259,7 +247,7 @@ fn initialise(endpoint: ipc.Handle) bool {
return false; return false;
} }
scanPorts(handle); scanPorts(exchanged.manager);
// Arm the timer: in polling mode it drains the event ring; in MSI mode it is the // Arm the timer: in polling mode it drains the event ring; in MSI mode it is the
// slower port-reconcile/safety-net tick. Re-armed on each tick in onNotification. // slower port-reconcile/safety-net tick. Re-armed on each tick in onNotification.
+1 -1
View File
@@ -2778,7 +2778,7 @@ fn usbReportTest(boot_information: *const BootInformation) void {
}; };
process.setInitialRamdisk(image); process.setInitialRamdisk(image);
_ = spawnRegistry(rd); // the xhci driver binds /protocol/usb-transfer _ = spawnRegistry(rd); // drivers reach /protocol/device-manager through it
var manager: u32 = 0; var manager: u32 = 0;
var i: u32 = 0; var i: u32 = 0;
while (i < rd.count) : (i += 1) { while (i < rd.count) : (i += 1) {
@@ -35,11 +35,14 @@
//! enumerating an xHCI bus (the `usb-storage` scenario); //! enumerating an xHCI bus (the `usb-storage` scenario);
//! - `scanout` — the virtio-gpu driver, which needs an emulated virtio-gpu the //! - `scanout` — the virtio-gpu driver, which needs an emulated virtio-gpu the
//! default harness does not attach (the `virtio-gpu` scenario); //! default harness does not attach (the `virtio-gpu` scenario);
//! - `device-manager`, `power` and `usb-transfer` — the three P4b rebased. All //! - `device-manager` and `power` — both come with the device manager: it *is*
//! three come with the device manager: it *is* the first, it spawns the //! the first and spawns the discovery service that binds the second, so
//! discovery service that binds the second, and the xHCI driver it spawns //! booting a provider for either means booting the whole driver tree here.
//! binds the third. So booting a provider for any one of them means booting //! - `usb-transfer` — never a registry name at all: several bus processes
//! the whole driver tree here. //! provide it (one per controller) and consumers get their controller's
//! channel from the device manager's hello, routed by lineage
//! (communication.md "Establishment: two planes"). Its row below checks the
//! contract's shape only.
//! //!
//! That is the reason this scenario stays at two providers rather than five or //! That is the reason this scenario stays at two providers rather than five or
//! eight. It is not only the cost of booting half the system to send two //! eight. It is not only the cost of booting half the system to send two
@@ -120,12 +123,11 @@ const contracts = [_]Contract{
contractOf(vfs_protocol.Protocol, false), // the FAT server — needs a volume contractOf(vfs_protocol.Protocol, false), // the FAT server — needs a volume
contractOf(block_protocol.Protocol, false), // usb-storage — needs the xHCI chain contractOf(block_protocol.Protocol, false), // usb-storage — needs the xHCI chain
contractOf(scanout_protocol.Protocol, false), // virtio-gpu — needs the device contractOf(scanout_protocol.Protocol, false), // virtio-gpu — needs the device
// The three P4b rebased. Each needs the device manager (and, for the last // device-manager and power need the device manager (and what it starts),
// two, what the device manager starts), which is more than this scenario // which is more than this scenario boots — see the header.
// boots — see the header.
contractOf(device_manager_protocol.Protocol, false), contractOf(device_manager_protocol.Protocol, false),
contractOf(power_protocol.Protocol, false), // the discovery service contractOf(power_protocol.Protocol, false), // the discovery service
contractOf(usb_transfer_protocol.Protocol, false), // the xHCI bus driver contractOf(usb_transfer_protocol.Protocol, false), // never bound: per-controller, routed by hello
}; };
/// A verb number no protocol in the system defines, and none plausibly will: far /// A verb number no protocol in the system defines, and none plausibly will: far