Delegation moves out of onHello and into the spawn itself. The manager holds the hardware and names it in the call that creates the driver; the kernel checks the device is the caller's to give, then hands it over as part of making the child. The reason is the window. A transfer after spawning always leaves an interval in which the child is running and does not yet hold its device. It would close on QEMU every time and open occasionally on a machine with different core counts and timing — the exact failure shape this track exists to delete, and not one worth introducing while removing the others. Fused into the spawn there is no interval: the child does not exist until it holds the device. Ownership is checked BEFORE the child is created, so a refusal leaves nothing running rather than a driver without the hardware it was spawned for. The IOMMU confinement moves with the device, as it does on the transfer path. systemCall6 is added for the sixth argument; r9 was free, and abi gains a no_device sentinel matching the protocol's. No driver had to change to receive a device, which is what makes this better than requiring every driver to hello: ps2-bus keeps its legacy status, and discovery — which has no assignment at all, since it is what produces the device tree — is unaffected. The attacker fixture now tries the spawn as a back door: name someone else's device, and both the spawn and any child must be refused. Verifying that assertion exposed a bug in the fixture itself. The kernel case's pass marker was "device-authority: ok", which matches the FIRST per-assertion line, so its wait loop exited before any failure was printed — the case would have passed with failures in it, and had been able to since D2. The verdict lines now carry a distinct VERDICT prefix, and with the ownership check removed the case genuinely fails. A green test that cannot go red is worse than no test. Suite 118/118.
615 lines
28 KiB
Zig
615 lines
28 KiB
Zig
//! /system/services/device-manager — the ring-3 process that turns the device
|
|
//! tree into a running system: **the matcher and the supervisor**
|
|
//! (docs/device-manager.md). The kernel enumerates the hardware and enforces the
|
|
//! claim capability (mechanism); this decides which driver serves which device,
|
|
//! spawns it, and keeps it alive (policy). Keeping that split in user space is
|
|
//! the whole point of the microkernel: the manager is an ordinary, restartable
|
|
//! process with no special privilege.
|
|
//!
|
|
//! M18.1 (this increment): the manager is a harness service on the well-known
|
|
//! `.device_manager` endpoint. Every driver is spawned **supervised** — exit
|
|
//! notifications land in the same loop as protocol messages. Drivers with an
|
|
//! assignment must `hello` within a deadline or be stopped; a driver that dies
|
|
//! is restarted with backoff, and a crash loop (three fast deaths) marks it
|
|
//! failed instead of respawning forever. Exit reasons (M17.2) drive the
|
|
//! decision: a clean exit meant to stop; only faults and missed deadlines
|
|
//! restart. Tree reports (`child_added`) land in M18.2.
|
|
|
|
const std = @import("std");
|
|
const device = @import("driver");
|
|
const ipc = @import("ipc");
|
|
const process = @import("process");
|
|
const service = @import("service");
|
|
const time = @import("time");
|
|
const memory = @import("memory");
|
|
const logging = @import("logging");
|
|
const device_manager_protocol = @import("device-manager-protocol");
|
|
const envelope = @import("envelope");
|
|
const registry = @import("device-registry");
|
|
|
|
/// The generated device-manager dispatch, plus the subscriber machinery the
|
|
/// harness owns (P4c): the watcher table, the reserved `subscribe` verb, the
|
|
/// exit sweep, and the fan-out. One manager per system, so the handler context is
|
|
/// empty and the tables stay in this file's globals.
|
|
const Serve = service.Subscribers(device_manager_protocol.Protocol, void);
|
|
|
|
const Invocation = envelope.Invocation;
|
|
const Answer = envelope.Answer;
|
|
const fs = @import("file-system");
|
|
|
|
// --- the device registry ------------------------------------------------------
|
|
// Driver matching is data-driven and authoritative: /system/configuration/devices.csv (parsed by
|
|
// the device-registry module) names, per bus, which driver binds a reported
|
|
// device, the most-specific match winning. There is no compiled-in fallback — a
|
|
// device no row matches goes unbound and is logged. This retired the hand-kept
|
|
// pciDriverForIdentity / hidDriverFor / usbDriverForIdentity switch tables
|
|
// (docs/device-manager.md: "matching stays code until the third bus").
|
|
|
|
/// The CSV bytes, held for the life of the process because the parsed rules'
|
|
/// string fields (hid, driver) slice into this buffer.
|
|
var registry_source: [8192]u8 = undefined;
|
|
var registry_rules: [64]registry.Rule = undefined;
|
|
var registry_count: usize = 0;
|
|
|
|
/// Read and parse /system/configuration/devices.csv once at boot. The file lives in the initial
|
|
/// ramdisk, which the kernel serves directly — no filesystem service need be up
|
|
/// (fat is spawned after the manager), so this is a plain fs.open + read.
|
|
fn loadRegistry() void {
|
|
var file = fs.open("/system/configuration/devices.csv", .{}) orelse {
|
|
_ = logging.write("/system/services/device-manager: /system/configuration/devices.csv missing — nothing will match\n");
|
|
return;
|
|
};
|
|
defer file.close();
|
|
var used: usize = 0;
|
|
while (used < registry_source.len) {
|
|
const n = file.read(registry_source[used..]) orelse break;
|
|
if (n == 0) break;
|
|
used += n;
|
|
}
|
|
const result = registry.parse(registry_source[0..used], ®istry_rules);
|
|
registry_count = result.count;
|
|
if (result.malformed != 0) std.log.info("/system/configuration/devices.csv: {d} malformed line(s) skipped", .{result.malformed});
|
|
if (result.truncated) _ = logging.write("/system/services/device-manager: /system/configuration/devices.csv has more rules than the table holds\n");
|
|
std.log.info("/system/configuration/devices.csv: {d} rule(s) loaded", .{registry_count});
|
|
}
|
|
|
|
/// Build a registry Identity from a bus driver's report: the bus it named, the
|
|
/// class triple unpacked from `identity` (0xCCSSPP — the same packing for a PCI
|
|
/// class code and a USB class triple), the widened numeric ids, and the ACPI hid.
|
|
fn identityFromReport(report: device_manager_protocol.ChildAdded) registry.Identity {
|
|
const bus: registry.Bus = switch (report.bus) {
|
|
@intFromEnum(device_manager_protocol.BusKind.pci) => .pci,
|
|
@intFromEnum(device_manager_protocol.BusKind.usb) => .usb,
|
|
@intFromEnum(device_manager_protocol.BusKind.acpi) => .acpi,
|
|
else => .unknown,
|
|
};
|
|
const hid_len = std.mem.indexOfScalar(u8, &report.hid, 0) orelse report.hid.len;
|
|
return .{
|
|
.bus = bus,
|
|
.base = @truncate(report.identity >> 16),
|
|
.subclass = @truncate(report.identity >> 8),
|
|
.prog_if = @truncate(report.identity),
|
|
.vendor = report.vendor,
|
|
.device = report.device,
|
|
.subsystem = report.subsystem,
|
|
.hid = report.hid[0..hid_len],
|
|
};
|
|
}
|
|
|
|
/// 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 {
|
|
for (&drivers) |*driver| {
|
|
if (driver.used and driver.device_id == device_id) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// --- supervision -------------------------------------------------------------
|
|
|
|
/// How long a protocol driver has to hello after its spawn.
|
|
const hello_deadline_ms: u64 = 3000;
|
|
/// Deaths faster than this count toward the crash loop; slower ones reset it.
|
|
const fast_death_ns: u64 = 2_000_000_000;
|
|
/// Consecutive fast deaths before the manager gives up on a driver.
|
|
const crash_loop_cap: u32 = 3;
|
|
/// Restart backoff: base << (restarts - 1), so 300 ms, 600 ms, 1200 ms.
|
|
const backoff_base_ms: u64 = 300;
|
|
|
|
const DriverState = enum {
|
|
awaiting_hello, // spawned; the deadline is armed (protocol drivers only)
|
|
running,
|
|
restarting, // dead; respawn due at restart_due_ns
|
|
stopped, // exited cleanly — it meant to; not restarted
|
|
failed, // crash loop, or unspawnable; the manager gave up
|
|
};
|
|
|
|
const Driver = struct {
|
|
used: bool = false,
|
|
name_buffer: [64]u8 = undefined, // fits a full binary path (abi.maximum_process_name)
|
|
name_len: usize = 0,
|
|
// The assigned device id (becomes argv[1]), or device_manager_protocol.no_device.
|
|
device_id: u64 = device_manager_protocol.no_device,
|
|
// Whether this driver speaks the protocol (hello expected, deadline
|
|
// enforced). Legacy drivers (e.g. ps2-bus) are supervised and restarted
|
|
// but not yet required to hello.
|
|
speaks_protocol: bool = false,
|
|
process_id: u32 = 0,
|
|
state: DriverState = .running,
|
|
restarts: u32 = 0,
|
|
spawn_ns: u64 = 0,
|
|
hello_deadline_ns: u64 = 0,
|
|
restart_due_ns: u64 = 0,
|
|
|
|
fn name(driver: *const Driver) []const u8 {
|
|
return driver.name_buffer[0..driver.name_len];
|
|
}
|
|
};
|
|
|
|
const maximum_drivers = 16;
|
|
var drivers: [maximum_drivers]Driver = .{Driver{}} ** maximum_drivers;
|
|
var manager_endpoint: ipc.Handle = 0;
|
|
var test_restart_mode = false;
|
|
var test_usb_restart_mode = false;
|
|
var test_usb_killed = false;
|
|
var test_pci_restart_mode = false;
|
|
var test_scanout_restart_mode = false;
|
|
var test_scanout_killed = false;
|
|
var test_kill_pid: u32 = 0;
|
|
var test_kill_due_ns: u64 = 0;
|
|
|
|
/// The manager's mirror of what bus drivers report (docs/device-manager.md "the
|
|
/// tree"): the children, keyed by (parent, bus address), each remembering which
|
|
/// driver instance reported it — that is what death-pruning sweeps by.
|
|
const Child = struct {
|
|
used: bool = false,
|
|
parent: u64 = 0,
|
|
bus_address: u64 = 0,
|
|
identity: u64 = 0,
|
|
// The kernel device id (registered by the reporter), or device_manager_protocol.no_device.
|
|
device_id: u64 = 0,
|
|
reporter: u32 = 0, // the reporting driver instance's process id
|
|
};
|
|
|
|
const maximum_children = 64; // ACPI adds ~34 device nodes (M20.2), plus PCI + USB
|
|
var children: [maximum_children]Child = .{Child{}} ** maximum_children;
|
|
|
|
/// Record (or refresh) a reported child. Refreshing matters: a restarted bus
|
|
/// driver re-reports what it rediscovers, and the same (parent, port) must not
|
|
/// duplicate.
|
|
fn addChild(parent: u64, bus_address: u64, identity: u64, device_id: u64, reporter: u32) bool {
|
|
var free: ?*Child = null;
|
|
for (&children) |*child| {
|
|
if (child.used and child.parent == parent and child.bus_address == bus_address) {
|
|
child.identity = identity;
|
|
child.device_id = device_id;
|
|
child.reporter = reporter;
|
|
return true;
|
|
}
|
|
if (!child.used and free == null) free = child;
|
|
}
|
|
const slot = free orelse return false;
|
|
slot.* = .{ .used = true, .parent = parent, .bus_address = bus_address, .identity = identity, .device_id = device_id, .reporter = reporter };
|
|
return true;
|
|
}
|
|
|
|
/// Prune every child a dead driver instance reported: the children describe
|
|
/// protocol state (slots, rings) that died with the process — keeping the nodes
|
|
/// would be keeping a lie. The restarted instance rediscovers and re-reports.
|
|
/// Watchers hear the honest story: removed now, added again on rediscovery.
|
|
fn pruneChildrenOf(reporter: u32) void {
|
|
for (&children) |*child| {
|
|
if (child.used and child.reporter == reporter) {
|
|
std.log.info("child removed (device {d} port {d})", .{ child.parent, child.bus_address });
|
|
child.used = false;
|
|
Serve.publish(.child_removed, 0, .{ .parent = child.parent, .bus_address = child.bus_address });
|
|
}
|
|
}
|
|
}
|
|
|
|
/// How many children a driver instance has reported (the test-usb-restart
|
|
/// trigger counts these).
|
|
fn childCountOf(reporter: u32) u32 {
|
|
var n: u32 = 0;
|
|
for (&children) |*child| {
|
|
if (child.used and child.reporter == reporter) n += 1;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
/// The driver entry a live process id belongs to. Zero is not a process id here:
|
|
/// it is what `onDriverExit` writes back to retire an id it has already acted on,
|
|
/// so a second notification for the same death matches nothing.
|
|
fn driverByProcess(process_id: u32) ?*Driver {
|
|
if (process_id == 0) return null;
|
|
for (&drivers) |*driver| {
|
|
if (driver.used and driver.process_id == process_id) return driver;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Whether a singleton driver is already in the table (two ACPI nodes can both
|
|
/// map to ps2-bus; one instance serves both).
|
|
fn alreadySupervised(name: []const u8) bool {
|
|
for (&drivers) |*driver| {
|
|
if (driver.used and std.mem.eql(u8, driver.name(), name)) return true;
|
|
}
|
|
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{
|
|
"usb-xhci-bus",
|
|
"pci-bus",
|
|
};
|
|
|
|
/// Matched on the **last path component**, because a driver reaches this table under
|
|
/// two different spellings: the boot-snapshot match records the bare `pci-bus`, while a
|
|
/// devices.csv match records the full `/system/drivers/pci-bus`. Comparing whole
|
|
/// strings silently missed the bare form — pci-bus was left neither claiming nor
|
|
/// delegated, and died on `ECAM mmio_map failed`.
|
|
fn isDelegated(name: []const u8) bool {
|
|
const leaf = if (std.mem.lastIndexOfScalar(u8, name, '/')) |slash| name[slash + 1 ..] else name;
|
|
for (delegated_drivers) |candidate| {
|
|
if (std.mem.eql(u8, leaf, 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| {
|
|
if (driver.used) continue;
|
|
const n = @min(name.len, driver.name_buffer.len);
|
|
@memcpy(driver.name_buffer[0..n], name[0..n]);
|
|
driver.name_len = n;
|
|
driver.device_id = device_id;
|
|
driver.speaks_protocol = speaks_protocol;
|
|
driver.used = true;
|
|
spawnDriver(driver);
|
|
return;
|
|
}
|
|
std.log.info("driver table full; cannot supervise {s}", .{name});
|
|
}
|
|
|
|
/// (Re)spawn a driver instance: supervised on the manager's own endpoint, the
|
|
/// 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;
|
|
if (driver.device_id != device_manager_protocol.no_device) {
|
|
arguments[0] = std.fmt.bufPrint(&id_text, "{d}", .{driver.device_id}) catch return;
|
|
argument_count = 1;
|
|
}
|
|
// The device rides the spawn, so the driver holds it before its first instruction.
|
|
// A transfer *after* spawning would leave a window in which the child is running
|
|
// without its hardware — closed on one machine, open on another
|
|
// (docs/bounds-track-plan.md, "the grant rides system_spawn").
|
|
const give = if (isDelegated(driver.name())) driver.device_id else device_manager_protocol.no_device;
|
|
if (give != device_manager_protocol.no_device)
|
|
std.log.info("delegated device {d} to {s}", .{ give, driver.name() });
|
|
const child = process.spawnSupervisedWithDevice(driver.name(), arguments[0..argument_count], manager_endpoint, give) orelse {
|
|
std.log.info("failed to spawn {s}", .{driver.name()});
|
|
driver.state = .failed;
|
|
return;
|
|
};
|
|
driver.process_id = child;
|
|
driver.spawn_ns = time.clock();
|
|
if (driver.speaks_protocol) {
|
|
driver.state = .awaiting_hello;
|
|
driver.hello_deadline_ns = driver.spawn_ns + hello_deadline_ms * 1_000_000;
|
|
_ = time.timerOnce(manager_endpoint, hello_deadline_ms + 100);
|
|
} else {
|
|
driver.state = .running;
|
|
}
|
|
if (driver.device_id != device_manager_protocol.no_device) {
|
|
std.log.info("spawned {s} for device {d}", .{ driver.name(), driver.device_id });
|
|
} else {
|
|
std.log.info("spawned {s}", .{driver.name()});
|
|
}
|
|
}
|
|
|
|
/// A driver died. Prune what it reported first — then the exit reason (M17.2)
|
|
/// is the whole restart decision: a clean exit meant to stop; anything else
|
|
/// restarts with backoff until the crash-loop cap.
|
|
fn onDriverExit(driver: *Driver) void {
|
|
const dead = driver.process_id;
|
|
// One death, two notifications: the manager is this driver's supervisor (its
|
|
// spawn named this endpoint) *and*, since P4c put the watcher table in the
|
|
// harness, a subscriber to published exits. Both badges carry the same id, and
|
|
// the ring delivers them separately — so the id is retired here, before any
|
|
// decision is taken, and the second notification finds no driver to act on.
|
|
// Without this the backoff would count one death twice and the crash-loop cap
|
|
// would fire at half the deaths it names.
|
|
driver.process_id = 0;
|
|
pruneChildrenOf(dead);
|
|
const reason = process.exitReason(dead) orelse .fault;
|
|
if (reason == .exited) {
|
|
driver.state = .stopped;
|
|
std.log.info("{s} exited cleanly; not restarting", .{driver.name()});
|
|
return;
|
|
}
|
|
const now = time.clock();
|
|
const alive_ns = now - driver.spawn_ns;
|
|
driver.restarts = if (alive_ns < fast_death_ns) driver.restarts + 1 else 1;
|
|
if (driver.restarts >= crash_loop_cap) {
|
|
driver.state = .failed;
|
|
std.log.info("{s} is failing repeatedly (crash loop); giving up", .{driver.name()});
|
|
return;
|
|
}
|
|
const delay_ms = backoff_base_ms << @intCast(driver.restarts - 1);
|
|
driver.state = .restarting;
|
|
driver.restart_due_ns = now + delay_ms * 1_000_000;
|
|
std.log.info("restarting {s} in {d} ms (died: {s})", .{ driver.name(), delay_ms, @tagName(reason) });
|
|
_ = time.timerOnce(manager_endpoint, delay_ms + 50);
|
|
}
|
|
|
|
/// A timer landed: sweep every deadline. Overdue hellos are killed (the exit
|
|
/// notification then routes through the normal restart policy); due restarts
|
|
/// respawn. Timers carry no id on purpose — the table is the state, and one
|
|
/// sweep serves every armed deadline.
|
|
fn sweepDeadlines() void {
|
|
const now = time.clock();
|
|
if (test_kill_pid != 0 and now >= test_kill_due_ns) {
|
|
std.log.info("test mode: killing the reporter", .{});
|
|
_ = process.kill(test_kill_pid);
|
|
test_kill_pid = 0;
|
|
}
|
|
for (&drivers) |*driver| {
|
|
if (!driver.used) continue;
|
|
switch (driver.state) {
|
|
.awaiting_hello => if (now >= driver.hello_deadline_ns) {
|
|
std.log.info("{s} missed its hello deadline", .{driver.name()});
|
|
_ = process.kill(driver.process_id);
|
|
// The exit notification finishes the job via onDriverExit.
|
|
},
|
|
.restarting => if (now >= driver.restart_due_ns) spawnDriver(driver),
|
|
else => {},
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- the harness callbacks -----------------------------------------------------
|
|
|
|
fn initialise(endpoint: ipc.Handle) bool {
|
|
manager_endpoint = endpoint;
|
|
|
|
// Load the authoritative driver-match registry before any bus driver can
|
|
// report a device to match against it.
|
|
loadRegistry();
|
|
|
|
// Enumerate into a heap buffer (too big for the one-page user stack).
|
|
const buffer = memory.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
|
_ = logging.write("/system/services/device-manager: out of memory\n");
|
|
return false;
|
|
};
|
|
const total = device.enumerate(buffer);
|
|
const count = @min(total, buffer.len);
|
|
|
|
var matched: usize = 0;
|
|
for (buffer[0..count]) |descriptor| {
|
|
if (descriptor.class == @intFromEnum(device.DeviceClass.pci_host_bridge)) {
|
|
// The PCI bus driver: enumeration in ring 3 (M19), one instance
|
|
// per bridge, the bridge id as its assignment.
|
|
matched += 1;
|
|
addDriver("pci-bus", descriptor.id, true);
|
|
continue;
|
|
}
|
|
// Nothing else is matched from the boot snapshot today. The kernel-seeded
|
|
// HPET timer node is served by the kernel's own clock (docs/timers.md), not
|
|
// a user-space driver; PCI functions and PS/2 _HID devices arrive later as
|
|
// pci-bus / acpi-service reports and match in onChildAdded (docs/discovery.md).
|
|
// A fuller system's static class->driver manifest (docs/device-manager.md)
|
|
// would slot in here.
|
|
}
|
|
|
|
// The discovery service (docs/discovery.md): one per firmware, packed
|
|
// under the neutral name "discovery", spawned once at startup. It finds and
|
|
// claims the acpi-tables (or devicetree-blob) node itself. Not a per-device
|
|
// match — it is the discoverer, not a driver bound to one device.
|
|
addDriver("discovery", device_manager_protocol.no_device, false);
|
|
|
|
if (test_restart_mode) {
|
|
// The driver-restart scenario's fixture: claims device 0 (the tree
|
|
// root, otherwise unclaimed), hellos, then faults — driving backoff,
|
|
// re-claim-after-death, and the crash-loop cap deterministically.
|
|
addDriver("crash-test", 0, true);
|
|
}
|
|
|
|
if (matched == 0) {
|
|
_ = logging.write("/system/services/device-manager: no matchable devices\n");
|
|
} else {
|
|
_ = logging.write("/system/services/device-manager: ok\n");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
|
return Serve.dispatch({}, handlers, message, sender, arrived, reply);
|
|
}
|
|
|
|
/// `subscribe` and `unsubscribe` are absent on purpose: the harness answers both,
|
|
/// and its table is what `publish` fans out over.
|
|
const handlers = Serve.Handlers{
|
|
.hello = onHello,
|
|
.child_added = onChildAdded,
|
|
.child_removed = onChildRemoved,
|
|
.enumerate = onEnumerate,
|
|
};
|
|
|
|
/// The handshake. The device this driver was assigned is the packet's target.
|
|
fn onHello(_: void, invocation: Invocation(device_manager_protocol.Hello), _: Answer(void)) isize {
|
|
if (invocation.request.version != device_manager_protocol.version) {
|
|
std.log.info("refused hello (version {d}) from process {d}", .{ invocation.request.version, invocation.sender });
|
|
return -envelope.EPROTO;
|
|
}
|
|
const driver = driverByProcess(invocation.sender) orelse {
|
|
std.log.info("hello from unknown process {d}", .{invocation.sender});
|
|
return -envelope.EPERM;
|
|
};
|
|
driver.state = .running;
|
|
|
|
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.
|
|
if (test_scanout_restart_mode and !test_scanout_killed and std.mem.eql(u8, driver.name(), "/system/drivers/virtio-gpu")) {
|
|
test_scanout_killed = true;
|
|
test_kill_pid = invocation.sender;
|
|
test_kill_due_ns = time.clock() + 1_500_000_000;
|
|
_ = time.timerOnce(manager_endpoint, 1600);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/// A bus driver reported a discovered device: mirror it, publish it, match a
|
|
/// driver for it — and in the restart drills kill the reporter once, the
|
|
/// deterministic trigger for prune -> backoff -> respawn -> re-report.
|
|
fn onChildAdded(_: void, invocation: Invocation(device_manager_protocol.ChildAdded), _: Answer(void)) isize {
|
|
const report = invocation.request;
|
|
const sender = invocation.sender;
|
|
// The registered kernel device id is the packet's target, not a field: what
|
|
// the manager hands a matched driver as its argv assignment.
|
|
const device_id = invocation.target;
|
|
|
|
var status: isize = 0;
|
|
if (driverByProcess(sender)) |driver| {
|
|
if (!addChild(report.parent, report.bus_address, report.identity, device_id, sender)) status = -envelope.ENOSPC;
|
|
std.log.info("child added (device {d} port {d}, identity {d}) by {s}", .{ report.parent, report.bus_address, report.identity, driver.name() });
|
|
if (status == 0) Serve.publish(.child_added, device_id, report);
|
|
// Matching from reports (M19.3), now data-driven via the /system/configuration/devices.csv
|
|
// registry: a registered child gets the most-specific driver its identity
|
|
// matches, once — re-reports after a bus restart dedupe on the registered
|
|
// id, exactly like the registrations do.
|
|
if (status == 0 and device_id != device_manager_protocol.no_device) {
|
|
const id = identityFromReport(report);
|
|
if (registry.matchDriver(registry_rules[0..registry_count], id)) |match| {
|
|
if (match.ambiguous)
|
|
std.log.info("/system/configuration/devices.csv: multiple equally-specific rules match the device {s} reported; binding {s}", .{ driver.name(), match.driver });
|
|
if (id.bus == .acpi) {
|
|
// An hid-matched driver (ps2-bus) is a singleton that finds its
|
|
// own devices once spawned — spawn it once, no device assignment.
|
|
if (!alreadySupervised(match.driver)) addDriver(match.driver, device_manager_protocol.no_device, false);
|
|
} else {
|
|
// A per-device driver: one instance, the registered id as argv[1].
|
|
if (!driverForDevice(device_id)) addDriver(match.driver, device_id, true);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
status = -envelope.EPERM;
|
|
}
|
|
if (test_pci_restart_mode and !test_usb_killed) {
|
|
if (driverByProcess(sender)) |driver| {
|
|
if (std.mem.eql(u8, driver.name(), "pci-bus") and childCountOf(sender) >= 3) {
|
|
// The pci restart drill: kill the enumerator after it has
|
|
// reported; the respawn must re-register without duplicates
|
|
// (M19.0 idempotence, proven end to end by pci-scan).
|
|
test_usb_killed = true;
|
|
test_kill_pid = sender;
|
|
test_kill_due_ns = time.clock() + 1_000_000_000;
|
|
_ = time.timerOnce(manager_endpoint, 1100);
|
|
}
|
|
}
|
|
}
|
|
if (test_usb_restart_mode and !test_usb_killed and childCountOf(sender) >= 2) {
|
|
// Only the xHCI reporter is the drill's victim — pci-bus also reports
|
|
// now, and whichever finishes second must not trigger the kill.
|
|
if (driverByProcess(sender)) |driver| {
|
|
if (std.mem.eql(u8, driver.name(), "/system/drivers/usb-xhci-bus")) {
|
|
// Delayed, not immediate: the device-list scenario's subscriber
|
|
// needs a window to enumerate and subscribe before the events.
|
|
test_usb_killed = true;
|
|
test_kill_pid = sender;
|
|
test_kill_due_ns = time.clock() + 2_000_000_000;
|
|
_ = time.timerOnce(manager_endpoint, 2100);
|
|
}
|
|
}
|
|
}
|
|
return status;
|
|
}
|
|
|
|
/// A bus driver reported a device gone (hot-unplug). Addressed by the composite
|
|
/// (parent, bus address) the reporter knows, which is why that pair is the
|
|
/// packet's body rather than its target.
|
|
fn onChildRemoved(_: void, invocation: Invocation(device_manager_protocol.ChildRemoved), _: Answer(void)) isize {
|
|
const report = invocation.request;
|
|
var status: isize = -envelope.ENOENT;
|
|
for (&children) |*child| {
|
|
if (child.used and child.parent == report.parent and child.bus_address == report.bus_address and child.reporter == invocation.sender) {
|
|
std.log.info("child removed (device {d} port {d})", .{ child.parent, child.bus_address });
|
|
child.used = false;
|
|
status = 0;
|
|
}
|
|
}
|
|
return status;
|
|
}
|
|
|
|
/// 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 {
|
|
const entry_size = @sizeOf(device_manager_protocol.ChildEntry);
|
|
const tail = answer.tail();
|
|
var written: usize = 0;
|
|
for (&children) |*child| {
|
|
if (!child.used) 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 };
|
|
@memcpy(tail[written..][0..entry_size], std.mem.asBytes(&entry));
|
|
written += entry_size;
|
|
}
|
|
return @intCast(written);
|
|
}
|
|
|
|
fn onNotification(badge: u64) void {
|
|
if (badge & ipc.notify_exit_bit != 0) {
|
|
const dead: u32 = @intCast(badge & ~(ipc.notify_badge_bit | ipc.notify_exit_bit));
|
|
// The harness has already swept the watcher table for this death; what is
|
|
// left is the manager's own concern, its supervised drivers.
|
|
if (driverByProcess(dead)) |driver| onDriverExit(driver);
|
|
return;
|
|
}
|
|
if (badge & ipc.notify_timer_bit != 0) sweepDeadlines();
|
|
}
|
|
|
|
pub fn main(init: process.Init) void {
|
|
if (init.arguments.get(1)) |mode| {
|
|
test_restart_mode = std.mem.eql(u8, mode, "test-restart");
|
|
test_usb_restart_mode = std.mem.eql(u8, mode, "test-usb-restart");
|
|
test_pci_restart_mode = std.mem.eql(u8, mode, "test-pci-restart");
|
|
test_scanout_restart_mode = std.mem.eql(u8, mode, "test-scanout-restart");
|
|
}
|
|
service.run(device_manager_protocol.message_maximum, .{
|
|
.service = "device-manager",
|
|
.init = initialise,
|
|
.on_message = onMessage,
|
|
.on_notification = onNotification,
|
|
.subscribers = Serve.hooks,
|
|
});
|
|
}
|