From a2a05d0b3d6240da53bbf13ab0e54ca376dcede0 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Mon, 13 Jul 2026 01:59:32 +0100 Subject: [PATCH] Discovery-migration prerequisites (M19.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/m17-m18-plan.md | 4 ++ docs/m19-m20-plan.md | 8 ++- system/devices/acpi.zig | 66 ++++++++++++++++++- system/devices/platform.zig | 3 +- system/kernel/devices-broker.zig | 20 ++++++ system/kernel/tests.zig | 57 ++++++++++++++++ .../device-manager-protocol.zig | 7 +- .../device-manager/device-manager.zig | 9 ++- 8 files changed, 165 insertions(+), 9 deletions(-) diff --git a/docs/m17-m18-plan.md b/docs/m17-m18-plan.md index 0348bf8..8518c9f 100644 --- a/docs/m17-m18-plan.md +++ b/docs/m17-m18-plan.md @@ -1,5 +1,9 @@ # 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) (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, diff --git a/docs/m19-m20-plan.md b/docs/m19-m20-plan.md index d1b2b66..47fa5a5 100644 --- a/docs/m19-m20-plan.md +++ b/docs/m19-m20-plan.md @@ -79,9 +79,11 @@ branch is green; keep branches; push everything. ## Status -- [ ] **M19.0** — prerequisites on `feat/pci-bus`: bridge MMIO apertures from - the memory-map holes; `device_register` idempotence (+ kernel unit - checks); `ChildAdded.device_id`; archive note on m17-m18-plan.md. +- [x] **M19.0** — prerequisites (bridge apertures from the memory map's + *gaps* — the single-hole rule died on OVMF's flash at the top of 4 GiB, + 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 ECAM window, walk bus/device/function headers, log what it finds. Scenario `pci-scan`: the kernel test compares the driver's reported count diff --git a/system/devices/acpi.zig b/system/devices/acpi.zig index 2f68c15..0e37c47 100644 --- a/system/devices/acpi.zig +++ b/system/devices/acpi.zig @@ -384,8 +384,9 @@ const PciHeader = extern struct { /// 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 /// 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; + boot_memory_regions = memory_regions; // Start clean so a re-run doesn't accumulate stale state. 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. _ = bridge.addResource(.memory, alloc.base_address, bus_count << 20); _ = bridge.addResource(.bus_range, alloc.start_bus, bus_count); + addBridgeApertures(bridge); 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 /// 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. diff --git a/system/devices/platform.zig b/system/devices/platform.zig index ee78cff..98ce62f 100644 --- a/system/devices/platform.zig +++ b/system/devices/platform.zig @@ -72,7 +72,8 @@ pub fn discover( var device_tree = try DeviceTree.init(allocator); 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 { // 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 diff --git a/system/kernel/devices-broker.zig b/system/kernel/devices-broker.zig index 2147188..8460c4e 100644 --- a/system/kernel/devices-broker.zig +++ b/system/kernel/devices-broker.zig @@ -180,6 +180,26 @@ pub fn register(parent_id: u64, owner: u32, descriptor: *const device_abi.Device 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); d.id = count; d.parent = parent_id; diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index bf6237b..2477f26 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -283,6 +283,34 @@ fn discoveryTest() void { 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); + // 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(); } @@ -2085,6 +2113,35 @@ fn hpetGsi() ?u32 { /// land in the device table with the containment invariant intact. fn busTest(boot_information: *const BootInformation) void { 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) { check("bootloader handed over an initial_ramdisk", false); result(); diff --git a/system/services/device-manager/device-manager-protocol.zig b/system/services/device-manager/device-manager-protocol.zig index 2a49db4..bf494a3 100644 --- a/system/services/device-manager/device-manager-protocol.zig +++ b/system/services/device-manager/device-manager-protocol.zig @@ -73,8 +73,13 @@ pub const ChildAdded = extern struct { parent: u64, /// Where on the bus (for USB: the root port number, 1-based). 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, + /// 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); diff --git a/system/services/device-manager/device-manager.zig b/system/services/device-manager/device-manager.zig index b59f2a1..262f026 100644 --- a/system/services/device-manager/device-manager.zig +++ b/system/services/device-manager/device-manager.zig @@ -136,6 +136,8 @@ const Child = struct { parent: u64 = 0, bus_address: 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 }; @@ -145,18 +147,19 @@ var children: [maximum_children]Child = .{Child{}} ** maximum_children; /// Record (or refresh) a reported child. Refreshing matters: a restarted bus /// driver re-reports what it rediscovers, and the same (parent, port) must not /// duplicate. -fn addChild(parent: u64, bus_address: u64, identity: u64, reporter: u32) bool { +fn addChild(parent: u64, bus_address: u64, identity: u64, device_id: u64, reporter: u32) bool { var free: ?*Child = null; for (&children) |*child| { if (child.used and child.parent == parent and child.bus_address == bus_address) { child.identity = identity; + child.device_id = device_id; child.reporter = reporter; return true; } if (!child.used and free == null) free = child; } const slot = free orelse return false; - slot.* = .{ .used = true, .parent = parent, .bus_address = bus_address, .identity = identity, .reporter = reporter }; + slot.* = .{ .used = true, .parent = parent, .bus_address = bus_address, .identity = identity, .device_id = device_id, .reporter = reporter }; 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]); var status: i32 = 0; 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() }); if (status == 0) publishEvent(message[0..protocol.child_added_size]); } else {