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.
This commit is contained in:
Daniel Samson
2026-07-13 01:59:32 +01:00
parent 75d62660b0
commit a2a05d0b3d
8 changed files with 165 additions and 9 deletions
+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,
+5 -3
View File
@@ -79,9 +79,11 @@ 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
`device_register` proven in `bus`; `ChildAdded.device_id`;
m17-m18-plan.md archived; suite 54/54).
- [ ] **M19.1** — pci-bus driver, scan only: claim the host bridge, map the - [ ] **M19.1** — pci-bus driver, scan only: claim the host bridge, map the
ECAM window, walk bus/device/function headers, log what it finds. ECAM window, walk bus/device/function headers, log what it finds.
Scenario `pci-scan`: the kernel test compares the driver's reported count Scenario `pci-scan`: the kernel test compares the driver's reported count
+65 -1
View File
@@ -384,8 +384,9 @@ const PciHeader = extern struct {
/// 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 = .{};
@@ -551,11 +552,74 @@ 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);
try enumeratePci(device_tree, bridge, hal, alloc.*); try enumeratePci(device_tree, bridge, hal, alloc.*);
} }
} }
/// The boot memory map, stored at discover() entry for the aperture derivation
/// below (and, in M20, for the acpi-tables node's containment windows).
var boot_memory_regions: []const boot_handoff.MemoryRegion = &.{};
/// The bridge's MMIO apertures, derived from the boot memory map's holes
/// (docs/m19-m20-plan.md decision 2): registered PCI functions carry BAR
/// resources, and `device_register` containment demands the bridge own windows
/// that cover them. Everything the firmware described is "not hole"; the low
/// aperture runs from the end of the described space below 4 GiB up to the
/// 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
/// matter what later moved to user space.
fn addBridgeApertures(bridge: *device_model.Device) void {
// Below 4 GiB the described regions are sparse (RAM low, firmware flash
// and tables high), so the holes are the *gaps between* them — a single
// "after the last region" rule dies on OVMF's flash at the very top.
// Sort-merge the described ranges, then keep the three largest gaps
// (resource slots are bounded at 8 per device; ECAM + bus range + 3 + the
// high aperture fits). Above 4 GiB one aperture runs from the end of the
// described space to the 46-bit line.
const Range = struct { base: u64, end: u64 };
var below: [64]Range = undefined;
var below_count: usize = 0;
var high_end: u64 = 1 << 32;
for (boot_memory_regions) |region| {
const end = region.base + region.pages * 4096;
if (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);
}
/// Brute-force scan the ECAM window's bus range for present PCI functions. No /// Brute-force scan the ECAM window's bus range for present PCI functions. No
/// bridge recursion yet: on the ECAM path the host bridge decodes every bus in /// bridge recursion yet: on the ECAM path the host bridge decodes every bus in
/// the window, so scanning the declared range finds everything QEMU exposes. /// the window, so scanning the declared range finds everything QEMU exposes.
+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
+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;
+57
View File
@@ -283,6 +283,34 @@ fn discoveryTest() void {
check("PCI functions were enumerated (MCFG/ECAM)", pci_functions >= 1); check("PCI functions were enumerated (MCFG/ECAM)", pci_functions >= 1);
check("each PCI function exposes its ECAM config space as resource 0", pci_config_ok); check("each PCI function exposes its ECAM config space as resource 0", pci_config_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| {
if (d.class != @intFromEnum(device_abi.DeviceClass.pci_device)) continue;
if (d.parent >= n) {
bars_contained = false;
continue;
}
const bridge = buffer[@intCast(d.parent)];
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();
} }
@@ -2085,6 +2113,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);
@@ -136,6 +136,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 +147,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;
} }
@@ -382,7 +385,7 @@ 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]);
} else { } else {