usb: the xHCI controller arrives by delegation, not by claiming

The first driver to stop claiming its own hardware. The device manager holds
the controller and transfers it in the hello reply, so its matching becomes
authoritative instead of advisory — until now the driver claimed the id it
found in argv[1], and any process could have claimed the same integer first.

The manager claims before it spawns, so there is no window in which anything
else could take the device, and transfers in onHello using invocation.sender
— the kernel-stamped task id, which cannot be forged by the caller. hello is
synchronous, so the transfer has completed before the reply lands: no gap
between being told yes and holding the thing.

usb-xhci-bus's hello moves from after controller bring-up to before anything
that needs the device, which is the bring-up reorder the design predicted.
It is the first member of an explicit delegated set, so every unconverted
driver keeps claiming exactly as before and the suite stays green; the set
and device_claim both go at D6. D3 and D4 could not be separated and the
plan records why: the moment the manager claims, any driver still calling
device_claim is refused, and D3 applied to nothing changes no behaviour and
cannot be tested.

This step introduced a regression and the incremental conversion is what
caught it. confineDevice runs inside systemDeviceClaim, so a device arriving
by transfer was never confined for its new owner. Three IOMMU+USB cases
failed on the driver's DMA rings going unbound, and two worse consequences
were latent: a manager death would have torn down a domain a live driver was
using, and a driver death would have leaked one. iommu.reassign now moves
the confinement with the device, keeping the domain and its attachment
intact so it never translates through nothing. Converting all five drivers
at once would have produced the same three failures with five suspects.

A log line of mine claimed "holding controller device N" before anything
verified it — it printed even in the failure case, where the driver held
nothing. Reworded to state only what is known there: where the registers
are.

usb-hid asserts the delegation with the device id backreferenced, so the id
delegated and the id the driver ends up with must match. Emptying the
delegated set fails it with "hello acknowledged" then "mmio_map failed".

usb-hub failed once in a full run and has passed six times since (four
isolated, two full) — recorded in the plan as a suspected instance of the
known intermittent AP fault, not dismissed, since this step did shift boot
timing.

Suite 118/118.
This commit is contained in:
Daniel Samson
2026-08-08 17:55:59 +01:00
parent 33376f24ab
commit a2d6056772
6 changed files with 134 additions and 13 deletions
@@ -237,6 +237,25 @@ fn alreadySupervised(name: []const u8) bool {
return false;
}
/// Drivers that receive their device from the manager rather than claiming it
/// themselves. Scaffolding for the conversion, not a permanent concept: it exists so
/// each driver can move across one at a time with the suite green throughout, and it
/// disappears at D6 when `device_claim` stops being a way to acquire a device at all
/// (docs/bounds-track-plan.md, Run 2).
///
/// `usb-xhci-bus` is first because it was the first driver to conform to `hello`
/// (device-manager.md, M18.1), so it is the one whose handshake is best proven.
const delegated_drivers = [_][]const u8{
"/system/drivers/usb-xhci-bus",
};
fn isDelegated(name: []const u8) bool {
for (delegated_drivers) |candidate| {
if (std.mem.eql(u8, name, candidate)) return true;
}
return false;
}
/// Record a driver in the table and spawn its first instance.
fn addDriver(name: []const u8, device_id: u64, speaks_protocol: bool) void {
for (&drivers) |*driver| {
@@ -257,6 +276,22 @@ fn addDriver(name: []const u8, device_id: u64, speaks_protocol: bool) void {
/// device id as argv[1] when it has one, the hello deadline armed when it
/// speaks the protocol.
fn spawnDriver(driver: *Driver) void {
// Take the device before the driver exists, so there is no window in which anyone
// else could claim it — which is the whole of what makes the handover authoritative
// rather than advisory. Re-claiming across a restart is expected to say
// AlreadyClaimed once the manager already holds it, and that is fine: it means the
// device never left our hands while the driver was dead.
if (isDelegated(driver.name()) and driver.device_id != device_manager_protocol.no_device) {
device.claim(driver.device_id) catch |e| switch (e) {
error.AlreadyClaimed => {}, // ours already, from a previous spawn of this driver
else => {
std.log.warn("cannot hold device {d} for {s}: {s}", .{ driver.device_id, driver.name(), @errorName(e) });
driver.state = .failed;
return;
},
};
}
var id_text: [20]u8 = undefined;
var arguments: [1][]const u8 = undefined;
var argument_count: usize = 0;
@@ -424,6 +459,23 @@ fn onHello(_: void, invocation: Invocation(device_manager_protocol.Hello), _: An
return -envelope.EPERM;
};
driver.state = .running;
// **Delegation.** The manager holds this driver's device and hands it over here —
// what replaces first-come-first-served `device_claim` with policy
// (docs/os-development/device-authority.md). `invocation.sender` is the driver's
// task id stamped by the kernel, so the manager cannot be lied to about who is
// asking, and the transfer is a move: the manager stops holding it.
//
// Gated on the delegated set so an unconverted driver still claims for itself and
// its path is untouched; the set and `device_claim` both go at D6.
if (isDelegated(driver.name()) and driver.device_id != device_manager_protocol.no_device) {
device.transfer(driver.device_id, invocation.sender) catch |e| {
std.log.warn("could not delegate device {d} to {s}: {s}", .{ driver.device_id, driver.name(), @errorName(e) });
return -envelope.EPERM;
};
std.log.info("delegated device {d} to {s}", .{ driver.device_id, driver.name() });
}
std.log.info("hello from {s} (device {d})", .{ driver.name(), invocation.target });
// Resilience drill (V6): once, kill the virtio-gpu driver a moment after it hellos, so
// the normal restart policy respawns it — the compositor must survive and re-attach.