Author SHA1 Message Date
Daniel Samson af2c766f42 The flip: PCI enumeration leaves the kernel (M19.3)
enumeratePci, addBars, pciConfigurationPtr, and the PciHeader struct are
deleted; the kernel seeds only the host bridge, and the ring-3 pci-bus
driver's reports are the sole source of PCI function nodes. The manager
matches PCI drivers from reported identity, deduped by registered device
id so a bus restart never double-spawns.

The flip did its job by exposing a latent SMP race: ring-3
device_register made the broker table concurrent for the first time, and
mmio_map read it lock-free — under load a torn resource length mapped
hpet's window wrong (its user fault) and underflowed r.len-1 into a
kernel integer-overflow panic. Fixed: the broker read in mmio_map (and
claim) runs under the big kernel lock, the arithmetic rejects
zero-length and wrapping windows cleanly, and pci-bus no longer registers
unimplemented size-0 BARs. driver-restart hammered 6x, suite 55/55.
2026-07-13 02:54:50 +01:00
Daniel Samson d26262bf56 pci-bus registers and reports what it scans (M19.2)
Each function is registered under the bridge with the config-space slice
and BARs sized by the same all-ones probe the kernel uses — byte-for-byte
equal descriptors, so the idempotent register returns the kernel's
existing node ids during coexistence instead of duplicating the tree.
The bridge gained the 16-bit io_port aperture that functions' I/O BARs
need to pass containment. Reports carry the registered device_id, and
the pci-scan scenario drills a forced restart: kill the enumerator after
its reports, watch the respawn re-scan, and assert the broker's PCI node
count never grew. The usb-restart test trigger is pinned to the xHCI
reporter (pci-bus racing it to two reports used to steal the kill).
Harness hardening: failing cases preserve their serial logs; the heavy
scenarios run at 150s.
2026-07-13 02:22:19 +01:00
Daniel Samson 10b89c06ff The PCI scan from ring 3: pci-bus walks the ECAM it mapped (M19.1)
The manager matches the pci_host_bridge node and spawns pci-bus with the
bridge id as its assignment — hello, supervision, restart, all the M18
contract for free. The driver claims the bridge, maps the ECAM window
(resource 0) through the ordinary mmio grant, and repeats the kernel's
brute-force bus/device/function walk from user space. The pci-scan
scenario builds its expected marker from the kernel's own function count,
so the two enumerations must agree exactly — the equivalence that
licenses retiring the kernel walk in M19.3.
2026-07-13 02:05:32 +01:00
Daniel Samson a2a05d0b3d Discovery-migration prerequisites (M19.0)
The host bridge now carries MMIO apertures derived from the boot memory
map's gaps below 4 GiB (largest three, sort-merged; a single after-the-
last-region hole dies on OVMF's flash at the top) plus one aperture above
the described space — so a user-space device_register of PCI functions
with BAR resources can pass containment. The discovery test asserts
every PCI memory resource lies inside a bridge window and names any
escapee. device_register is idempotent on exact (parent, class, identity,
resources) match — a restarted registering bus cannot duplicate its
children; proven directly against the broker in the bus test.
ChildAdded gains device_id so a report can carry the registered kernel
id a matched driver needs as its assignment.
2026-07-13 01:59:32 +01:00
Daniel Samson 75d62660b0 Bring the docs up to the M17-M18 reality; pre-settle M19-M20 ambiguities
resilience.md: steps 1-4 of the ladder are built — supervision, exit
reasons, restart with backoff, crash-loop caps, all proven by scenario;
what remains is scope, not mechanism. README statuses follow. drivers.md
gains the driver-contract section (harness, hello, crash-freely). The
M19-M20 plan pre-settles three things the loop would otherwise have had
to decide alone: the memory-map pass-through for apertures, the manager
spawning 'discovery' from M20.1, and hid[8] riding ChildAdded for ACPI
string identity until the FDT widening.
2026-07-13 01:49:52 +01:00
16 changed files with 735 additions and 198 deletions
+3
View File
@@ -339,6 +339,7 @@ pub fn build(b: *std.Build) void {
const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig"); const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig");
const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig"); const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig");
const usb_xhci_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-xhci-bus", "system/drivers/usb-xhci-bus/usb-xhci-bus.zig"); const usb_xhci_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-xhci-bus", "system/drivers/usb-xhci-bus/usb-xhci-bus.zig");
const pci_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "pci-bus", "system/drivers/pci-bus/pci-bus.zig");
// A test fixture, not a real driver: hellos to the device manager, then faults — // A test fixture, not a real driver: hellos to the device manager, then faults —
// what the driver-restart scenario drives the crash-loop cap with. // what the driver-restart scenario drives the crash-loop cap with.
const crash_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "crash-test", "system/services/crash-test/crash-test.zig"); const crash_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "crash-test", "system/services/crash-test/crash-test.zig");
@@ -388,6 +389,8 @@ pub fn build(b: *std.Build) void {
mk_run.addFileArg(ps2_mouse_exe.getEmittedBin()); mk_run.addFileArg(ps2_mouse_exe.getEmittedBin());
mk_run.addArg("usb-xhci-bus"); mk_run.addArg("usb-xhci-bus");
mk_run.addFileArg(usb_xhci_bus_exe.getEmittedBin()); mk_run.addFileArg(usb_xhci_bus_exe.getEmittedBin());
mk_run.addArg("pci-bus");
mk_run.addFileArg(pci_bus_exe.getEmittedBin());
mk_run.addArg("crash-test"); mk_run.addArg("crash-test");
mk_run.addFileArg(crash_test_exe.getEmittedBin()); mk_run.addFileArg(crash_test_exe.getEmittedBin());
mk_run.addArg("device-list"); mk_run.addArg("device-list");
+8 -6
View File
@@ -45,20 +45,22 @@ rather than restate it. Roughly in the order things happen at runtime:
until its hardware interrupts it**. The claim is the capability; `irq_ack` is the until its hardware interrupts it**. The claim is the capability; `irq_ack` is the
unmask. unmask.
14. **[driver-model.md](driver-model.md) — buses, classes and host controllers.** How 14. **[driver-model.md](driver-model.md) — buses, classes and host controllers.** How
real driver stacks factor into three shapes, how families share code, and the real driver stacks factor into three shapes and how families share code. The
proposed ABI for the three primitives still missing (capability passing, DMA + three primitives it proposed are long since built (M13 capability passing,
memory barriers, MSI). M14 DMA + barriers, M15 MSI), and the driver *contract* on top of them —
hello, supervision, restart — is built too (device-manager.md, M18).
15. **[process-management.md](process-management.md) — process management.** The 15. **[process-management.md](process-management.md) — process management.** The
microkernel's `ps`/`kill`/SIGCHLD: enumerate as a table snapshot, the microkernel's `ps`/`kill`/SIGCHLD: enumerate as a table snapshot, the
supervision link as the kill authority, and child-exit notifications over the supervision link as the kill authority, and child-exit notifications over the
same endpoints IRQs arrive on. same endpoints IRQs arrive on.
16. **[process-lifecycle.md](process-lifecycle.md) — the process lifecycle.** Design: 16. **[process-lifecycle.md](process-lifecycle.md) — the process lifecycle.** Built
signals over IPC as the one lifecycle vocabulary every process speaks — the (M17): signals over IPC as the one lifecycle vocabulary every process speaks — the
POSIX.1-1990 words with message delivery instead of stack hijack, the stable POSIX.1-1990 words with message delivery instead of stack hijack, the stable
`runtime.process` interface, exit reasons, published exit events any stateful `runtime.process` interface, exit reasons, published exit events any stateful
service can subscribe to (the VFS releasing dead clients' handles), and the two service can subscribe to (the VFS releasing dead clients' handles), and the two
iron rules (cleanup is the kernel's job; kill is not a signal). iron rules (cleanup is the kernel's job; kill is not a signal).
17. **[device-manager.md](device-manager.md) — the device manager.** Design: the 17. **[device-manager.md](device-manager.md) — the device manager.** Built (M18,
through the app surface): the
tree, the matcher, and the supervisor. Tree structure lives in the manager, tree, the matcher, and the supervisor. Tree structure lives in the manager,
authority stays in the kernel; bus drivers report what they see; drivers are authority stays in the kernel; bus drivers report what they see; drivers are
restarted through the lifecycle vocabulary — the plan that turns restarted through the lifecycle vocabulary — the plan that turns
+10
View File
@@ -167,3 +167,13 @@ free; discovery on x86 is partly about *finding* what ARM just tells you.
- [ipc.md](ipc.md) — the channels that interrupts-as-messages and the device manager - [ipc.md](ipc.md) — the channels that interrupts-as-messages and the device manager
will ride on. will ride on.
- [vision.md](vision.md) — why drivers belong in isolated user space at all. - [vision.md](vision.md) — why drivers belong in isolated user space at all.
## Update (M19.3, 2026-07-13): PCI enumeration left the kernel
The kernel now seeds only the `pci_host_bridge` node (ECAM window, MMIO
apertures derived from the memory map's holes, bus range, and the 16-bit I/O
window). The per-function walk moved to the ring-3 `pci-bus` driver
([device-manager.md](device-manager.md)): it claims the bridge, repeats the
ECAM scan through its mmio grant, and `device_register`s what it finds, which
the device manager mirrors and matches. The ACPI namespace walk follows in M20;
the static tables (MADT, HPET, MCFG, FADT + `\\_S5`) stay kernel-side.
+20
View File
@@ -362,3 +362,23 @@ the first DMA driver to protect and test against) and these smaller items:
- **Interrupt priority / threaded IRQ latency.** `notifyFromIsr` enqueues the woken - **Interrupt priority / threaded IRQ latency.** `notifyFromIsr` enqueues the woken
driver but doesn't preempt (`wakeLocked` deliberately leaves that to the caller), so driver but doesn't preempt (`wakeLocked` deliberately leaves that to the caller), so
a woken driver waits for the next scheduling point. a woken driver waits for the next scheduling point.
## The driver contract (M17–M18)
Claiming and mapping is half of being a danos driver; the other half is the
**lifecycle and protocol contract**, and the runtime makes it nearly free:
- Build on `runtime.service.run` — one replyWait loop folding protocol
requests, signals, and notifications into callbacks. The harness answers the
universal zero-length ping and turns `terminate` into a clean exit for you
([process-lifecycle.md](process-lifecycle.md)).
- A driver spawned with an assignment (its device id as argv[1]) sends the
versioned `hello` to the device manager inside the deadline, and a **bus**
driver reports what it discovers with `child_added`
([device-manager.md](device-manager.md); usb-xhci-bus is the reference
implementation).
- Crash freely — that is the design. The kernel releases your claims, IRQ
bindings, and MSI vectors at death; the manager reads your exit reason,
prunes what you reported, restarts you with backoff, and your fresh instance
re-claims and re-reports. Never depend on your own cleanup running
(iron rule 1).
+4
View File
@@ -1,5 +1,9 @@
# M17–M18 execution plan: process lifecycle + device manager # M17–M18 execution plan: process lifecycle + device manager
**Archived — completed 2026-07-13** (every item checked; suite ended 54/54).
Kept as the record of how M17–M18 landed; the successor is
[m19-m20-plan.md](m19-m20-plan.md).
The operational plan for building [process-lifecycle.md](process-lifecycle.md) The operational plan for building [process-lifecycle.md](process-lifecycle.md)
(M17) and [device-manager.md](device-manager.md) increments 5–7 (M18). Design is (M17) and [device-manager.md](device-manager.md) increments 5–7 (M18). Design is
settled in those documents; this file is the build order — one phase at a time, settled in those documents; this file is the build order — one phase at a time,
+36 -19
View File
@@ -79,24 +79,32 @@ branch is green; keep branches; push everything.
## Status ## Status
- [ ] **M19.0** — prerequisites on `feat/pci-bus`: bridge MMIO apertures from - [x] **M19.0** — prerequisites (bridge apertures from the memory map's
the memory-map holes; `device_register` idempotence (+ kernel unit *gaps* — the single-hole rule died on OVMF's flash at the top of 4 GiB,
checks); `ChildAdded.device_id`; archive note on m17-m18-plan.md. caught by the new every-BAR-contained assert in `discovery`; idempotent
- [ ] **M19.1** — pci-bus driver, scan only: claim the host bridge, map the `device_register` proven in `bus`; `ChildAdded.device_id`;
ECAM window, walk bus/device/function headers, log what it finds. m17-m18-plan.md archived; suite 54/54).
Scenario `pci-scan`: the kernel test compares the driver's reported count - [x] **M19.1** — pci-bus driver, scan only (claims the bridge, maps ECAM
against the broker table's `pci_device` count — equivalence, per class. through its grant, brute-force walk with the multifunction rule; the
- [ ] **M19.2** — register + report: each function registered under the bridge manager matches pci_host_bridge → pci-bus per device with the full
(config-space slice + BARs, `pci_class` in the descriptor), reported with protocol contract; `pci-scan` builds its expected marker from the
`child_added { device_id, identity = class triple }`. Manager mirrors; kernel's own count — equivalence on the first run; suite 55/55).
spawn-from-reports stays **off**. Scenario extends `pci-scan`: - [x] **M19.2** — register + report (BAR probe mirrored byte-for-byte from the
registered ids resolve, no duplicates after a forced driver restart kernel's addBars so dedupe returns the kernel's node ids during
(idempotence proven end to end). coexistence; the bridge gained the io_port aperture I/O BARs need;
- [ ] **M19.3** — the flip: kernel `enumeratePci` call removed (bridge node reports carry the registered device_id; pci-scan drills a forced restart
stays); manager matches PCI drivers from reports. One commit. The and asserts the PCI node count never grows — plus harness hardening: a
existing xHCI scenarios (`driver-restart`, `usb-report`, `device-list`) failing case now preserves its serial as <case>-failed-serial.log, and
are the assertion — xhci must come up spawned off a pci-bus report, and the heavy scenarios run at 150s; suite 55/55).
the suite must not be able to tell the difference. discovery.md updated. - [x] **M19.3** — the flip: kernel `enumeratePci`/`addBars`/`PciHeader` all
deleted (bridge node stays); manager matches PCI drivers from reported
identity, deduped by registered id. Surfaced and fixed a real SMP race the
flip created — ring-3 device_register made the broker table concurrent, so
mmio_map's lock-free read intermittently tore hpet's resource length
(user fault) and overflowed `r.len-1` into a kernel panic; now the broker
read is under the big lock and the arithmetic is guarded, and pci-bus
skips size-0 BARs. discovery.md updated; suite 55/55 (driver-restart
hammered 6×).
- [ ] **merge** `feat/pci-bus` → main, push. - [ ] **merge** `feat/pci-bus` → main, push.
- [ ] **M20.1** — acpi service, parse only (fills the existing placeholder at - [ ] **M20.1** — acpi service, parse only (fills the existing placeholder at
system/services/acpi/acpi.zig): kernel publishes `acpi-tables` system/services/acpi/acpi.zig): kernel publishes `acpi-tables`
@@ -122,7 +130,9 @@ branch is green; keep branches; push everything.
## Phase notes ## Phase notes
**M19.0 apertures:** the boot memory map already crosses the handoff **M19.0 apertures:** the boot memory map already crosses the handoff
([boot-handoff]); the holes computation belongs where the bridge node is built ([boot-handoff]), but discovery never sees it today — expect a small
pass-through (kernel init hands the map to the platform layer) before the
holes computation, which belongs where the bridge node is built
(`parseMcfg`). Sanity-check on QEMU q35: the xHCI BAR (`0xc0000000`-region (`parseMcfg`). Sanity-check on QEMU q35: the xHCI BAR (`0xc0000000`-region
values seen in the M18 logs) must land inside a derived aperture, asserted in values seen in the M18 logs) must land inside a derived aperture, asserted in
the kernel unit test. the kernel unit test.
@@ -142,6 +152,13 @@ scenario can assert it.
kernel still enumerates (timers, ACPI nodes until M20.3). The PCI arm of kernel still enumerates (timers, ACPI nodes until M20.3). The PCI arm of
`pciDriverFor` switches source; `driverFor` doesn't move until M20.3. `pciDriverFor` switches source; `driverFor` doesn't move until M20.3.
**M20.1 spawn and identity (pre-settled 2026-07-13):** the manager spawns
`discovery` by its neutral ramdisk name at startup, as an ordinary protocol
driver (hello, supervision) — from M20.1 on, on every boot. For reporting ACPI
devices, `ChildAdded` gains `hid: [8]u8` (EISA ids fit; zero = none):
firmware *string* identity travels beside the numeric `identity` field until
the FDT-driven widening replaces both (decision 7).
**M20.1 Hal in ring 3:** `mapMmio` → `device.mmioMap` over the claimed **M20.1 Hal in ring 3:** `mapMmio` → `device.mmioMap` over the claimed
acpi-tables node (plus a table-offset map for blobs); `pioRead`/`pioWrite` → acpi-tables node (plus a table-offset map for blobs); `pioRead`/`pioWrite` →
`device.ioRead`/`ioWrite` against its io_port resource. The interpreter cannot `device.ioRead`/`ioWrite` against its io_port resource. The interpreter cannot
+12 -5
View File
@@ -1,10 +1,17 @@
# Resilience: fault isolation and live restart # Resilience: fault isolation and live restart
Steps 1–2 of the ordering below are **built**: user-mode isolation, and fault → Steps 1–4 of the ordering below are **built** (M17–M18, 2026-07-13): user-mode
kill the process → keep the core (`onException` in `system/kernel/kernel.zig`; the isolation; fault → kill the process → keep the core (`onException`; the
`fault-recovery` test proves a crashing ring-3 process dies alone while the system `fault-recovery` test); the supervisor notification **with exit reasons**
keeps running). The supervisor notification and restart policy (steps 3+) are ([process-lifecycle.md](process-lifecycle.md) — clean exit, fault class, or
still design. This is the property danos is really chasing: killed, recorded before the notice posts); and the **restart policy itself**
([device-manager.md](device-manager.md)): the device manager supervises every
driver, restarts crashes with backoff, caps crash loops, and re-claims work
because the kernel releases a dead process's claims. The `driver-restart` and
`usb-report` scenarios prove kill → release → respawn → re-claim → re-report
end to end. What remains of this document's ladder is scope, not mechanism:
more of the system moved into restartable processes (the discovery migration,
[m19-m20-plan.md](m19-m20-plan.md), is the next rung). This is the property danos is really chasing:
**if a part of the OS breaks, isolate it, and re-initialise it — without rebooting.** **if a part of the OS breaks, isolate it, and re-initialise it — without rebooting.**
A crashed driver gets restarted; a wedged service gets killed and brought back. It's A crashed driver gets restarted; a wedged service gets killed and brought back. It's
the reason the [microkernel](vision.md) shape was chosen, and it's a *separate* goal the reason the [microkernel](vision.md) shape was chosen, and it's a *separate* goal
+74 -132
View File
@@ -360,32 +360,14 @@ const Hpet = extern struct {
page_protection: u8, page_protection: u8,
}; };
// --- PCI configuration-space header (first 64 bytes, common fields) ---------
const PciHeader = extern struct {
vendor_id: u16 align(1),
device_id: u16 align(1),
command: u16 align(1),
status: u16 align(1),
revision_id: u8,
prog_if: u8,
subclass: u8,
class_code: u8,
cache_line_size: u8,
latency_timer: u8,
/// bit 7 set => multi-function device.
header_type: u8,
bist: u8,
// 0x10 onward (BARs, etc.) depends on header_type; read separately.
};
// --- Entry point ------------------------------------------------------------ // --- Entry point ------------------------------------------------------------
/// Discover hardware from the ACPI tables rooted at `rsdp_physical` and populate /// Discover hardware from the ACPI tables rooted at `rsdp_physical` and populate
/// `device_tree`. `hal` provides MMIO mapping (for PCIe ECAM) and port I/O. Also parses the /// `device_tree`. `hal` provides MMIO mapping (for PCIe ECAM) and port I/O. Also parses the
/// FADT and the AML sleep-state (`_Sx`) packages into `power_information` for the power service. /// FADT and the AML sleep-state (`_Sx`) packages into `power_information` for the power service.
pub fn discover(rsdp_physical: u64, device_tree: *DeviceTree, hal: Hal) !void { pub fn discover(rsdp_physical: u64, memory_regions: []const boot_handoff.MemoryRegion, device_tree: *DeviceTree, hal: Hal) !void {
if (rsdp_physical == 0) return error.NoRsdp; if (rsdp_physical == 0) return error.NoRsdp;
boot_memory_regions = memory_regions;
// Start clean so a re-run doesn't accumulate stale state. // Start clean so a re-run doesn't accumulate stale state.
power_information = .{}; power_information = .{};
@@ -451,7 +433,7 @@ fn handleTable(device_tree: *DeviceTree, hal: Hal, sdt_physical: u64) !void {
if (std.mem.eql(u8, &sig, &APIC)) { if (std.mem.eql(u8, &sig, &APIC)) {
try parseMadt(device_tree, header); try parseMadt(device_tree, header);
} else if (std.mem.eql(u8, &sig, &MCFG)) { } else if (std.mem.eql(u8, &sig, &MCFG)) {
try parseMcfg(device_tree, hal, header); try parseMcfg(device_tree, header);
} else if (std.mem.eql(u8, &sig, &HPET)) { } else if (std.mem.eql(u8, &sig, &HPET)) {
try parseHpet(device_tree, hal, header); try parseHpet(device_tree, hal, header);
} else if (std.mem.eql(u8, &sig, &FACP)) { } else if (std.mem.eql(u8, &sig, &FACP)) {
@@ -536,7 +518,7 @@ fn parseMadt(device_tree: *DeviceTree, header: *const SystemDescriptorTableHeade
} }
/// MCFG -> a pci_host_bridge per ECAM segment, then a PCI enumeration underneath. /// MCFG -> a pci_host_bridge per ECAM segment, then a PCI enumeration underneath.
fn parseMcfg(device_tree: *DeviceTree, hal: Hal, header: *const SystemDescriptorTableHeader) !void { fn parseMcfg(device_tree: *DeviceTree, header: *const SystemDescriptorTableHeader) !void {
const total: usize = header.length; const total: usize = header.length;
const base: [*]const u8 = @ptrCast(header); const base: [*]const u8 = @ptrCast(header);
@@ -551,109 +533,85 @@ fn parseMcfg(device_tree: *DeviceTree, hal: Hal, header: *const SystemDescriptor
// ECAM window: 1 MiB of configuration space per bus. // ECAM window: 1 MiB of configuration space per bus.
_ = bridge.addResource(.memory, alloc.base_address, bus_count << 20); _ = bridge.addResource(.memory, alloc.base_address, bus_count << 20);
_ = bridge.addResource(.bus_range, alloc.start_bus, bus_count); _ = bridge.addResource(.bus_range, alloc.start_bus, bus_count);
addBridgeApertures(bridge);
// The bridge decodes the whole 16-bit I/O space toward its bus — the
// window functions' I/O BARs must register-contain within (M19.2).
_ = bridge.addResource(.io_port, 0, 1 << 16);
try enumeratePci(device_tree, bridge, hal, alloc.*); // The function walk itself retired to ring 3 (M19.3): the pci-bus
// driver claims this bridge, repeats the scan through its ECAM grant,
// and device_registers what it finds — the kernel seeds only the
// bridge. The scan's equivalence was proven before the hand-off
// (pci-scan), and the walk's history is in git if archaeology calls.
} }
} }
/// Brute-force scan the ECAM window's bus range for present PCI functions. No /// The boot memory map, stored at discover() entry for the aperture derivation
/// bridge recursion yet: on the ECAM path the host bridge decodes every bus in /// below (and, in M20, for the acpi-tables node's containment windows).
/// the window, so scanning the declared range finds everything QEMU exposes. var boot_memory_regions: []const boot_handoff.MemoryRegion = &.{};
fn enumeratePci(
device_tree: *DeviceTree,
bridge: *device_model.Device,
hal: Hal,
alloc: McfgAllocation,
) !void {
var bus: u16 = alloc.start_bus;
while (bus <= alloc.end_bus) : (bus += 1) {
var device: u8 = 0;
while (device < 32) : (device += 1) {
const h0: *align(1) const PciHeader = @ptrCast(pciConfigurationPtr(alloc, hal, @intCast(bus), device, 0));
if (h0.vendor_id == 0xFFFF) continue; // no function 0 => slot empty
const funcs: u8 = if (h0.header_type & 0x80 != 0) 8 else 1; /// The bridge's MMIO apertures, derived from the boot memory map's holes
var function: u8 = 0; /// (docs/m19-m20-plan.md decision 2): registered PCI functions carry BAR
while (function < funcs) : (function += 1) { /// resources, and `device_register` containment demands the bridge own windows
const configuration = pciConfigurationPtr(alloc, hal, @intCast(bus), device, function); /// that cover them. Everything the firmware described is "not hole"; the low
const h: *align(1) const PciHeader = @ptrCast(configuration); /// aperture runs from the end of the described space below 4 GiB up to the
if (h.vendor_id == 0xFFFF) continue; /// I/O-APIC region, the high one from 4 GiB (or the end of RAM above it) to
/// the 46-bit line. Coarse, mechanical, and AML-free — available at boot no
var nb: [24]u8 = undefined; /// matter what later moved to user space.
const nm = std.fmt.bufPrint(&nb, "{s}:{x:0>2}:{x:0>2}.{d}", .{ fn addBridgeApertures(bridge: *device_model.Device) void {
bridge.name(), bus, device, function, // Below 4 GiB the described regions are sparse (RAM low, firmware flash
}) catch "pcidev"; // and tables high), so the holes are the *gaps between* them — a single
const node = try device_tree.addChild(bridge, .pci_device, nm); // "after the last region" rule dies on OVMF's flash at the very top.
// Resource 0 is the function's own 4 KiB ECAM configuration space. A // Sort-merge the described ranges, then keep the three largest gaps
// claimed PCI driver mmio_maps this to reach its command register, // (resource slots are bounded at 8 per device; ECAM + bus range + 3 + the
// BARs, and — the point — its capability list (MSI/MSI-X, PCIe // high aperture fits). Above 4 GiB one aperture runs from the end of the
// extended caps), without any new syscall. Physical address per the // described space to the 46-bit line.
// ECAM formula (same as pciConfigurationPtr). const Range = struct { base: u64, end: u64 };
const config_physical = alloc.base_address + var below: [64]Range = undefined;
(@as(u64, @as(u8, @intCast(bus)) - alloc.start_bus) << 20) + var below_count: usize = 0;
(@as(u64, device) << 15) + (@as(u64, function) << 12); var high_end: u64 = 1 << 32;
_ = node.addResource(.memory, config_physical, abi.page_size); for (boot_memory_regions) |region| {
node.ids.pci_vendor = h.vendor_id; const end = region.base + region.pages * 4096;
node.ids.pci_device = h.device_id; // Above 4 GiB only *usable RAM* blocks the aperture: OVMF describes
node.ids.pci_class = (@as(u24, h.class_code) << 16) | // its own 64-bit PCI window as a reserved region and then programs
(@as(u24, h.subclass) << 8) | h.prog_if; // BARs inside it — honoring reserved there would exclude the very
node.ids.pci_bdf = (@as(u16, @intCast(bus)) << 8) | (@as(u16, device) << 3) | function; // space BARs live in. Below 4 GiB every described region blocks (the
// kernel image, the tables, the ramdisk all live there). Bring-up
// BARs only exist in header type 0 (normal devices), not bridges. // trust: only the bridge's claimant can register into the aperture.
if (h.header_type & 0x7F == 0) addBars(node, configuration); if (region.kind == .usable and end > high_end) high_end = end;
if (region.base >= (1 << 32) or below_count == below.len) continue;
below[below_count] = .{ .base = region.base, .end = @min(end, 1 << 32) };
below_count += 1;
}
// Insertion sort by base (the map is small and this runs once at boot).
for (1..below_count) |i| {
const key = below[i];
var j = i;
while (j > 0 and below[j - 1].base > key.base) : (j -= 1) below[j] = below[j - 1];
below[j] = key;
}
// Walk the sorted ranges, collecting inter-region gaps of at least 1 MiB.
var gaps: [3]Range = .{Range{ .base = 0, .end = 0 }} ** 3;
var cursor: u64 = 0;
var index: usize = 0;
while (index <= below_count) : (index += 1) {
const gap_end = if (index == below_count) (1 << 32) else below[index].base;
if (gap_end > cursor and gap_end - cursor >= (1 << 20)) {
// Keep the three largest, replacing the smallest kept so far.
var smallest: usize = 0;
for (gaps, 0..) |gap, gi| {
if (gap.end - gap.base < gaps[smallest].end - gaps[smallest].base) smallest = gi;
}
if (gap_end - cursor > gaps[smallest].end - gaps[smallest].base) {
gaps[smallest] = .{ .base = cursor, .end = gap_end };
} }
} }
if (index < below_count and below[index].end > cursor) cursor = below[index].end;
} }
for (gaps) |gap| {
if (gap.end > gap.base) _ = bridge.addResource(.memory, gap.base, gap.end - gap.base);
} }
_ = bridge.addResource(.memory, high_end, (@as(u64, 1) << 46) - high_end);
/// Record and size the memory/IO windows named by a device's Base Address
/// Registers. Sizing is the standard probe: disable decode, write all-ones, read
/// back the writable (address) bits, restore. `size = ~mask + 1`.
fn addBars(node: *device_model.Device, configuration: [*]align(1) u8) void {
// Stop the device decoding its BARs while we transiently write all-ones.
const command = rd(u16, configuration, 0x04);
wr(u16, configuration, 0x04, command & ~@as(u16, 0b11));
var i: usize = 0;
while (i < 6) : (i += 1) {
const off = 0x10 + i * 4;
const orig = rd(u32, configuration, off);
if (orig == 0) continue;
if (orig & 1 != 0) {
// I/O-space BAR (16-bit address space on x86).
wr(u32, configuration, off, 0xFFFF_FFFF);
const readback = rd(u32, configuration, off);
wr(u32, configuration, off, orig);
const mask = readback & 0xFFFF_FFFC;
const size: u32 = if (mask == 0) 0 else (~mask +% 1) & 0xFFFF;
_ = node.addResource(.io_port, orig & 0xFFFF_FFFC, size);
} else if ((orig >> 1) & 0x3 == 2) {
// 64-bit memory BAR: this BAR pair spans two configuration slots.
const orig_hi = rd(u32, configuration, off + 4);
wr(u32, configuration, off, 0xFFFF_FFFF);
wr(u32, configuration, off + 4, 0xFFFF_FFFF);
const lo = rd(u32, configuration, off);
const hi = rd(u32, configuration, off + 4);
wr(u32, configuration, off, orig);
wr(u32, configuration, off + 4, orig_hi);
const readback = (@as(u64, hi) << 32) | (lo & 0xFFFF_FFF0);
const size: u64 = if (readback == 0) 0 else ~readback +% 1;
const address = (@as(u64, orig_hi) << 32) | (orig & 0xFFFF_FFF0);
_ = node.addResource(.memory, address, size);
i += 1; // consumed the high half
} else {
// 32-bit memory BAR.
wr(u32, configuration, off, 0xFFFF_FFFF);
const readback = rd(u32, configuration, off);
wr(u32, configuration, off, orig);
const mask = readback & 0xFFFF_FFF0;
const size: u32 = if (mask == 0) 0 else ~mask +% 1;
_ = node.addResource(.memory, orig & 0xFFFF_FFF0, size);
}
}
wr(u16, configuration, 0x04, command); // restore decode
} }
/// HPET -> a timer node with its register block as an MMIO resource, plus the GSI /// HPET -> a timer node with its register block as an MMIO resource, plus the GSI
@@ -1186,16 +1144,6 @@ fn readCntRegister(base: [*]align(1) const u8, len: usize, xoff: usize, legacy_o
/// The mapped configuration space of one PCI function (its 4 KiB ECAM page). Mapped /// The mapped configuration space of one PCI function (its 4 KiB ECAM page). Mapped
/// writable so BAR sizing can probe it; reads and writes both go through here. /// writable so BAR sizing can probe it; reads and writes both go through here.
fn pciConfigurationPtr(alloc: McfgAllocation, hal: Hal, bus: u8, device: u8, function: u8) [*]align(1) u8 {
const physical = alloc.base_address +
(@as(u64, bus - alloc.start_bus) << 20) +
(@as(u64, device) << 15) +
(@as(u64, function) << 12);
// Map the configuration page (writable, for BAR sizing) and use the virtual
// address the HAL hands back.
return @ptrFromInt(hal.mapMmio(physical, abi.page_size, true));
}
/// Read a little-endian integer at `off` from a (possibly unaligned) byte pointer. /// Read a little-endian integer at `off` from a (possibly unaligned) byte pointer.
/// x86 is little-endian and native, so an unaligned load suffices. /// x86 is little-endian and native, so an unaligned load suffices.
fn rd(comptime T: type, bytes: [*]align(1) const u8, off: usize) T { fn rd(comptime T: type, bytes: [*]align(1) const u8, off: usize) T {
@@ -1203,12 +1151,6 @@ fn rd(comptime T: type, bytes: [*]align(1) const u8, off: usize) T {
return p.*; return p.*;
} }
/// Write a little-endian integer at `off` through a (possibly unaligned) pointer.
fn wr(comptime T: type, bytes: [*]align(1) u8, off: usize, value: T) void {
const p: *align(1) T = @ptrCast(bytes + off);
p.* = value;
}
// --- tests ------------------------------------------------------------------ // --- tests ------------------------------------------------------------------
test "eisaIdToStr decodes a packed EISA id" { test "eisaIdToStr decodes a packed EISA id" {
+2 -1
View File
@@ -72,7 +72,8 @@ pub fn discover(
var device_tree = try DeviceTree.init(allocator); var device_tree = try DeviceTree.init(allocator);
if (boot_information.acpi_rsdp != 0) { if (boot_information.acpi_rsdp != 0) {
try acpi.discover(boot_information.acpi_rsdp, &device_tree, hal); const memory_regions = @as([*]const boot_handoff.MemoryRegion, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.memory_map.regions)))[0..boot_information.memory_map.len];
try acpi.discover(boot_information.acpi_rsdp, memory_regions, &device_tree, hal);
} else { } else {
// No ACPI RSDP. A device-tree boot would parse its blob here; today that // No ACPI RSDP. A device-tree boot would parse its blob here; today that
// path is a stub, so this reports the machine described itself no way we // path is a stub, so this reports the machine described itself no way we
+253
View File
@@ -0,0 +1,253 @@
//! /system/drivers/pci-bus — the PCI bus driver: enumeration moved out of ring 0
//! (docs/m19-m20-plan.md, M19). The device manager matches the `pci_host_bridge`
//! node and spawns one instance per bridge, the bridge's device id as argv[1] —
//! the same per-device contract as usb-xhci-bus.
//!
//! M19.1 (this increment): claim the bridge, map its ECAM window (resource 0;
//! the bus range and the MMIO apertures follow it), walk every
//! bus/device/function config header, and log what the walk finds — ending
//! with "pci-bus: N functions found", which the `pci-scan` scenario compares
//! against the kernel's own enumeration. Registration and reports (M19.2), and
//! the kernel walk's retirement (M19.3), build on this proven-equivalent scan.
const std = @import("std");
const runtime = @import("runtime");
const protocol = runtime.device_manager_protocol;
const device = runtime.device;
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
var line: [128]u8 = undefined;
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
}
var bridge_id: u64 = protocol.no_device;
var ecam_base: usize = 0;
var ecam_physical: u64 = 0;
var start_bus: u64 = 0;
var bus_count: u64 = 0;
var manager_handle: runtime.ipc.Handle = 0;
/// One aligned 32-bit read from a function's configuration space.
fn configRead(bus: u64, dev: u64, function: u64, offset: u64) u32 {
const address = ecam_base + (((bus - start_bus) << 20) | (dev << 15) | (function << 12) | offset);
const register: *volatile u32 = @ptrFromInt(address);
return register.*;
}
fn configWrite(bus: u64, dev: u64, function: u64, offset: u64, value: u32) void {
const address = ecam_base + (((bus - start_bus) << 20) | (dev << 15) | (function << 12) | offset);
const register: *volatile u32 = @ptrFromInt(address);
register.* = value;
}
fn configRead16(bus: u64, dev: u64, function: u64, offset: u64) u16 {
const word = configRead(bus, dev, function, offset & ~@as(u64, 3));
return @truncate(word >> @intCast((offset & 3) * 8));
}
fn configWrite16(bus: u64, dev: u64, function: u64, offset: u64, value: u16) void {
const aligned = offset & ~@as(u64, 3);
const shift: u5 = @intCast((offset & 3) * 8);
const word = configRead(bus, dev, function, aligned);
const mask = @as(u32, 0xFFFF) << shift;
configWrite(bus, dev, function, aligned, (word & ~mask) | (@as(u32, value) << shift));
}
/// Claim the bridge, map the ECAM, hello the manager, then scan.
fn initialise(endpoint: runtime.ipc.Handle) bool {
_ = endpoint;
if (!device.claim(bridge_id)) {
writeLine("pci-bus: unable to claim bridge device {d}\n", .{bridge_id});
return false;
}
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
_ = runtime.system.write("pci-bus: out of memory\n");
return false;
};
const total = device.enumerate(buffer);
const descriptor = for (buffer[0..@min(total, buffer.len)]) |d| {
if (d.id == bridge_id) break d;
} else {
writeLine("pci-bus: device {d} not in the device tree\n", .{bridge_id});
return false;
};
// Resource 0 is the ECAM window (1 MiB of config space per bus); the bus
// range rides beside it. The MMIO apertures (M19.0) come after both.
if (descriptor.resource_count < 2 or descriptor.resources[0].kind != @intFromEnum(device.ResourceKind.memory)) {
_ = runtime.system.write("pci-bus: bridge has no ECAM window\n");
return false;
}
const bus_range = for (descriptor.resources[0..@intCast(descriptor.resource_count)]) |resource| {
if (resource.kind == @intFromEnum(device.ResourceKind.bus_range)) break resource;
} else {
_ = runtime.system.write("pci-bus: bridge has no bus range\n");
return false;
};
start_bus = bus_range.start;
bus_count = bus_range.len;
ecam_physical = descriptor.resources[0].start;
ecam_base = device.mmioMap(bridge_id, 0) orelse {
_ = runtime.system.write("pci-bus: ECAM mmio_map failed\n");
return false;
};
// The handshake, then the scan (reports join in M19.2).
var manager: ?runtime.ipc.Handle = null;
var tries: u32 = 0;
while (manager == null and tries < 100) : (tries += 1) {
manager = runtime.ipc.lookup(.device_manager);
if (manager == null) runtime.system.sleep(20);
}
const h = manager orelse {
_ = runtime.system.write("pci-bus: no device manager to hello\n");
return false;
};
const hello = protocol.Hello{ .role = @intFromEnum(protocol.Role.bus), .device_id = bridge_id };
var reply: [protocol.message_maximum]u8 = undefined;
const n = runtime.ipc.call(h, std.mem.asBytes(&hello), &reply) catch {
_ = runtime.system.write("pci-bus: hello call failed\n");
return false;
};
if (n < protocol.reply_size or std.mem.bytesToValue(protocol.HelloReply, reply[0..protocol.reply_size]).status != 0) {
_ = runtime.system.write("pci-bus: hello refused\n");
return false;
}
manager_handle = h;
scan();
return true;
}
/// The brute-force walk the kernel does today, from ring 3: every bus in the
/// range, 32 devices, 8 functions; vendor id FFFFh means nothing decodes there,
/// and only multifunction devices get their functions 1..7 probed.
fn scan() void {
var found: u32 = 0;
var bus: u64 = start_bus;
while (bus < start_bus + bus_count) : (bus += 1) {
var dev: u64 = 0;
while (dev < 32) : (dev += 1) {
const first = configRead(bus, dev, 0, 0);
if (first & 0xFFFF == 0xFFFF) continue;
const multifunction = (configRead(bus, dev, 0, 0x0C) >> 16) & 0x80 != 0;
var function: u64 = 0;
while (function < 8) : (function += 1) {
if (function != 0 and !multifunction) break;
const vendor_device = configRead(bus, dev, function, 0);
if (vendor_device & 0xFFFF == 0xFFFF) continue;
const class_revision = configRead(bus, dev, function, 0x08);
found += 1;
writeLine("pci-bus: {d}:{d}.{d} class 0x{x:0>6}\n", .{ bus, dev, function, class_revision >> 8 });
registerAndReport(bus, dev, function, class_revision >> 8);
}
}
}
writeLine("pci-bus: {d} functions found\n", .{found});
}
/// Register one function under the bridge and report it to the manager. The
/// descriptor mirrors the kernel's own recording byte for byte — config slice
/// as resource 0, then the sized BARs — so during coexistence the idempotent
/// device_register (M19.0) returns the kernel's existing node id rather than
/// growing a duplicate, and the report carries the id drivers already use.
fn registerAndReport(bus: u64, dev: u64, function: u64, class_triple: u32) void {
var descriptor = std.mem.zeroes(device.DeviceDescriptor);
descriptor.class = @intFromEnum(device.DeviceClass.pci_device);
descriptor.pci_class = class_triple;
descriptor.resources[0] = .{
.kind = @intFromEnum(device.ResourceKind.memory),
.start = ecam_physical + (((bus - start_bus) << 20) | (dev << 15) | (function << 12)),
.len = 4096,
};
descriptor.resource_count = 1;
// The standard BAR-sizing probe, exactly as the kernel does it: decode off,
// write all-ones, read the writable mask back, restore. Header type 0 only.
const header_type = (configRead(bus, dev, function, 0x0C) >> 16) & 0x7F;
if (header_type == 0) {
const command = configRead16(bus, dev, function, 0x04);
configWrite16(bus, dev, function, 0x04, command & ~@as(u16, 0b11));
var i: u64 = 0;
while (i < 6) : (i += 1) {
if (descriptor.resource_count >= 8) break;
const off = 0x10 + i * 4;
const original = configRead(bus, dev, function, off);
if (original == 0) continue;
const slot: usize = @intCast(descriptor.resource_count);
if (original & 1 != 0) {
configWrite(bus, dev, function, off, 0xFFFF_FFFF);
const readback = configRead(bus, dev, function, off);
configWrite(bus, dev, function, off, original);
const mask = readback & 0xFFFF_FFFC;
const size: u32 = if (mask == 0) 0 else (~mask +% 1) & 0xFFFF;
if (size == 0) continue; // unimplemented BAR — nothing to register
descriptor.resources[slot] = .{ .kind = @intFromEnum(device.ResourceKind.io_port), .start = original & 0xFFFF_FFFC, .len = size };
descriptor.resource_count += 1;
} else if ((original >> 1) & 0x3 == 2) {
const original_high = configRead(bus, dev, function, off + 4);
configWrite(bus, dev, function, off, 0xFFFF_FFFF);
configWrite(bus, dev, function, off + 4, 0xFFFF_FFFF);
const lo = configRead(bus, dev, function, off);
const hi = configRead(bus, dev, function, off + 4);
configWrite(bus, dev, function, off, original);
configWrite(bus, dev, function, off + 4, original_high);
const readback = (@as(u64, hi) << 32) | (lo & 0xFFFF_FFF0);
const size: u64 = if (readback == 0) 0 else ~readback +% 1;
i += 1; // consumed the high half regardless
if (size == 0) continue;
descriptor.resources[slot] = .{ .kind = @intFromEnum(device.ResourceKind.memory), .start = (@as(u64, original_high) << 32) | (original & 0xFFFF_FFF0), .len = size };
descriptor.resource_count += 1;
} else {
configWrite(bus, dev, function, off, 0xFFFF_FFFF);
const readback = configRead(bus, dev, function, off);
configWrite(bus, dev, function, off, original);
const mask = readback & 0xFFFF_FFF0;
const size: u32 = if (mask == 0) 0 else ~mask +% 1;
if (size == 0) continue;
descriptor.resources[slot] = .{ .kind = @intFromEnum(device.ResourceKind.memory), .start = original & 0xFFFF_FFF0, .len = size };
descriptor.resource_count += 1;
}
}
configWrite16(bus, dev, function, 0x04, command);
}
const registered = device.register(bridge_id, &descriptor) orelse {
writeLine("pci-bus: register refused for {d}:{d}.{d}\n", .{ bus, dev, function });
return;
};
const report = protocol.ChildAdded{
.parent = bridge_id,
.bus_address = (bus << 8) | (dev << 3) | function,
.identity = class_triple,
.device_id = registered,
};
var reply: [protocol.message_maximum]u8 = undefined;
_ = runtime.ipc.call(manager_handle, std.mem.asBytes(&report), &reply) catch {
writeLine("pci-bus: child report for {d}:{d}.{d} failed\n", .{ bus, dev, function });
};
}
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
_ = message;
_ = reply;
_ = sender;
_ = capability;
return 0;
}
pub fn main(init: runtime.process.Init) void {
const argument = init.arguments.get(1) orelse return; // bare (ramdisk sweep): stay silent
bridge_id = std.fmt.parseInt(u64, argument, 10) catch {
writeLine("pci-bus: malformed bridge device id '{s}'\n", .{argument});
return;
};
runtime.service.run(protocol.message_maximum, .{
.init = initialise,
.on_message = onMessage,
});
}
pub const panic = runtime.panic;
comptime {
_ = &runtime.start._start; // pull the runtime entry shim into the image
}
+20
View File
@@ -180,6 +180,26 @@ pub fn register(parent_id: u64, owner: u32, descriptor: *const device_abi.Device
if (!ok) return error.NotContained; if (!ok) return error.NotContained;
} }
// Idempotent on exact match (docs/m19-m20-plan.md decision 3): a restarted
// registering bus re-registers what it rediscovers, and the table has no
// unregister — an identical (class, identity, resources) child under the
// same parent returns the existing id instead of appending a duplicate.
for (devices[0..count]) |*existing| {
if (existing.parent != parent_id) continue;
if (existing.class != descriptor.class) continue;
if (existing.pci_class != descriptor.pci_class) continue;
if (existing.hid_len != descriptor.hid_len) continue;
if (!std.mem.eql(u8, existing.hid[0..@intCast(existing.hid_len)], descriptor.hid[0..@intCast(descriptor.hid_len)])) continue;
if (existing.resource_count != descriptor.resource_count) continue;
var same = true;
for (0..@intCast(descriptor.resource_count)) |i| {
const a = existing.resources[i];
const b = descriptor.resources[i];
if (a.kind != b.kind or a.start != b.start or a.len != b.len) same = false;
}
if (same) return existing.id;
}
var d = std.mem.zeroes(device_abi.DeviceDescriptor); var d = std.mem.zeroes(device_abi.DeviceDescriptor);
d.id = count; d.id = count;
d.parent = parent_id; d.parent = parent_id;
+20 -1
View File
@@ -298,6 +298,8 @@ fn systemDeviceEnumerate(state: *architecture.CpuState) void {
/// device_claim(id) -> 0/-1: take exclusive ownership of a device for this process. /// device_claim(id) -> 0/-1: take exclusive ownership of a device for this process.
fn systemDeviceClaim(state: *architecture.CpuState) void { fn systemDeviceClaim(state: *architecture.CpuState) void {
const claim_flags = sync.enter();
defer sync.leave(claim_flags);
if (devices_broker.claim(architecture.systemCallArg(state, 0), scheduler.current().id)) if (devices_broker.claim(architecture.systemCallArg(state, 0), scheduler.current().id))
architecture.setSystemCallResult(state, 0) architecture.setSystemCallResult(state, 0)
else else
@@ -312,10 +314,22 @@ fn systemMmioMap(state: *architecture.CpuState) void {
const resource_index = architecture.systemCallArg(state, 1); const resource_index = architecture.systemCallArg(state, 1);
const t = scheduler.current(); const t = scheduler.current();
if (t.aspace == 0) return fail(state); if (t.aspace == 0) return fail(state);
// Read the broker table under the lock: ring-3 device_register (M19) now
// mutates it concurrently on other cores, so a lock-free read here could
// see a torn resource (and a torn length used to panic the arithmetic
// below on integer overflow).
const r = blk: {
const flags = sync.enter();
defer sync.leave(flags);
const owner = devices_broker.ownerOf(device_id) orelse return fail(state); const owner = devices_broker.ownerOf(device_id) orelse return fail(state);
if (owner != t.id) return fail(state); // not claimed by this process if (owner != t.id) return fail(state); // not claimed by this process
const r = devices_broker.resourceOf(device_id, resource_index) orelse return fail(state); break :blk devices_broker.resourceOf(device_id, resource_index) orelse return fail(state);
};
if (r.kind != @intFromEnum(device_abi.ResourceKind.memory)) return fail(state); if (r.kind != @intFromEnum(device_abi.ResourceKind.memory)) return fail(state);
// A zero-length or wrapping window is not mappable — fail cleanly rather
// than underflow `r.len - 1`.
if (r.len == 0) return fail(state);
if (@addWithOverflow(r.start, r.len)[1] != 0) return fail(state);
if (t.device_map_next == 0) t.device_map_next = device_arena_base; if (t.device_map_next == 0) t.device_map_next = device_arena_base;
const first = r.start & ~@as(u64, page_size - 1); const first = r.start & ~@as(u64, page_size - 1);
@@ -451,6 +465,11 @@ fn systemDeviceRegister(state: *architecture.CpuState) void {
var descriptor: device_abi.DeviceDescriptor = undefined; var descriptor: device_abi.DeviceDescriptor = undefined;
if (!ipc.copyFromUser(t.aspace, descriptor_ptr, std.mem.asBytes(&descriptor))) return fail(state); if (!ipc.copyFromUser(t.aspace, descriptor_ptr, std.mem.asBytes(&descriptor))) return fail(state);
// Under the big kernel lock: the broker's table is also mutated by the
// death sweep (releaseAllOwnedBy) and read by enumerate on other cores —
// ring-3 registration (M19) made those genuinely concurrent.
const flags = sync.enter();
defer sync.leave(flags);
const id = devices_broker.register(parent_id, t.id, &descriptor) catch return fail(state); const id = devices_broker.register(parent_id, t.id, &descriptor) catch return fail(state);
architecture.setSystemCallResult(state, id); architecture.setSystemCallResult(state, id);
} }
+185 -9
View File
@@ -144,6 +144,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
usbReportTest(boot_information); usbReportTest(boot_information);
} else if (eql(case, "device-list")) { } else if (eql(case, "device-list")) {
deviceListTest(boot_information); deviceListTest(boot_information);
} else if (eql(case, "pci-scan")) {
pciScanTest(boot_information);
} else if (eql(case, "initial-ramdisk")) { } else if (eql(case, "initial-ramdisk")) {
initialRamdiskTest(boot_information); initialRamdiskTest(boot_information);
} else if (eql(case, "vfs")) { } else if (eql(case, "vfs")) {
@@ -268,20 +270,56 @@ fn discoveryTest() void {
// M15: every PCI function now carries its own 4 KiB ECAM configuration space as // M15: every PCI function now carries its own 4 KiB ECAM configuration space as
// resource 0 — the window a driver mmio_maps to walk its capability list (MSI etc). // resource 0 — the window a driver mmio_maps to walk its capability list (MSI etc).
// M19.3: the kernel seeds only the bridge; functions arrive by the ring-3
// scan (proven equivalent in pci-scan before the walk retired).
var buffer: [64]device_abi.DeviceDescriptor = undefined; var buffer: [64]device_abi.DeviceDescriptor = undefined;
const n = @min(devices_broker.enumerate(&buffer), buffer.len); const n = @min(devices_broker.enumerate(&buffer), buffer.len);
var pci_functions: u32 = 0; var bridges: u32 = 0;
var pci_config_ok = true; var bridge_shape_ok = false;
for (buffer[0..n]) |d| {
if (d.class != @intFromEnum(device_abi.DeviceClass.pci_host_bridge)) continue;
bridges += 1;
var has_bus_range = false;
var has_io = false;
var memory_windows: u32 = 0;
for (d.resources[0..@intCast(d.resource_count)]) |resource| {
if (resource.kind == @intFromEnum(device_abi.ResourceKind.bus_range)) has_bus_range = true;
if (resource.kind == @intFromEnum(device_abi.ResourceKind.io_port)) has_io = true;
if (resource.kind == @intFromEnum(device_abi.ResourceKind.memory)) memory_windows += 1;
}
// ECAM plus at least one MMIO aperture, the bus range, the I/O window.
if (has_bus_range and has_io and memory_windows >= 2) bridge_shape_ok = true;
}
check("a PCI host bridge was seeded (MCFG)", bridges >= 1);
check("the bridge carries ECAM, apertures, bus range, and the I/O window", bridge_shape_ok);
// M19.0: every PCI memory resource (config slice and BARs alike) must be
// contained in one of its parent bridge's windows — the aperture derivation
// from the memory map is what makes a future user-space device_register of
// these functions pass containment. This is the assert that catches a
// too-coarse hole computation before M19.2 would.
var bars_contained = true;
for (buffer[0..n]) |d| { for (buffer[0..n]) |d| {
if (d.class != @intFromEnum(device_abi.DeviceClass.pci_device)) continue; if (d.class != @intFromEnum(device_abi.DeviceClass.pci_device)) continue;
pci_functions += 1; if (d.parent >= n) {
const has_config = d.resource_count >= 1 and bars_contained = false;
d.resources[0].kind == @intFromEnum(device_abi.ResourceKind.memory) and continue;
d.resources[0].len == abi.page_size;
if (!has_config) pci_config_ok = false;
} }
check("PCI functions were enumerated (MCFG/ECAM)", pci_functions >= 1); const bridge = buffer[@intCast(d.parent)];
check("each PCI function exposes its ECAM config space as resource 0", pci_config_ok); for (d.resources[0..@intCast(d.resource_count)]) |r| {
if (r.kind != @intFromEnum(device_abi.ResourceKind.memory)) continue;
var inside = false;
for (bridge.resources[0..@intCast(bridge.resource_count)]) |w| {
if (w.kind != @intFromEnum(device_abi.ResourceKind.memory)) continue;
if (r.start >= w.start and r.start + r.len <= w.start + w.len) inside = true;
}
if (!inside) {
bars_contained = false;
log(" escaping BAR: 0x{x}+0x{x} on device {d}\n", .{ r.start, r.len, d.id });
}
}
}
check("every PCI BAR lies inside a bridge aperture (M19.0)", bars_contained);
result(); result();
} }
@@ -1765,6 +1803,115 @@ fn deviceListTest(boot_information: *const BootInformation) void {
result(); result();
} }
/// M19.1: the ring-3 PCI scan agrees with the kernel's. The manager spawns
/// pci-bus for the host bridge; the driver walks the same ECAM window through
/// its mmio_map grant and must find exactly the functions the kernel's own
/// enumeration recorded — the equivalence that licenses retiring the kernel
/// walk in M19.3.
fn pciScanTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: pci-scan\n", .{});
if (boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over an initial_ramdisk", false);
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
// Post-flip (M19.3) ground truth: the kernel no longer enumerates PCI
// functions, so equivalence inverts — the broker's function count after
// the scan must equal what the driver itself reported finding.
var buffer: [64]device_abi.DeviceDescriptor = undefined;
const n = @min(devices_broker.enumerate(&buffer), buffer.len);
var boot_pci: u32 = 0;
for (buffer[0..n]) |d| {
if (d.class == @intFromEnum(device_abi.DeviceClass.pci_device)) boot_pci += 1;
}
check("the kernel seeded no PCI functions (the walk retired)", boot_pci == 0);
process.setInitialRamdisk(image);
process.write_count = 0;
var manager: u32 = 0;
var i: u32 = 0;
while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue;
if (!eql(item.name, "device-manager")) continue;
manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-pci-restart" }, scheduler.currentId(), null) catch 0;
break;
}
check("device-manager spawned (test-pci-restart mode)", manager != 0);
// First scan: wait for the driver's count line and parse the number.
const count_prefix = "pci-bus: ";
const count_suffix = " functions found";
var reported: u32 = 0;
scheduler.setPriority(1);
var deadline = architecture.millis() + 15000;
while (architecture.millis() < deadline and reported == 0) {
if (process.write_len > count_prefix.len + count_suffix.len and eql(process.write_buffer[0..count_prefix.len], count_prefix)) {
const line = process.write_buffer[0..process.write_len];
const digits_end = std.mem.indexOf(u8, line, count_suffix) orelse {
scheduler.yield();
continue;
};
reported = std.fmt.parseInt(u32, line[count_prefix.len..digits_end], 10) catch 0;
}
scheduler.yield();
}
scheduler.setPriority(4);
check("the ring-3 scan reported a function count", reported >= 1);
// Every reported function was registered: the broker holds exactly them.
var registered: [64]device_abi.DeviceDescriptor = undefined;
const r = @min(devices_broker.enumerate(&registered), registered.len);
var registered_pci: u32 = 0;
for (registered[0..r]) |d| {
if (d.class == @intFromEnum(device_abi.DeviceClass.pci_device)) registered_pci += 1;
}
check("the broker holds exactly the reported functions", registered_pci == reported);
const kernel_count = reported; // the no-duplicate check below reuses it
// The restart drill: the manager kills pci-bus after its reports; the
// respawn re-claims, re-scans, and re-registers.
const restart_marker = "device-manager: restarting pci-bus";
scheduler.setPriority(1);
deadline = architecture.millis() + 15000;
var restarted = false;
while (architecture.millis() < deadline and !restarted) {
if (process.write_len >= restart_marker.len and eql(process.write_buffer[0..restart_marker.len], restart_marker)) restarted = true;
scheduler.yield();
}
scheduler.setPriority(4);
check("the manager restarted pci-bus", restarted);
var marker_buffer: [48]u8 = undefined;
const marker = std.fmt.bufPrint(&marker_buffer, "pci-bus: {d} functions found", .{reported}) catch "";
scheduler.setPriority(1);
deadline = architecture.millis() + 15000;
var seen = false;
while (architecture.millis() < deadline and !seen) {
if (process.write_len >= marker.len and eql(process.write_buffer[0..marker.len], marker)) seen = true;
scheduler.yield();
}
scheduler.setPriority(4);
check("the respawned scan reported the same count", seen);
// No duplicates: the registrations deduped against the kernel's own nodes
// on the first pass, and against themselves on the second.
var after: [64]device_abi.DeviceDescriptor = undefined;
const m = @min(devices_broker.enumerate(&after), after.len);
var after_count: u32 = 0;
for (after[0..m]) |d| {
if (d.class == @intFromEnum(device_abi.DeviceClass.pci_device)) after_count += 1;
}
check("no duplicate PCI nodes after register + restart + re-register", after_count == kernel_count);
result();
}
/// The whole user-side surface at once: spawn process-test's supervisor role, /// The whole user-side surface at once: spawn process-test's supervisor role,
/// which — entirely from ring 3 — creates an exit endpoint, spawns its two /// which — entirely from ring 3 — creates an exit endpoint, spawns its two
/// children supervised, sees them in process_enumerate, kills them (one blocked, /// children supervised, sees them in process_enumerate, kills them (one blocked,
@@ -2085,6 +2232,35 @@ fn hpetGsi() ?u32 {
/// land in the device table with the containment invariant intact. /// land in the device table with the containment invariant intact.
fn busTest(boot_information: *const BootInformation) void { fn busTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: bus\n", .{}); log("DANOS-TEST-BEGIN: bus\n", .{});
// M19.0: device_register is idempotent on exact match — a restarted
// registering bus must not duplicate its children. Driven directly against
// the broker: claim an unclaimed node, register the same (class, hid,
// resourceless) child twice, expect one id and one table entry.
{
const me = scheduler.currentId();
var probe: [1]device_abi.DeviceDescriptor = undefined;
const total = devices_broker.enumerate(&probe);
check("device tree is seeded for the idempotence check", total >= 1);
if (devices_broker.ownerOf(0) == null) {
check("claimed device 0 for the idempotence check", devices_broker.claim(0, me));
var child = std.mem.zeroes(device_abi.DeviceDescriptor);
child.class = @intFromEnum(device_abi.DeviceClass.unknown);
child.pci_class = device_abi.no_pci_class;
child.hid_len = 4;
child.hid[0..4].* = "idem".*;
const first = devices_broker.register(0, me, &child) catch 0;
check("first register succeeded", first != 0);
const before = devices_broker.enumerate(&probe);
const second = devices_broker.register(0, me, &child) catch 0;
check("re-register returned the same id", second == first);
check("re-register grew nothing", devices_broker.enumerate(&probe) == before);
devices_broker.releaseAllOwnedBy(me);
} else {
check("device 0 unexpectedly claimed before the idempotence check", false);
}
}
if (boot_information.initial_ramdisk_len == 0) { if (boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over an initial_ramdisk", false); check("bootloader handed over an initial_ramdisk", false);
result(); result();
@@ -73,8 +73,13 @@ pub const ChildAdded = extern struct {
parent: u64, parent: u64,
/// Where on the bus (for USB: the root port number, 1-based). /// Where on the bus (for USB: the root port number, 1-based).
bus_address: u64, bus_address: u64,
/// Bus-specific identity (for USB: the PORTSC port-speed class). /// Bus-specific identity (for USB: the PORTSC port-speed class; for PCI:
/// the class triple).
identity: u64, identity: u64,
/// The kernel device id this child was `device_register`ed as — what the
/// manager hands a matched driver as its argv assignment — or `no_device`
/// for an unregistered leaf (a USB port before the descriptor track).
device_id: u64 = no_device,
}; };
pub const child_added_size = @sizeOf(ChildAdded); pub const child_added_size = @sizeOf(ChildAdded);
@@ -50,17 +50,26 @@ fn driverFor(d: device.DeviceDescriptor) ?[]const u8 {
/// pci-class.zig decodes. /// pci-class.zig decodes.
const xhci_pci_class: u64 = 0x0C_03_30; const xhci_pci_class: u64 = 0x0C_03_30;
/// The bus driver that serves a PCI function, or null. A machine can carry /// The driver that serves a *reported* PCI function (M19.3: matching moved
/// several identical controllers — one driver instance per device, the id as /// from the boot snapshot to the bus reports), or null. A machine can carry
/// argv[1]. These drivers speak the protocol: a hello is expected. /// several identical controllers — one driver instance per reported device,
fn pciDriverFor(d: device.DeviceDescriptor) ?[]const u8 { /// its registered id as argv[1].
if (d.class != @intFromEnum(device.DeviceClass.pci_device)) return null; fn pciDriverForIdentity(identity: u64) ?[]const u8 {
return switch (d.pci_class) { return switch (identity) {
xhci_pci_class => "usb-xhci-bus", xhci_pci_class => "usb-xhci-bus",
else => null, else => null,
}; };
} }
/// 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 ------------------------------------------------------------- // --- supervision -------------------------------------------------------------
/// How long a protocol driver has to hello after its spawn. /// How long a protocol driver has to hello after its spawn.
@@ -108,6 +117,7 @@ var manager_endpoint: runtime.ipc.Handle = 0;
var test_restart_mode = false; var test_restart_mode = false;
var test_usb_restart_mode = false; var test_usb_restart_mode = false;
var test_usb_killed = false; var test_usb_killed = false;
var test_pci_restart_mode = false;
var test_kill_pid: u32 = 0; var test_kill_pid: u32 = 0;
var test_kill_due_ns: u64 = 0; var test_kill_due_ns: u64 = 0;
@@ -136,6 +146,8 @@ const Child = struct {
parent: u64 = 0, parent: u64 = 0,
bus_address: u64 = 0, bus_address: u64 = 0,
identity: u64 = 0, identity: u64 = 0,
// The kernel device id (registered by the reporter), or protocol.no_device.
device_id: u64 = 0,
reporter: u32 = 0, // the reporting driver instance's process id reporter: u32 = 0, // the reporting driver instance's process id
}; };
@@ -145,18 +157,19 @@ var children: [maximum_children]Child = .{Child{}} ** maximum_children;
/// Record (or refresh) a reported child. Refreshing matters: a restarted bus /// Record (or refresh) a reported child. Refreshing matters: a restarted bus
/// driver re-reports what it rediscovers, and the same (parent, port) must not /// driver re-reports what it rediscovers, and the same (parent, port) must not
/// duplicate. /// duplicate.
fn addChild(parent: u64, bus_address: u64, identity: u64, reporter: u32) bool { fn addChild(parent: u64, bus_address: u64, identity: u64, device_id: u64, reporter: u32) bool {
var free: ?*Child = null; var free: ?*Child = null;
for (&children) |*child| { for (&children) |*child| {
if (child.used and child.parent == parent and child.bus_address == bus_address) { if (child.used and child.parent == parent and child.bus_address == bus_address) {
child.identity = identity; child.identity = identity;
child.device_id = device_id;
child.reporter = reporter; child.reporter = reporter;
return true; return true;
} }
if (!child.used and free == null) free = child; if (!child.used and free == null) free = child;
} }
const slot = free orelse return false; const slot = free orelse return false;
slot.* = .{ .used = true, .parent = parent, .bus_address = bus_address, .identity = identity, .reporter = reporter }; slot.* = .{ .used = true, .parent = parent, .bus_address = bus_address, .identity = identity, .device_id = device_id, .reporter = reporter };
return true; return true;
} }
@@ -315,11 +328,15 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
var matched: usize = 0; var matched: usize = 0;
for (buffer[0..count]) |descriptor| { for (buffer[0..count]) |descriptor| {
if (pciDriverFor(descriptor)) |driver_name| { 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; matched += 1;
addDriver(driver_name, descriptor.id, true); addDriver("pci-bus", descriptor.id, true);
continue; continue;
} }
// PCI functions no longer appear in the boot snapshot (M19.3): the
// pci-bus driver reports them, and onChildAdded matches from reports.
const driver_name = driverFor(descriptor) orelse continue; const driver_name = driverFor(descriptor) orelse continue;
matched += 1; matched += 1;
// Skip a singleton that is already alive (the initial-ramdisk sweep test // Skip a singleton that is already alive (the initial-ramdisk sweep test
@@ -382,22 +399,49 @@ fn onChildAdded(message: []const u8, reply: []u8, sender: u32) usize {
const report = std.mem.bytesToValue(protocol.ChildAdded, message[0..protocol.child_added_size]); const report = std.mem.bytesToValue(protocol.ChildAdded, message[0..protocol.child_added_size]);
var status: i32 = 0; var status: i32 = 0;
if (driverByProcess(sender)) |driver| { if (driverByProcess(sender)) |driver| {
if (!addChild(report.parent, report.bus_address, report.identity, sender)) status = -1; if (!addChild(report.parent, report.bus_address, report.identity, report.device_id, sender)) status = -1;
writeLine("device-manager: child added (device {d} port {d}, identity {d}) by {s}\n", .{ report.parent, report.bus_address, report.identity, driver.name() }); writeLine("device-manager: child added (device {d} port {d}, identity {d}) by {s}\n", .{ report.parent, report.bus_address, report.identity, driver.name() });
if (status == 0) publishEvent(message[0..protocol.child_added_size]); if (status == 0) publishEvent(message[0..protocol.child_added_size]);
// Matching from reports (M19.3): a registered child whose identity
// names a driver gets one, once — re-reports after a bus restart
// dedupe on the registered id, exactly like the registrations do.
if (status == 0 and report.device_id != protocol.no_device) {
if (pciDriverForIdentity(report.identity)) |child_driver| {
if (!driverForDevice(report.device_id)) addDriver(child_driver, report.device_id, true);
}
}
} else { } else {
status = -1; status = -1;
} }
const report_reply = protocol.ReportReply{ .status = status }; const report_reply = protocol.ReportReply{ .status = status };
@memcpy(reply[0..@sizeOf(protocol.ReportReply)], std.mem.asBytes(&report_reply)); @memcpy(reply[0..@sizeOf(protocol.ReportReply)], std.mem.asBytes(&report_reply));
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 = system.clock() + 1_000_000_000;
_ = system.timerOnce(manager_endpoint, 1100);
}
}
}
if (test_usb_restart_mode and !test_usb_killed and childCountOf(sender) >= 2) { if (test_usb_restart_mode and !test_usb_killed and childCountOf(sender) >= 2) {
// Delayed, not immediate: the device-list scenario's subscriber needs a // Only the xHCI reporter is the drill's victim — pci-bus also reports
// window to enumerate and subscribe before the events start. // now, and whichever finishes second must not trigger the kill.
if (driverByProcess(sender)) |driver| {
if (std.mem.eql(u8, driver.name(), "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_usb_killed = true;
test_kill_pid = sender; test_kill_pid = sender;
test_kill_due_ns = system.clock() + 2_000_000_000; test_kill_due_ns = system.clock() + 2_000_000_000;
_ = system.timerOnce(manager_endpoint, 2100); _ = system.timerOnce(manager_endpoint, 2100);
} }
}
}
return @sizeOf(protocol.ReportReply); return @sizeOf(protocol.ReportReply);
} }
@@ -466,6 +510,7 @@ pub fn main(init: runtime.process.Init) void {
if (init.arguments.get(1)) |mode| { if (init.arguments.get(1)) |mode| {
test_restart_mode = std.mem.eql(u8, mode, "test-restart"); test_restart_mode = std.mem.eql(u8, mode, "test-restart");
test_usb_restart_mode = std.mem.eql(u8, mode, "test-usb-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");
} }
runtime.service.run(protocol.message_maximum, .{ runtime.service.run(protocol.message_maximum, .{
.service = .device_manager, .service = .device_manager,
+18 -5
View File
@@ -168,7 +168,7 @@ CASES = [
# Stress the big kernel lock across cores; heavier, so a longer timeout. # Stress the big kernel lock across cores; heavier, so a longer timeout.
{"name": "smp-stress", {"name": "smp-stress",
"smp": 4, "smp": 4,
"timeout": 90, "timeout": 150,
"expect": r"DANOS-TEST-RESULT: PASS", "expect": r"DANOS-TEST-RESULT: PASS",
"fail": r"DANOS-TEST-RESULT: FAIL"}, "fail": r"DANOS-TEST-RESULT: FAIL"},
# Retry: a forced first-wake failure must still bring every core online. # Retry: a forced first-wake failure must still bring every core online.
@@ -263,7 +263,7 @@ CASES = [
# death, and the respawned driver re-reports (docs/device-manager.md). # death, and the respawned driver re-reports (docs/device-manager.md).
{"name": "usb-report", {"name": "usb-report",
"smp": 4, "smp": 4,
"timeout": 90, "timeout": 150,
"qemu_extra": ["-device", "qemu-xhci,id=xhci", "qemu_extra": ["-device", "qemu-xhci,id=xhci",
"-device", "usb-kbd,bus=xhci.0", "-device", "usb-kbd,bus=xhci.0",
"-device", "usb-mouse,bus=xhci.0"], "-device", "usb-mouse,bus=xhci.0"],
@@ -274,16 +274,23 @@ CASES = [
r"device-manager: restarting usb-xhci-bus[\s\S]*" r"device-manager: restarting usb-xhci-bus[\s\S]*"
r"device-manager: child added", r"device-manager: child added",
"fail": r"DANOS-TEST-RESULT: FAIL"}, "fail": r"DANOS-TEST-RESULT: FAIL"},
# M19.1: the ring-3 PCI scan (pci-bus walks the ECAM through its mmio_map
# grant) finds exactly the functions the kernel's own walk recorded.
{"name": "pci-scan",
"smp": 4,
"timeout": 60,
"expect": r"DANOS-TEST-RESULT: PASS",
"fail": r"DANOS-TEST-RESULT: FAIL"},
# M18.3: the application surface — device-list enumerates the tree over IPC, # M18.3: the application surface — device-list enumerates the tree over IPC,
# subscribes (endpoint as capability), and observes the removed/added events # subscribes (endpoint as capability), and observes the removed/added events
# the reporter's test-kill produces (docs/device-manager.md). # the reporter's test-kill produces (docs/device-manager.md).
{"name": "device-list", {"name": "device-list",
"smp": 4, "smp": 4,
"timeout": 90, "timeout": 150,
"qemu_extra": ["-device", "qemu-xhci,id=xhci", "qemu_extra": ["-device", "qemu-xhci,id=xhci",
"-device", "usb-kbd,bus=xhci.0", "-device", "usb-kbd,bus=xhci.0",
"-device", "usb-mouse,bus=xhci.0"], "-device", "usb-mouse,bus=xhci.0"],
"expect": r"device-list: 2 devices[\s\S]*" "expect": r"device-list: \d+ devices[\s\S]*"
r"device-list: subscribed[\s\S]*" r"device-list: subscribed[\s\S]*"
r"device-manager: test mode: killing the reporter[\s\S]*" r"device-manager: test mode: killing the reporter[\s\S]*"
r"device-list: removed \(device[\s\S]*" r"device-list: removed \(device[\s\S]*"
@@ -294,7 +301,7 @@ CASES = [
# each time), and hits the crash-loop cap (docs/device-manager.md). # each time), and hits the crash-loop cap (docs/device-manager.md).
{"name": "driver-restart", {"name": "driver-restart",
"smp": 4, "smp": 4,
"timeout": 90, "timeout": 150,
"qemu_extra": ["-device", "qemu-xhci,id=xhci", "qemu_extra": ["-device", "qemu-xhci,id=xhci",
"-device", "usb-kbd,bus=xhci.0", "-device", "usb-kbd,bus=xhci.0",
"-device", "usb-mouse,bus=xhci.0"], "-device", "usb-mouse,bus=xhci.0"],
@@ -472,6 +479,12 @@ def main():
for case in selected: for case in selected:
print(f" {case['name']:<12} ... ", end="", flush=True) print(f" {case['name']:<12} ... ", end="", flush=True)
ok, detail = run_case(arch, case) ok, detail = run_case(arch, case)
if not ok:
# Keep the evidence: serial.log is otherwise overwritten by the
# next case, and an intermittent failure's log is unrecoverable.
source = os.path.join(WORK, "serial.log")
if os.path.exists(source):
shutil.copy(source, os.path.join(WORK, f"{case['name']}-failed-serial.log"))
print(("PASS" if ok else "FAIL") + f" ({detail})") print(("PASS" if ok else "FAIL") + f" ({detail})")
if not ok: if not ok:
failures += 1 failures += 1