The AML interpreter runs in ring 3: the acpi service parses (M20.1)

The AML module becomes a build module compiled into both the kernel (for
the \_S5 sleep state it still needs) and the new acpi service — one
source, two builds, no fork. The kernel publishes a single acpi-tables
node: the DSDT/SSDT blobs as memory resources, a broad io_port grant (the
honest trust boundary — firmware AML names whatever ports it chose, known
only after parsing), and the SCI for the M21 event track. The acpi
service claims the node, maps each blob through the ordinary mmio grant
(which preserves the sub-page offset onto the bytecode), and runs the
same parser the kernel does. It self-verifies its namespace Device count
against the kernel's — 34 = 34 — deterministically via an argv the
acpi-parse test passes, so no racing the shared serial buffer. Parse-only
touches no hardware; OperationRegion evaluation waits for _CRS/_STA in
M20.2. The manager spawns 'discovery' (the neutral ramdisk name) at
startup. Suite 56/56.
This commit is contained in:
Daniel Samson
2026-07-13 03:07:11 +01:00
parent d8dd62c639
commit a299363b59
11 changed files with 229 additions and 27 deletions
+8
View File
@@ -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.
+8 -7
View File
@@ -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
+41
View File
@@ -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;
+14
View File
@@ -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 {
+5
View File
@@ -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,
};
+7
View File
@@ -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;
}
+45
View File
@@ -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,
+86 -19
View File
@@ -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;
@@ -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,
+2 -1
View File
@@ -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
+7
View File
@@ -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",