diff --git a/build.zig b/build.zig index 3dbc40b..675893d 100644 --- a/build.zig +++ b/build.zig @@ -131,6 +131,13 @@ pub fn build(b: *std.Build) void { }); // ACPI/PnP hardware-ID (_HID) names — the flat analog of pci-class for acpi_device // nodes. Also shared reference data. + // The AML interpreter, a build module so the ring-3 acpi service can run the + // same parser the kernel does (docs/m19-m20-plan.md decision 1). Pure Zig, + // no kernel imports — one source, two builds. + const aml_module = b.addModule("aml", .{ + .root_source_file = b.path("system/devices/aml/aml.zig"), + }); + const acpi_ids_module = b.addModule("acpi-ids", .{ .root_source_file = b.path("system/devices/acpi-ids.zig"), }); @@ -358,6 +365,7 @@ pub fn build(b: *std.Build) void { .fdt => "system/services/fdt/fdt.zig", }; const discovery_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "discovery", discovery_source); + if (discovery == .acpi) discovery_exe.root_module.addImport("aml", aml_module); const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-manager", "system/services/device-manager/device-manager.zig"); // The input service and its exercisers: the fan-out server, a hardware-free synthetic // source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md. diff --git a/docs/m19-m20-plan.md b/docs/m19-m20-plan.md index b98979b..1bc05aa 100644 --- a/docs/m19-m20-plan.md +++ b/docs/m19-m20-plan.md @@ -106,13 +106,14 @@ branch is green; keep branches; push everything. skips size-0 BARs. discovery.md updated; suite 55/55 (driver-restart hammered 6×). - [x] **merge** `feat/pci-bus` → main, push (merged 2026-07-13). -- [ ] **M20.1** — acpi service, parse only (fills the existing placeholder at - system/services/acpi/acpi.zig): kernel publishes `acpi-tables` - (decision 5) — memory over the table blobs, the broad io_port grant, and - **the SCI as an irq resource** (from the FADT; unused until M21 but free - to record now). The service claims it, maps the blobs, runs the shared - AML module in ring 3, logs the namespace device count and `_HID`s. - Scenario `acpi-parse`: user-space count equals the kernel walk's count. +- [x] **M20.1** — acpi service, parse only: the AML interpreter is now a build + module compiled into both kernel and service; the kernel publishes the + `acpi-tables` node (AML blobs as memory resources, the broad io_port grant, + the SCI); the service claims it, maps the blobs, runs the shared parser in + ring 3, and self-verifies its Device count against the kernel's (34 = 34, + deterministic via argv, no log-scraping); the manager spawns `discovery` + at startup. Parse-only touches no hardware. Suite 56/56. + - [ ] **M20.2** — register + report: namespace devices with `_HID` + `_CRS` resources registered under `acpi-tables` (its io_port + the memory-map holes give containment), reported to the manager. Spawn-from-reports for diff --git a/system/devices/acpi.zig b/system/devices/acpi.zig index 8e008db..e7f78d4 100644 --- a/system/devices/acpi.zig +++ b/system/devices/acpi.zig @@ -41,6 +41,9 @@ pub const RegisterAccess = struct { /// Everything the power subsystem needs, extracted from the FADT and the AML /// sleep packages during discovery. Populated by `discover`, read by `power`. pub const PowerInformation = struct { + /// The System Control Interrupt's GSI (FADT SCI_INT) — the line ACPI events + /// (power button, GPEs) arrive on. Published to the acpi service for M21. + sci_interrupt: u16 = 0, /// The SMM command port and the value that switches the platform into ACPI mode. smi_cmd: u16 = 0, acpi_enable: u8 = 0, @@ -408,6 +411,42 @@ pub fn discover(rsdp_physical: u64, memory_regions: []const boot_handoff.MemoryR // AML parse failed (e.g. out of memory); power stays best-effort with // whatever the FADT alone provided. } + + // Publish the acpi-tables node (docs/m19-m20-plan.md M20): the AML blobs as + // memory resources for the acpi service to map and parse in ring 3, a broad + // io_port grant for the OperationRegion access its interpreter needs, and + // the SCI for the events track (M21). Exactly one node, one trusted + // claimant. Kept even when the kernel-side device building (above) retires + // in M20.3 — the kernel still owns the *static* tables and \_S5. + publishAcpiTablesNode(device_tree) catch {}; +} + +/// Build the acpi-tables node (see the call site in discover). Best-effort: a +/// failure here leaves the kernel-seeded tree working, only the ring-3 service +/// finds nothing to claim. +fn publishAcpiTablesNode(device_tree: *DeviceTree) !void { + const node = try device_tree.addChild(device_tree.root, .acpi_tables, "acpi-tables"); + // One memory resource per AML block — page-aligned base down, length padded + // up to cover the bytecode, so mmio_map hands the service a pointer into it. + var i: usize = 0; + while (i < aml_block_count and i < device_model.maximum_resources - 2) : (i += 1) { + // mmio_map preserves the sub-page offset, so the service maps this and + // gets a pointer straight to the bytecode. + _ = node.addResource(.memory, aml_block_physical[i], aml_block_len[i]); + } + // The broad I/O grant: OperationRegions name whatever ports the firmware + // chose (EC, PM1, GPE, SMBus); which ports cannot be known before the AML + // that names them is parsed, so the grant is the whole space — the honest + // trust boundary of docs/m19-m20-plan.md decision 5. + _ = node.addResource(.io_port, 0, 1 << 16); + // The SCI (M21 events); harmless to record now. + if (power_information.sci_interrupt != 0) _ = node.addResource(.irq, power_information.sci_interrupt, 1); +} + +/// The number of Device objects in the namespace built during discovery, or 0. +pub fn amlDeviceCount() usize { + if (namespace) |*ns| return aml.deviceCount(ns); + return 0; } /// Walk the RSDT (Entry = u32) or XSDT (Entry = u64): validate it, then dispatch @@ -670,6 +709,7 @@ const fadt_pm1a_cnt_blk = 64; // u32 (I/O port) const fadt_pm1b_cnt_blk = 68; // u32 (I/O port) const fadt_pm_tmr_blk = 76; // u32 (I/O port) — the PM timer counter const fadt_pm1_cnt_len = 89; // u8 (bytes) +const fadt_sci_int = 46; // u16 (the SCI's GSI) const fadt_flags = 112; // u32 const fadt_reset_register = 116; // GAS (12 bytes) const fadt_reset_value = 128; // u8 @@ -687,6 +727,7 @@ fn parseFadt(header: *const SystemDescriptorTableHeader) void { const len: usize = header.length; const pi = &power_information; + pi.sci_interrupt = @truncate(fadt(u16, base, len, fadt_sci_int) orelse 0); pi.smi_cmd = @truncate(fadt(u32, base, len, fadt_smi_cmd) orelse 0); pi.acpi_enable = fadt(u8, base, len, fadt_acpi_enable) orelse 0; pi.acpi_disable = fadt(u8, base, len, fadt_acpi_disable) orelse 0; diff --git a/system/devices/aml/aml.zig b/system/devices/aml/aml.zig index 8a37648..cbe6b83 100644 --- a/system/devices/aml/aml.zig +++ b/system/devices/aml/aml.zig @@ -50,6 +50,20 @@ pub fn parse(allocator: std.mem.Allocator, blocks: []const []const u8) !ParseRes return .{ .namespace = namespace, .consumed = consumed, .total = total }; } +/// Count the Device objects in a parsed namespace — what the acpi service +/// (docs/m19-m20-plan.md M20) reports, and what the kernel's own parse counts +/// so the two can be checked equal across the ring-3 move. +pub fn deviceCount(namespace: *const Namespace) usize { + return countKind(namespace.root, .device); +} + +fn countKind(node: *const Node, kind: NodeKind) usize { + var n: usize = if (node.kind == kind) 1 else 0; + var c = node.first_child; + while (c) |child| : (c = child.next_sibling) n += countKind(child, kind); + return n; +} + /// Look up the `\_S{state}` sleep package in a parsed namespace and return its /// first two integer elements (SLP_TYP for PM1a / PM1b), or null if absent. pub fn sleepState(namespace: *Namespace, state: u8) ?SleepType { diff --git a/system/devices/device-abi.zig b/system/devices/device-abi.zig index 9b04bf4..33cf15e 100644 --- a/system/devices/device-abi.zig +++ b/system/devices/device-abi.zig @@ -28,6 +28,11 @@ pub const DeviceClass = enum(u32) { /// A device named in the ACPI namespace (from the DSDT/SSDT), carrying a /// hardware ID (`_HID`) and, where static, current resource settings (`_CRS`). acpi_device, + /// The ACPI tables themselves, published as one node for the user-space acpi + /// service (docs/m19-m20-plan.md M20): memory resources over the AML blobs, + /// a broad io_port grant for OperationRegion access, and the SCI interrupt. + /// The one node whose claimant is trusted to run firmware bytecode. + acpi_tables, unknown, }; diff --git a/system/devices/platform.zig b/system/devices/platform.zig index 98ce62f..200e6d4 100644 --- a/system/devices/platform.zig +++ b/system/devices/platform.zig @@ -40,6 +40,13 @@ pub fn platformInformation() PlatformInformation { } /// AML parse integrity/diagnostics (namespace node count, bytes consumed). +/// The number of Device objects in the kernel's own AML namespace, or 0 if the +/// parse produced none — the `acpi-parse` test compares the ring-3 service's +/// count against this. +pub fn amlDeviceCount() usize { + return acpi.amlDeviceCount(); +} + pub fn amlStats() AmlStats { return acpi.aml_stats; } diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index 47a6596..fd4a764 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -146,6 +146,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void { deviceListTest(boot_information); } else if (eql(case, "pci-scan")) { pciScanTest(boot_information); + } else if (eql(case, "acpi-parse")) { + acpiParseTest(boot_information); } else if (eql(case, "initial-ramdisk")) { initialRamdiskTest(boot_information); } else if (eql(case, "vfs")) { @@ -1912,6 +1914,49 @@ fn pciScanTest(boot_information: *const BootInformation) void { result(); } +/// M20.1: the ring-3 AML parse agrees with the kernel's. The manager spawns +/// the discovery service (the acpi build variant); it claims the acpi-tables +/// node, maps the blobs, parses them, and logs its Device count — which must +/// equal what the kernel's own parse produced (the equivalence that licenses +/// retiring the kernel's device build in M20.3). +fn acpiParseTest(boot_information: *const BootInformation) void { + log("DANOS-TEST-BEGIN: acpi-parse\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; + }; + + // The kernel's own count, from the namespace it already built for \_S5. + const kernel_devices = platform.amlDeviceCount(); + check("the kernel namespace has devices to compare against", kernel_devices >= 1); + + // Spawn the discovery service directly with that count as argv: it parses + // the same blobs in ring 3 and self-verifies, printing "acpi-parse: ok" iff + // the counts match. The harness's expect regex is that marker — deterministic, + // no racing the shared serial buffer. + process.setInitialRamdisk(image); + var count_text: [16]u8 = undefined; + const count_arg = std.fmt.bufPrint(&count_text, "{d}", .{kernel_devices}) catch "0"; + var spawned = false; + var i: u32 = 0; + while (i < rd.count) : (i += 1) { + const item = rd.entry(i) orelse continue; + if (!eql(item.name, "discovery")) continue; + _ = process.spawnProcessSupervised(item.blob, 4, &.{ "discovery", count_arg }, scheduler.currentId(), null) catch 0; + spawned = true; + break; + } + check("discovery service spawned", spawned); + result(); +} + /// 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 /// children supervised, sees them in process_enumerate, kills them (one blocked, diff --git a/system/services/acpi/acpi.zig b/system/services/acpi/acpi.zig index 740e185..67c9fc9 100644 --- a/system/services/acpi/acpi.zig +++ b/system/services/acpi/acpi.zig @@ -1,27 +1,94 @@ //! /system/services/acpi — the ACPI discovery service: the x86 firmware -//! interpreter, moved out of ring 0 (docs/m19-m20-plan.md, M20). **Placeholder: -//! not implemented until M20.1** — it exists so the build's `-Ddiscovery` -//! option has both of its values and the ramdisk's neutral `discovery` slot is -//! wired before the implementation lands. +//! interpreter, moved out of ring 0 (docs/m19-m20-plan.md, M20). Claims the +//! `acpi-tables` node the kernel publishes (the AML blobs, the broad io_port +//! grant, the SCI), and runs the **shared AML module** in ring 3 — the same +//! parser the kernel uses for `\_S5`, now the sole builder of the device +//! namespace. //! -//! What it becomes (the plan's decisions 5 and 7): claim the `acpi-tables` -//! node the kernel publishes (table blobs + the broad io_port grant + the SCI), -//! map the tables, and run the **shared AML module** in ring 3 behind a `Hal` -//! backed by `mmio_map` and `io_read`/`io_write` — the interpreter cannot tell -//! it moved. Then the bus-driver shape: `device_register` the namespace -//! devices (`_HID`, `_CRS` resources, containment against the node's -//! apertures), report each to the device manager, stay resident under its -//! supervision. M21 grows the event side on the same claim: the SCI, PM1 fixed -//! events, GPEs, Notify — published through the domain-named power protocol, -//! never an "ACPI events" protocol. +//! M20.1 (this increment): claim the node, map each AML blob through the +//! ordinary mmio grant, parse them into a namespace, and log the Device count — +//! which the `acpi-parse` scenario checks equals the kernel's own parse. +//! Parsing touches no hardware (the io_port grant and the interpreter's +//! OperationRegion evaluation come in with `_CRS`/`_STA` at M20.2). Registering +//! and reporting the namespace devices, and retiring the kernel's device build, +//! follow in M20.2 and M20.3. +const std = @import("std"); const runtime = @import("runtime"); +const aml = @import("aml"); +const device = runtime.device; -pub fn main() void { - // Not implemented: exit cleanly and silently (a bare spawn by the - // initial-ramdisk sweep must not derange other tests' markers). The - // supervisor reads a clean exit as "meant to stop" — correct for a - // placeholder. +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); +} + +/// Find the acpi-tables node the kernel published, or null. +fn findTablesNode(buffer: []device.DeviceDescriptor) ?device.DeviceDescriptor { + const total = device.enumerate(buffer); + for (buffer[0..@min(total, buffer.len)]) |d| { + if (d.class == @intFromEnum(device.DeviceClass.acpi_tables)) return d; + } + return null; +} + +pub fn main(init: runtime.process.Init) void { + // When the acpi-parse scenario spawns this directly, argv[1] is the kernel's + // own device count to self-verify against — deterministic, no log-scraping. + const expected: ?usize = if (init.arguments.get(1)) |a| (std.fmt.parseInt(usize, a, 10) catch null) else null; + + const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch { + _ = runtime.system.write("acpi: out of memory\n"); + return; + }; + const node = findTablesNode(buffer) orelse { + _ = runtime.system.write("acpi: no acpi-tables node to claim\n"); + return; + }; + if (!device.claim(node.id)) { + _ = runtime.system.write("acpi: unable to claim acpi-tables\n"); + return; + } + + // Map each memory resource (an AML blob) and collect the byte slices. The + // grant preserves each blob's sub-page offset, so the mapped pointer lands + // straight on the bytecode. + var blocks: [8][]const u8 = undefined; + var block_count: usize = 0; + for (node.resources[0..@intCast(node.resource_count)], 0..) |resource, index| { + if (resource.kind != @intFromEnum(device.ResourceKind.memory)) continue; + const base = device.mmioMap(node.id, index) orelse { + writeLine("acpi: mmio_map failed for blob {d}\n", .{index}); + continue; + }; + const pointer: [*]const u8 = @ptrFromInt(base); + blocks[block_count] = pointer[0..@intCast(resource.len)]; + block_count += 1; + if (block_count == blocks.len) break; + } + if (block_count == 0) { + _ = runtime.system.write("acpi: no AML blobs on the node\n"); + return; + } + + const result = aml.parse(runtime.allocator(), blocks[0..block_count]) catch { + _ = runtime.system.write("acpi: AML parse failed\n"); + return; + }; + var namespace = result.namespace; + const devices = aml.deviceCount(&namespace); + writeLine("acpi: parsed {d} AML blob(s), {d} namespace devices\n", .{ block_count, devices }); + if (expected) |want| { + if (devices == want) { + _ = runtime.system.write("acpi-parse: ok\n"); + } else { + writeLine("acpi-parse: mismatch (ring-3 {d} vs kernel {d})\n", .{ devices, want }); + } + } + + // Registration and reports arrive in M20.2; stay resident so the claim + // holds and the service is here to grow into the supervised discoverer. + while (true) runtime.system.sleep(1000); } pub const panic = runtime.panic; diff --git a/system/services/device-manager/device-manager.zig b/system/services/device-manager/device-manager.zig index ed2b4cb..39cfc2b 100644 --- a/system/services/device-manager/device-manager.zig +++ b/system/services/device-manager/device-manager.zig @@ -347,6 +347,12 @@ fn initialise(endpoint: runtime.ipc.Handle) bool { } } + // The discovery service (docs/m19-m20-plan.md M20): one per firmware, packed + // under the neutral name "discovery", spawned once at startup. It finds and + // claims the acpi-tables (or devicetree-blob) node itself. Not a per-device + // match — it is the discoverer, not a driver bound to one device. + addDriver("discovery", protocol.no_device, false); + if (test_restart_mode) { // The driver-restart scenario's fixture: claims device 0 (the tree // root, otherwise unclaimed), hellos, then faults — driving backoff, diff --git a/system/services/fdt/fdt.zig b/system/services/fdt/fdt.zig index f3e2d83..cba17e9 100644 --- a/system/services/fdt/fdt.zig +++ b/system/services/fdt/fdt.zig @@ -21,7 +21,8 @@ const runtime = @import("runtime"); -pub fn main() void { +pub fn main(init: runtime.process.Init) void { + _ = init; // Not implemented: exit cleanly and silently (a bare spawn by the // initial-ramdisk sweep must not derange other tests' markers). The // supervisor reads a clean exit as "meant to stop" — correct for a diff --git a/test/qemu_test.py b/test/qemu_test.py index 17771d7..3cf74a1 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -274,6 +274,13 @@ CASES = [ r"device-manager: restarting usb-xhci-bus[\s\S]*" r"device-manager: child added", "fail": r"DANOS-TEST-RESULT: FAIL"}, + # M20.1: the ring-3 AML parse (the acpi service maps the blobs and parses + # them) finds exactly the Device count the kernel's own parse produced. + {"name": "acpi-parse", + "smp": 4, + "timeout": 60, + "expect": r"acpi-parse: ok", + "fail": r"acpi-parse: mismatch|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",