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,
};
/// 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
/// namespace"): a provider hands `serving` — the endpoint its consumers will
/// 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", .{});
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;
const framed = device_manager_protocol.Protocol.encodeRequest(
.hello,
+14 -16
View File
@@ -5,12 +5,16 @@
//! service and `device.zig` over the raw device calls.
//!
//! 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
//! var device = usb.open(id) orelse return; // open + get its endpoints
//! const bus = device_manager.helloForChannel(.device, id) orelse return;
//! var device = usb.open(bus, id) orelse return; // open + get its endpoints
//! _ = device.controlOut(usb_abi.setProtocol(...));// class requests, descriptors
//! _ = device.subscribeInterrupt(address, length); // reports arrive asynchronously
//! 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`
//! event packets, decoded with `reportOf` (the class driver runs a bare `replyWait`
//! 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.
const std = @import("std");
const channel = @import("channel");
const envelope = @import("envelope");
const ipc = @import("ipc");
const time = @import("time");
const usb_transfer_protocol = @import("usb-transfer-protocol");
const Protocol = usb_transfer_protocol.Protocol;
@@ -157,18 +159,14 @@ pub fn reportOf(packet: []const u8) ?InterruptReport {
return Protocol.decodeEvent(.interrupt_report, packet);
}
/// Open `/protocol/usb-transfer` and, on that channel, open the device with the
/// assigned id, handing over a freshly created endpoint for asynchronous interrupt
/// reports. Retries while the bus is still coming up (a class driver races the bus
/// driver at boot). Two opens, deliberately: the first names the contract, the
/// second names an object within it.
pub fn open(device_id: u64) ?Device {
var attempts: usize = 0;
const bus = while (attempts < 100) : (attempts += 1) {
if (channel.openEndpoint("usb-transfer")) |handle| break handle;
time.sleepMillis(20);
} else return null;
/// On `bus` — the channel to this device's own controller, handed to the class
/// driver by the device manager's hello (establishment by lineage,
/// communication.md "Establishment: two planes") — open the device with the
/// assigned id, handing over a freshly created endpoint for asynchronous
/// interrupt reports. The channel is never found by name: one machine carries
/// several controllers, several processes provide this contract, and only the
/// manager knows which one reported this device.
pub fn open(bus: ipc.Handle, device_id: u64) ?Device {
const endpoint = ipc.createIpcEndpoint() orelse return null;
// 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
+4 -4
View File
@@ -63,8 +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").
/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/virtio-gpu, /system/services/device-manager, bind, scanout
@@ -116,10 +119,7 @@
# to the compositor.
/system/drivers/*, /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-mouse, /system/services/device-manager, open, usb-transfer
/system/drivers/usb-hid-mouse, /system/services/device-manager, open, input
/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;
// Hello the manager first (meet the spawn deadline), then open the device.
if (device_manager.hello(.device, device_id) == null) return;
var device = usb.open(device_id) orelse {
// 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;
var device = usb.open(bus, device_id) orelse {
std.log.info("could not open device {d}", .{device_id});
return;
};
+4 -2
View File
@@ -41,8 +41,10 @@ pub fn main(init: process.Init) void {
return;
};
if (device_manager.hello(.device, device_id) == null) return;
var device = usb.open(device_id) orelse {
// 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;
var device = usb.open(bus, device_id) orelse {
std.log.info("could not open device {d}", .{device_id});
return;
};
+4 -2
View File
@@ -83,8 +83,10 @@ var bring_up_failed = false;
fn initialise(endpoint: ipc.Handle) bool {
_ = endpoint;
if (device_manager.hello(.device, device_id) == null) return false;
device = usb.open(device_id) orelse {
// 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;
device = usb.open(bus, device_id) orelse {
std.log.info("could not open device {d}", .{device_id});
return false;
};
+4 -4
View File
@@ -10,10 +10,10 @@ pub fn build(b: *std.Build) void {
.name = "usb-xhci-bus",
.root_source_file = b.path("usb-xhci-bus.zig"),
.imports = &.{
"channel", "device-manager-protocol", "driver", "envelope",
"input-client", "ipc", "logging", "memory",
"mmio", "pci", "process", "service",
"time", "usb-abi", "usb-ids", "usb-transfer-protocol",
"device-manager-protocol", "driver", "envelope", "ipc",
"logging", "memory", "mmio", "pci",
"process", "service", "time", "usb-abi",
"usb-ids", "usb-transfer-protocol",
},
});
b.installArtifact(exe);
+14 -26
View File
@@ -15,12 +15,10 @@
const std = @import("std");
const device = @import("driver");
const channel = @import("channel");
const ipc = @import("ipc");
const process = @import("process");
const service = @import("service");
const time = @import("time");
const input = @import("input-client");
const device_manager = @import("driver");
const memory = @import("memory");
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.
_ = process.subscribeExits(endpoint);
// The transfer contract, bound by hand rather than through the harness's
// `.service`, because **losing it is not fatal here**. One machine can carry
// several xHCI controllers and the driver model spawns one process per
// controller, so several processes provide the same contract for different
// hardware — and `/protocol` holds exactly one name, deliberately (addressing
// lives inside the protocol, never in the path). Whoever binds first is the
// one clients reach by name; a later instance still owns its controller,
// enumerates its bus, and reports its children to the device manager, so it
// keeps running. **Known gap:** a class driver behind a second controller
// cannot reach it — the transfer protocol has no controller field for
// `target`, and the fix is either one process multiplexing every controller
// 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).
// **The handshake comes first, because it is where everything moves.** 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). And the serving endpoint rides
// up with the same call: one machine can carry several xHCI controllers, one
// process per controller, so the transfer contract is never a registry name
// — `/protocol` holds no instances, deliberately. Class drivers reach *this*
// controller because the manager answers their hellos with this endpoint,
// routed by lineage (communication.md "Establishment: two planes"). The bind
// race this replaces left every device behind a losing controller
// unreachable — a real machine's mouse, "could not open device 50".
//
// `hello` is synchronous, so the transfer has completed before the reply lands —
// there is no window between being told yes and holding the thing.
//
// 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});
return false;
};
manager_handle = handle;
manager_handle = exchanged.manager;
// Fetch our own descriptor back for the controller's resources.
const buffer = memory.allocator().alloc(device.DeviceDescriptor, 64) catch {
@@ -259,7 +247,7 @@ fn initialise(endpoint: ipc.Handle) bool {
return false;
}
scanPorts(handle);
scanPorts(exchanged.manager);
// 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.
+1 -1
View File
@@ -2778,7 +2778,7 @@ fn usbReportTest(boot_information: *const BootInformation) void {
};
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 i: u32 = 0;
while (i < rd.count) : (i += 1) {
@@ -35,11 +35,14 @@
//! enumerating an xHCI bus (the `usb-storage` scenario);
//! - `scanout` — the virtio-gpu driver, which needs an emulated virtio-gpu the
//! default harness does not attach (the `virtio-gpu` scenario);
//! - `device-manager`, `power` and `usb-transfer` — the three P4b rebased. All
//! three come with the device manager: it *is* the first, it spawns the
//! discovery service that binds the second, and the xHCI driver it spawns
//! binds the third. So booting a provider for any one of them means booting
//! the whole driver tree here.
//! - `device-manager` and `power` — both come with the device manager: it *is*
//! the first and spawns the discovery service that binds the second, so
//! booting a provider for either means booting the whole driver tree here.
//! - `usb-transfer` — never a registry name at all: several bus processes
//! 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
//! 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(block_protocol.Protocol, false), // usb-storage — needs the xHCI chain
contractOf(scanout_protocol.Protocol, false), // virtio-gpu — needs the device
// The three P4b rebased. Each needs the device manager (and, for the last
// two, what the device manager starts), which is more than this scenario
// boots — see the header.
// device-manager and power need the device manager (and what it starts),
// which is more than this scenario boots — see the header.
contractOf(device_manager_protocol.Protocol, false),
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