//! ACPI discovery backend. //! //! Walks the ACPI tables the firmware left in memory (starting from the RSDP the //! bootloader handed us) and translates the static tables into the generic //! `device` model, so the kernel enumerates hardware without knowing ACPI is the //! source. This is deliberately the *static-table* path, and **only** that: MADT //! (CPUs / interrupt controllers), MCFG (PCIe ECAM -> PCI enumeration), HPET //! (timer), and FADT (power register map). The DSDT/SSDT bytecode is *not* //! interpreted here — the kernel collects the blobs and publishes them on the //! acpi-tables node for the ring-3 acpi service to parse (device enumeration and //! soft-off). Keeping the ~0.5 MB AML interpretation out of kernel init keeps it //! off the single-core critical path (nothing else runs alongside it there). //! //! ACPI tables live in `.acpi_tables` / `.acpi_nvs` memory, which the kernel //! identity-maps, so table addresses are dereferenced directly. PCIe ECAM is MMIO //! and is *not* mapped up front, so configuration-space pages are mapped on demand via //! the `Hal.mapMmio` callback the caller supplies (the architecture VMM's map primitive). const std = @import("std"); const boot_handoff = @import("boot-handoff"); const abi = @import("abi"); const parameters = @import("parameters"); const device_model = @import("device-model.zig"); const DeviceTree = device_model.DeviceTree; const Hal = device_model.Hal; /// A hardware register located either in MMIO or I/O-port space, as ACPI's /// Generic Address Structure describes. `address == 0` means "not present". pub const RegisterAccess = struct { /// true = system memory (MMIO), false = system I/O port space. mmio: bool = false, address: u64 = 0, /// Access width in bytes. width: u8 = 0, pub fn present(self: RegisterAccess) bool { return self.address != 0; } }; /// The power register map, extracted from the FADT during discovery. Populated by /// `discover`, read by `power` (kernel reboot). The **sleep-state (`_Sx`) values /// live in AML**, which the kernel no longer parses — soft-off (S5) is owned by the /// ring-3 acpi service (it re-parses the blobs on the published acpi-tables node and /// writes the PM1 control register itself). So this holds only the FADT scalars. 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, acpi_disable: u8 = 0, /// PM1 control registers — writing SLP_TYP|SLP_EN here enters a sleep state. pm1a_cnt: RegisterAccess = .{}, pm1b_cnt: RegisterAccess = .{}, /// The FADT reset register and the value to write to it. reset: RegisterAccess = .{}, reset_value: u8 = 0, reset_supported: bool = false, }; /// Filled in by `discover`; `reboot` (below) reads it to restart the machine. pub var power_information: PowerInformation = .{}; /// A legacy ISA IRQ remapped to a different global system interrupt (GSI), from a /// MADT Interrupt Source Override. `flags` are the MPS INTI polarity/trigger bits. pub const IsoEntry = struct { source: u8, gsi: u32, flags: u16, }; /// Firmware facts the architecture layer needs to avoid legacy assumptions (so danos boots /// on legacy-free UEFI Class 3 machines). MMIO device *addresses* (HPET, IOAPIC) /// come from the device tree instead; this holds the scalar facts that have no /// natural device node. pub const PlatformInformation = struct { /// Whether the legacy 8259 PIC is present (MADT flags bit 0, PCAT_COMPAT). When /// false, the PIC must not be programmed (it may not exist). pic_present: bool = false, /// Local APIC MMIO base (MADT, honouring a type-5 address override). lapic_base: u64 = 0xFEE00000, /// The ACPI power-management timer — a fixed 3.579545 MHz counter usable as a /// calibration reference when no HPET is present. pm_timer: RegisterAccess = .{}, /// true = 32-bit PM timer counter, false = 24-bit (FADT flag TMR_VALUE_EXT). pm_timer_32bit: bool = false, /// The console UART the firmware points at (SPCR), if any — MMIO or I/O port. spcr_uart: ?RegisterAccess = null, /// SPCR interface type (0/1 = 16550/16450, …). spcr_kind: u8 = 0, /// ISA-IRQ-to-GSI remappings from the MADT (for future IOAPIC routing). overrides: [16]IsoEntry = undefined, override_count: usize = 0, /// Whether an IOMMU (VT-d DMA-remapping unit) was found in the ACPI DMAR table. /// When false, `device_claim` on a DMA-capable device is equivalent to granting /// ring 0 — a device can DMA to any physical address (docs/driver-model.md M16), /// and the kernel says so at every boot (the fail-open platform log line). When /// true, the IOMMU core builds per-device translation domains from this record. iommu_present: bool = false, /// true when the present unit is AMD-Vi (from IVRS) rather than Intel VT-d (DMAR). /// The two are mutually exclusive on real hardware; the IOMMU core picks the backend. iommu_is_amd: bool = false, /// MMIO base of the selected DMA-remapping hardware unit — the VT-d DRHD with /// INCLUDE_PCI_ALL (the catch-all unit; falls back to the first), or the AMD-Vi /// IOMMU's control-register base from the first IVHD. iommu_base: u64 = 0, /// Whether the selected unit carries INCLUDE_PCI_ALL. False means every unit is /// device-scoped (unusual) — the core still enables on the selected unit but /// devices outside its scope remain untranslated. iommu_include_all: bool = false, /// DRHD units in the DMAR beyond the selected one. Devices scoped to those units /// (typically the integrated GPU) are NOT translated by v1 — the boot log warns. iommu_extra_units: u8 = 0, /// Reserved-memory regions (DMAR RMRRs): firmware-owned buffers a named device /// keeps DMAing into across the OS handoff (classically the xHC keyboard-emulation /// buffer). These must be identity-mapped in the device's domain BEFORE translation /// enables, or platform firmware breaks. Only single-path endpoint scopes are /// recorded; anything fancier is skipped with a loud log at parse time. rmrr: [maximum_rmrr]RmrrRegion = undefined, rmrr_count: usize = 0, /// RMRR device scopes the parser could not record (multi-hop paths, sub-hierarchy /// types, or table overflow). Non-zero means a device keeps an unmapped firmware /// buffer — the kernel boot log warns loudly (the platform module itself is /// log-free by design; it records, the kernel reports). rmrr_skipped: u8 = 0, }; pub const maximum_rmrr = 8; /// One recorded RMRR: the device (requester id) and the inclusive physical range it /// must always be allowed to reach. pub const RmrrRegion = struct { bdf: u16, base: u64, limit: u64, }; /// Filled in by `discover`; the architecture layer reads it during bring-up. pub var platform_information: PlatformInformation = .{}; /// One usable logical processor, from a MADT type-0 (Local APIC) record. The /// `apic_id` is the Local APIC ID that SMP bring-up targets to wake this core /// (INIT–SIPI–SIPI); `processor_id` is the ACPI namespace handle. Only processors /// the firmware marks *enabled* are recorded — a disabled one can't be started. pub const Cpu = struct { processor_id: u8, apic_id: u8, /// MADT flags bit 1: usable but firmware-started offline (hot-plug / deferred /// bring-up), as opposed to already available. Informational for now. online_capable: bool, }; /// The set of usable logical processors the MADT listed — the hardware's degree of /// parallelism. Includes the bootstrap processor danos already runs on; the rest /// are the application processors SMP bring-up would start (see docs/smp.md). pub const CpuInformation = struct { /// A static pool sized well above any danos target (a desktop, two 4-core Pis). /// If the MADT ever lists more, the surplus is dropped and counted in `dropped` /// so the truncation is never silent. cpus: [maximum_cpus]Cpu = undefined, count: usize = 0, dropped: usize = 0, }; const maximum_cpus = parameters.maximum_cpus; /// Filled in by `discover` (from the MADT); SMP bring-up reads it to wake the APs. pub var cpu_information: CpuInformation = .{}; /// Physical address of the DSDT the FADT points at, or 0. pub var dsdt_physical: u64 = 0; /// The FADT itself (physical + length), published on the acpi-tables node so /// the ring-3 acpi service can read the PM1 event and GPE blocks it needs for /// the event side (docs/acpi.md — ACPI events). Distinguished from the AML /// blob resources by its intact "FACP" header — the blobs are header-stripped. var fadt_physical: u64 = 0; var fadt_length: u64 = 0; // AML blocks (DSDT + any SSDTs) collected during the table walk, as physical // address + length of each table's post-header bytecode. The kernel does not // interpret them — it publishes them on the acpi-tables node for the ring-3 acpi // service to parse (device enumeration + soft-off). See publishAcpiTablesNode. var aml_block_physical: [32]u64 = undefined; var aml_block_len: [32]usize = undefined; var aml_block_count: usize = 0; fn addAmlBlock(sdt_physical: u64) void { if (aml_block_count >= aml_block_physical.len or sdt_physical == 0) return; const h: *const SystemDescriptorTableHeader = @ptrFromInt(boot_handoff.physicalToVirtual(sdt_physical)); if (h.length <= @sizeOf(SystemDescriptorTableHeader)) return; aml_block_physical[aml_block_count] = sdt_physical + @sizeOf(SystemDescriptorTableHeader); aml_block_len[aml_block_count] = h.length - @sizeOf(SystemDescriptorTableHeader); aml_block_count += 1; } /// RSDP structure for revision 0 (version 1.0) const RootSystemDescriptionPointer = extern struct { /// An 8 byte magic number used for locating the RSDP, containing RSD PTR. signature: [8]u8, /// A byte used to verify the first 20 bytes of the RSDP checksum: u8, /// An OEM-supplied string that identified the OEM. oem_id: [6]u8, /// The RSDP revision, used for determining which fields are available. revision: u8, /// A 32-bit physical address pointing to the RSDT. root_system_description_table_address: u32 align(1), }; /// XSDP structure for revision 2 (version 2.0+) const ExtendedSystemDescriptorPointer = extern struct { /// An 8 byte magic number used for locating the RSDP, containing RSD PTR. signature: [8]u8, /// A byte used to verify the first 20 bytes of the RSDP checksum: u8, /// An OEM-supplied string that identified the OEM. oem_id: [6]u8, /// The RSDP revision, used for determining which fields are available. revision: u8, /// deprecated since version 2.0. A 32-bit physical address pointing to the RSDT. root_system_description_table_address: u32 align(1), /// The size of the RSDP. length: u32 align(1), /// A 64-bit physical address pointing to the XSDT. If the revision is at least 2, the XSDT /// should be used regardless of architecture, as the RSDT was deprecated. extended_system_descriptor_table_address: u64 align(1), /// A checksum used for the entire table. extended_checksum: u8, reserved: [3]u8, }; /// Multiple APIC Description Table (MADT) const APIC: [4]u8 = "APIC".*; /// Boot Error Record Table (BERT) const BERT: [4]u8 = "BERT".*; /// Corrected Platform Error Polling Table (CPEP) const CPEP: [4]u8 = "CPEP".*; /// Differentiated System Description Table (DSDT) const DSDT: [4]u8 = "DSDT".*; /// Embedded Controller Boot Resources Table (ECDT) const ECDT: [4]u8 = "ECDT".*; /// Error Injection Table (EINJ) const EINJ: [4]u8 = "EINJ".*; /// Error Record Serialization Table (ERST) const ERST: [4]u8 = "ERST".*; /// Fixed ACPI Description Table (FADT) const FACP: [4]u8 = "FACP".*; /// Firmware ACPI Control Structure (FACS) const FACS: [4]u8 = "FACS".*; /// Hardware Error Source Table (HEST) const HEST: [4]u8 = "HEST".*; /// High Precision Event Timer table (HPET) const HPET: [4]u8 = "HPET".*; /// PCI Express memory-mapped configuration space table (MCFG) const MCFG: [4]u8 = "MCFG".*; /// Maximum System Characteristics Table (MSCT) const MSCT: [4]u8 = "MSCT".*; /// Memory Power State Table (MPST) const MPST: [4]u8 = "MPST".*; // Platform Memory Topology Table (PMTT) const PMTT: [4]u8 = "PMTT".*; /// Persistent System Description Table (PSDT) const PSDT: [4]u8 = "PSDT".*; /// ACPI RAS Feature Table (RASF) const RASF: [4]u8 = "RASF".*; /// Root System Description Table const RSDT: [4]u8 = "RSDT".*; /// Smart Battery Specification Table (SBST) const SBST: [4]u8 = "SBST".*; /// System Locality System Information Table (SLIT) const SLIT: [4]u8 = "SLIT".*; /// System Resource Affinity Table (SRAT) const SRAT: [4]u8 = "SRAT".*; /// Secondary System Description Table (SSDT) const DMAR: [4]u8 = "DMAR".*; /// I/O Virtualization Reporting Structure (IVRS) — the AMD-Vi analogue of DMAR. const IVRS: [4]u8 = "IVRS".*; const SSDT: [4]u8 = "SSDT".*; /// Serial Port Console Redirection table (SPCR) — the firmware's console UART. const SPCR: [4]u8 = "SPCR".*; /// Extended System Description Table (XSDT; 64-bit version of the RSDT) const XSDT: [4]u8 = "XSDT".*; /// The header every system descriptor table (RSDT/XSDT and each SDT) begins with. const SystemDescriptorTableHeader = extern struct { /// A 4 byte signature used for identification (e.g. "RSDT", "APIC"). signature: [4]u8, /// The length of the entire table, including the header. length: u32 align(1), /// The revision of the ACPI spec this table conforms to. revision: u8, /// An 8-bit checksum field for the whole table, inclusive of the header. checksum: u8, /// An OEM-supplied string that identified the OEM. oem_id: [6]u8, oem_table_id: [8]u8, oem_revision: u32 align(1), creator_id: u32 align(1), creator_revision: u32 align(1), }; // --- MADT: Multiple APIC Description Table (signature "APIC") --------------- const Madt = extern struct { header: SystemDescriptorTableHeader, local_apic_address: u32 align(1), flags: u32 align(1), // Followed by a variable-length run of interrupt-controller records, each a // MadtRecordHeader plus a type-specific body. }; const MadtRecordHeader = extern struct { type: u8, length: u8, }; /// MADT record type 0: a processor's Local APIC. const MadtLocalApic = extern struct { record: MadtRecordHeader, processor_id: u8, apic_id: u8, /// bit 0 = enabled, bit 1 = online-capable. flags: u32 align(1), }; /// MADT record type 1: an I/O APIC. const MadtIoApic = extern struct { record: MadtRecordHeader, io_apic_id: u8, reserved: u8, address: u32 align(1), /// First global system interrupt this I/O APIC handles. gsi_base: u32 align(1), }; /// MADT record type 2: an Interrupt Source Override (ISA IRQ -> GSI remap). const MadtIso = extern struct { record: MadtRecordHeader, bus: u8, source: u8, gsi: u32 align(1), flags: u16 align(1), }; /// MADT record type 5: Local APIC Address Override (64-bit MMIO base). const MadtLapicOverride = extern struct { record: MadtRecordHeader, reserved: u16 align(1), address: u64 align(1), }; // --- MCFG: PCIe ECAM configuration space (signature "MCFG") ----------------- const Mcfg = extern struct { header: SystemDescriptorTableHeader, reserved: u64 align(1), // Followed by one or more McfgAllocation entries. }; const McfgAllocation = extern struct { /// Physical base of this segment group's ECAM window. base_address: u64 align(1), segment_group: u16 align(1), start_bus: u8, end_bus: u8, reserved: u32 align(1), }; // --- HPET (signature "HPET") ------------------------------------------------ const Hpet = extern struct { header: SystemDescriptorTableHeader, hardware_rev_id: u8, flags: u8, pci_vendor_id: u16 align(1), // Generic Address Structure describing the register block. address_space_id: u8, register_bit_width: u8, register_bit_offset: u8, gas_reserved: u8, address: u64 align(1), hpet_number: u8, minimum_tick: u16 align(1), page_protection: u8, }; // --- Entry point ------------------------------------------------------------ /// 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 into `power_information`, and publishes the AML blobs for the ring-3 acpi service. 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 = .{}; fadt_physical = 0; fadt_length = 0; platform_information = .{}; dsdt_physical = 0; aml_block_count = 0; const rsdp: *const RootSystemDescriptionPointer = @ptrFromInt(boot_handoff.physicalToVirtual(rsdp_physical)); if (!std.mem.eql(u8, &rsdp.signature, "RSD PTR ")) return error.BadRsdpSignature; // Revision 0 checksums only the first 20 bytes (the v1.0 RSDP). if (!checksumOk(@ptrFromInt(boot_handoff.physicalToVirtual(rsdp_physical)), 20)) return error.BadRsdpChecksum; if (rsdp.revision >= 2) { const xsdp: *const ExtendedSystemDescriptorPointer = @ptrFromInt(boot_handoff.physicalToVirtual(rsdp_physical)); if (!checksumOk(@ptrFromInt(boot_handoff.physicalToVirtual(rsdp_physical)), xsdp.length)) return error.BadXsdpChecksum; try walkRoot(u64, xsdp.extended_system_descriptor_table_address, device_tree, hal); } else { try walkRoot(u32, rsdp.root_system_description_table_address, device_tree, hal); } // The kernel does **not** interpret the DSDT/SSDTs. Static-table discovery // above (MADT/HPET/FADT/MCFG) is all the kernel needs — CPUs, timers, PCIe, // and the power register map. The AML bytecode (device enumeration and the // sleep-state `_Sx` values for soft-off) is entirely the ring-3 acpi service's // job: it claims the acpi-tables node published below, parses the same blobs, // and both registers the `_HID` devices and owns S5. Not parsing ~0.5 MB of // AML in the kernel keeps boot latency off the critical, single-core path. // Publish the acpi-tables node (docs/discovery.md): 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 — the sole path by which AML (devices + soft-off) reaches ring 3, // now that the kernel keeps only the *static* tables for itself. 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/discovery.md (the acpi service's one trusted node). _ = node.addResource(.io_port, 0, 1 << 16); // A broad interrupt window: ACPI _CRS names legacy ISA IRQs (the PS/2 lines // 1 and 12, the RTC, …), and the service registers those devices under this // node, so it must own a superset. The range [0, 256) covers every GSI; the // SCI (recorded first, len 1) stays distinct so M21 can pick it out. if (power_information.sci_interrupt != 0) _ = node.addResource(.irq, power_information.sci_interrupt, 1); _ = node.addResource(.irq, 0, 256); // The FADT rides along (M21): the service reads the PM1 event / GPE blocks // from its own copy, telling it apart from the AML blobs by signature. if (fadt_physical != 0) _ = node.addResource(.memory, fadt_physical, fadt_length); } /// Walk the RSDT (Entry = u32) or XSDT (Entry = u64): validate it, then dispatch /// each SDT it points at. A bad individual table is skipped, not fatal. fn walkRoot(comptime Entry: type, root_physical: u64, device_tree: *DeviceTree, hal: Hal) !void { const header: *const SystemDescriptorTableHeader = @ptrFromInt(boot_handoff.physicalToVirtual(root_physical)); if (!checksumOk(@ptrFromInt(boot_handoff.physicalToVirtual(root_physical)), header.length)) return error.BadRootChecksum; const count = (header.length - @sizeOf(SystemDescriptorTableHeader)) / @sizeOf(Entry); const base: [*]const u8 = @ptrFromInt(boot_handoff.physicalToVirtual(root_physical)); const entries: [*]align(1) const Entry = @ptrCast(base + @sizeOf(SystemDescriptorTableHeader)); for (entries[0..count]) |ent| { const sdt_physical: u64 = ent; // u32 entries widen; u64 pass through handleTable(device_tree, hal, sdt_physical) catch continue; } } /// Dispatch a single SDT on its signature. fn handleTable(device_tree: *DeviceTree, hal: Hal, sdt_physical: u64) !void { const header: *const SystemDescriptorTableHeader = @ptrFromInt(boot_handoff.physicalToVirtual(sdt_physical)); const sig = header.signature; if (std.mem.eql(u8, &sig, &APIC)) { try parseMadt(device_tree, header); } else if (std.mem.eql(u8, &sig, &MCFG)) { try parseMcfg(device_tree, header); } else if (std.mem.eql(u8, &sig, &HPET)) { try parseHpet(device_tree, hal, header); } else if (std.mem.eql(u8, &sig, &FACP)) { fadt_physical = sdt_physical; fadt_length = header.length; parseFadt(header); } else if (std.mem.eql(u8, &sig, &SPCR)) { parseSpcr(header); } else if (std.mem.eql(u8, &sig, &DMAR)) { parseDmar(header); } else if (std.mem.eql(u8, &sig, &IVRS)) { parseIvrs(header); } else if (std.mem.eql(u8, &sig, &SSDT)) { // Secondary namespace bytecode — collect it to publish for the ring-3 parse. addAmlBlock(sdt_physical); } // Any other signature is recognised but left opaque for now. } /// MADT -> one processor node per Local APIC, one interrupt_controller per I/O APIC. fn parseMadt(device_tree: *DeviceTree, header: *const SystemDescriptorTableHeader) !void { const madt: *const Madt = @ptrCast(header); const total: usize = header.length; const base: [*]const u8 = @ptrCast(header); var ioapic_index: usize = 0; // MADT header: local APIC base + flags (bit 0 = 8259 PIC present). platform_information.lapic_base = madt.local_apic_address; platform_information.pic_present = madt.flags & 1 != 0; var off: usize = @sizeOf(Madt); while (off + @sizeOf(MadtRecordHeader) <= total) { const rec: *const MadtRecordHeader = @ptrCast(base + off); if (rec.length < @sizeOf(MadtRecordHeader)) break; // malformed; avoid a spin switch (rec.type) { 0 => { const la: *const MadtLocalApic = @ptrCast(base + off); // bit 0 = enabled: skip processors the firmware marks unusable. if (la.flags & 1 != 0) { var nb: [24]u8 = undefined; const nm = std.fmt.bufPrint(&nb, "cpu{d}", .{la.processor_id}) catch "cpu"; _ = try device_tree.addChild(device_tree.root, .processor, nm); // Also record it as a schedulable core (with the APIC ID an AP // wake needs, which the device node name doesn't preserve). if (cpu_information.count < cpu_information.cpus.len) { cpu_information.cpus[cpu_information.count] = .{ .processor_id = la.processor_id, .apic_id = la.apic_id, .online_capable = la.flags & 2 != 0, }; cpu_information.count += 1; } else { cpu_information.dropped += 1; } } }, 1 => { const io: *const MadtIoApic = @ptrCast(base + off); var nb: [24]u8 = undefined; const nm = std.fmt.bufPrint(&nb, "ioapic{d}", .{ioapic_index}) catch "ioapic"; ioapic_index += 1; const d = try device_tree.addChild(device_tree.root, .interrupt_controller, nm); _ = d.addResource(.memory, io.address, 0x20); // The GSI range this I/O APIC handles, starting at gsi_base. _ = d.addResource(.irq, io.gsi_base, 0); }, 2 => { const iso: *const MadtIso = @ptrCast(base + off); if (platform_information.override_count < platform_information.overrides.len) { platform_information.overrides[platform_information.override_count] = .{ .source = iso.source, .gsi = iso.gsi, .flags = iso.flags, }; platform_information.override_count += 1; } }, 5 => { const ovr: *const MadtLapicOverride = @ptrCast(base + off); platform_information.lapic_base = ovr.address; }, else => {}, } off += rec.length; } } /// MCFG -> a pci_host_bridge per ECAM segment, then a PCI enumeration underneath. fn parseMcfg(device_tree: *DeviceTree, header: *const SystemDescriptorTableHeader) !void { const total: usize = header.length; const base: [*]const u8 = @ptrCast(header); var off: usize = @sizeOf(Mcfg); while (off + @sizeOf(McfgAllocation) <= total) : (off += @sizeOf(McfgAllocation)) { const alloc: *const McfgAllocation = @ptrCast(base + off); const bus_count: u64 = @as(u64, alloc.end_bus - alloc.start_bus) + 1; var nb: [24]u8 = undefined; const nm = std.fmt.bufPrint(&nb, "pci{d}", .{alloc.segment_group}) catch "pci"; const bridge = try device_tree.addChild(device_tree.root, .pci_host_bridge, nm); // 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); // 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); // 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. } } /// 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/discovery.md — apertures from the memory map): 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; // Above 4 GiB only *usable RAM* blocks the aperture: OVMF describes // its own 64-bit PCI window as a reserved region and then programs // BARs inside it — honoring reserved there would exclude the very // space BARs live in. Below 4 GiB every described region blocks (the // kernel image, the tables, the ramdisk all live there). Bring-up // trust: only the bridge's claimant can register into the aperture. 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); } /// HPET -> a timer node with its register block as an MMIO resource, plus the GSI /// its comparators can raise. /// /// Unlike a PCI device or an ACPI `_CRS` node, the HPET table carries **no interrupt /// number**: which I/O APIC inputs a comparator may drive is advertised at runtime, /// as a bitmask in `Tn_INT_ROUTE_CAP` (bits 63:32 of the Timer 0 configuration register). /// So discovery maps the register block, reads the mask, and records one concrete /// `irq` resource — the GSI a driver is entitled to bind. The driver commits to it /// by writing `Tn_INT_ROUTE_CNF`; the kernel checks the binding against this /// resource (see process.ownedGsi), which is what keeps `irq_bind` a capability /// rather than a request for an arbitrary interrupt line. fn parseHpet(device_tree: *DeviceTree, hal: Hal, header: *const SystemDescriptorTableHeader) !void { const hpet: *const Hpet = @ptrCast(header); const d = try device_tree.addChild(device_tree.root, .timer, "hpet"); // The GAS tag must say System Memory (0) before we treat `address` as a physical // address. The HPET spec mandates it, but firmware is not a thing to trust: a // System I/O (1) tag here would have us map an arbitrary page and read a bogus // route-capability mask out of it. if (hpet.address_space_id != gas_system_memory) return; _ = d.addResource(.memory, hpet.address, 0x400); const regs = hal.mapMmio(hpet.address, 0x400, true); const t0_configuration: *const volatile u64 = @ptrFromInt(regs + 0x100); const route_cap: u32 = @truncate(t0_configuration.* >> 32); if (hpetGsi(route_cap)) |gsi| _ = d.addResource(.irq, gsi, 1); } /// ACPI Generic Address Structure address-space ids we care about. const gas_system_memory: u8 = 0; /// Pick a GSI for the HPET out of its route-capability mask. Prefer an input at or /// above 16: the low ones overlap the legacy ISA lines (2 = cascaded PIT, 8 = RTC), /// which the MADT may separately override, whereas 16+ are the free upper inputs on /// every I/O APIC we care about. Falls back to the lowest bit set if there are none. fn hpetGsi(route_cap: u32) ?u32 { if (route_cap == 0) return null; var gsi: u32 = 16; while (gsi < 32) : (gsi += 1) { if (route_cap & (@as(u32, 1) << @intCast(gsi)) != 0) return gsi; } return @ctz(route_cap); } // FADT field offsets (bytes from the table start). The FADT grew across ACPI // revisions, so every field is read through `fadt()` with a length guard rather // than a fixed struct — an older/shorter FADT simply lacks the later (X_) fields. const fadt_dsdt = 40; // u32 const fadt_smi_cmd = 48; // u32 (an I/O port) const fadt_acpi_enable = 52; // u8 const fadt_acpi_disable = 53; // u8 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 const fadt_x_dsdt = 140; // u64 const fadt_x_pm1a_cnt_blk = 172; // GAS const fadt_x_pm1b_cnt_blk = 184; // GAS const fadt_x_pm_tmr_blk = 208; // GAS const flag_reset_register_supported = 1 << 10; const flag_tmr_value_ext = 1 << 8; // PM timer counter is 32-bit (else 24-bit) /// FADT -> the power register map (into `power_information`) and the DSDT address, /// whose bytecode is collected for the ring-3 parse. No AML interpretation here. fn parseFadt(header: *const SystemDescriptorTableHeader) void { const base: [*]align(1) const u8 = @ptrCast(header); 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; const cnt_width = fadt(u8, base, len, fadt_pm1_cnt_len) orelse 2; pi.pm1a_cnt = readCntRegister(base, len, fadt_x_pm1a_cnt_blk, fadt_pm1a_cnt_blk, cnt_width); pi.pm1b_cnt = readCntRegister(base, len, fadt_x_pm1b_cnt_blk, fadt_pm1b_cnt_blk, cnt_width); const flags = fadt(u32, base, len, fadt_flags) orelse 0; pi.reset_supported = flags & flag_reset_register_supported != 0; pi.reset = readGas(base, len, fadt_reset_register) orelse .{}; pi.reset_value = fadt(u8, base, len, fadt_reset_value) orelse 0; // The PM timer — a fixed-rate counter used as a calibration reference when no // HPET is present. Prefer the 64-bit-capable X_ GAS, fall back to the port. platform_information.pm_timer = readCntRegister(base, len, fadt_x_pm_tmr_blk, fadt_pm_tmr_blk, 4); platform_information.pm_timer_32bit = flags & flag_tmr_value_ext != 0; var dsdt: u64 = fadt(u32, base, len, fadt_dsdt) orelse 0; if (fadt(u64, base, len, fadt_x_dsdt)) |x| { if (x != 0) dsdt = x; } dsdt_physical = dsdt; addAmlBlock(dsdt); } // SPCR field offsets (bytes from the table start). const spcr_interface_type = 36; // u8 const spcr_base_address = 40; // GAS (12 bytes) /// SPCR -> the console UART's address + interface type, so serial can target the /// firmware's actual debug port instead of assuming legacy COM1. fn parseSpcr(header: *const SystemDescriptorTableHeader) void { const base: [*]align(1) const u8 = @ptrCast(header); const len: usize = header.length; const gas = readGas(base, len, spcr_base_address) orelse return; if (gas.address == 0) return; platform_information.spcr_uart = gas; platform_information.spcr_kind = fadt(u8, base, len, spcr_interface_type) orelse 0; } // DMAR remapping-structure layout (Intel VT-d spec §8): the DMAR-specific header is 12 // bytes (host-address-width, flags, 10 reserved), then a list of {type u16, length u16} // structures. Type 0 is a DRHD (DMA Remapping Hardware Unit Definition): flags byte at // offset 4 (bit 0 = INCLUDE_PCI_ALL, the catch-all unit), 64-bit register base at // offset 8. Type 1 is an RMRR (Reserved Memory Region Reporting): a physical range at // offsets 8/16 (base / inclusive limit) that the device(s) named by the trailing // device-scope entries keep DMAing into across the firmware→OS handoff. const dmar_structures_offset = 48; // 36-byte ACPI header + 12-byte DMAR header const dmar_type_drhd: u16 = 0; const dmar_type_rmrr: u16 = 1; const drhd_flags_offset = 4; const drhd_include_pci_all: u8 = 1; const drhd_register_base_offset = 8; const rmrr_base_offset = 8; const rmrr_limit_offset = 16; const rmrr_scopes_offset = 24; // Device-scope entry (within DRHD/RMRR structures): type 1 = PCI endpoint; the path is // (device, function) byte pairs from offset 6, one pair per bridge hop plus the leaf. const scope_type_pci_endpoint: u8 = 1; const scope_start_bus_offset = 5; const scope_path_offset = 6; /// DMAR -> the VT-d unit(s) and reserved memory regions. Walks every remapping /// structure: selects the INCLUDE_PCI_ALL DRHD (the catch-all covering all devices not /// scoped elsewhere — commonly the SECOND unit on real machines, after an iGPU-scoped /// one), counts the rest so the boot log can warn that their devices stay untranslated, /// and records single-path endpoint RMRRs for the IOMMU core to pre-map before it /// enables translation. Multi-hop RMRR scopes are skipped loudly: better a named gap /// than a silent one. fn parseDmar(header: *const SystemDescriptorTableHeader) void { const base: [*]align(1) const u8 = @ptrCast(header); const total: usize = header.length; var off: usize = dmar_structures_offset; while (off + 4 <= total) { const kind = fadt(u16, base, total, off) orelse break; const length = fadt(u16, base, total, off + 2) orelse break; if (length < 4 or off + length > total) break; // malformed; stop rather than loop if (kind == dmar_type_drhd) { const register_base = fadt(u64, base, total, off + drhd_register_base_offset) orelse 0; const include_all = ((fadt(u8, base, total, off + drhd_flags_offset) orelse 0) & drhd_include_pci_all) != 0; if (register_base != 0) { // Selection: the INCLUDE_PCI_ALL unit wins; otherwise keep the first // seen. A later catch-all replaces an earlier scoped unit. const replace = !platform_information.iommu_present or (include_all and !platform_information.iommu_include_all); if (replace) { // Table facts only: the unit's registers are the IOMMU // backend's business (it maps and validates them at // detect) — discovery records where they live, never // reads them. if (platform_information.iommu_present) platform_information.iommu_extra_units += 1; platform_information.iommu_present = true; platform_information.iommu_base = register_base; platform_information.iommu_include_all = include_all; } else { platform_information.iommu_extra_units += 1; } } } else if (kind == dmar_type_rmrr) { parseRmrr(base, total, off, length); } off += length; } } /// One RMRR structure: record a {bdf, base, limit} per single-path endpoint scope. fn parseRmrr(base: [*]align(1) const u8, total: usize, off: usize, length: usize) void { const range_base = fadt(u64, base, total, off + rmrr_base_offset) orelse return; const range_limit = fadt(u64, base, total, off + rmrr_limit_offset) orelse return; if (range_limit < range_base) return; var scope = off + rmrr_scopes_offset; const end = off + length; while (scope + 6 <= end) { const scope_type = fadt(u8, base, total, scope) orelse break; const scope_length = fadt(u8, base, total, scope + 1) orelse break; if (scope_length < 6 or scope + scope_length > end) break; if (scope_type == scope_type_pci_endpoint and scope_length == scope_path_offset + 2) { // Single (device, function) pair: a directly-reachable endpoint. const bus = fadt(u8, base, total, scope + scope_start_bus_offset) orelse 0; const device = fadt(u8, base, total, scope + scope_path_offset) orelse 0; const function = fadt(u8, base, total, scope + scope_path_offset + 1) orelse 0; if (platform_information.rmrr_count < maximum_rmrr) { platform_information.rmrr[platform_information.rmrr_count] = .{ .bdf = (@as(u16, bus) << 8) | (@as(u16, device) << 3) | function, .base = range_base, .limit = range_limit, }; platform_information.rmrr_count += 1; } else { platform_information.rmrr_skipped +|= 1; // table full } } else { platform_information.rmrr_skipped +|= 1; // multi-hop path or non-endpoint scope } scope += scope_length; } } // IVRS layout (AMD I/O Virtualization spec): 36-byte ACPI header, IVinfo u32 @36, // 8 reserved @40, then IVHD/IVMD blocks from @48. An IVHD common header is type u8 @0, // flags u8 @1, length u16 @2, device id u16 @4, capability offset u16 @6, IOMMU base // address u64 @8, PCI segment u16 @16, IOMMU info u16 @18. const ivrs_blocks_offset = 48; const ivhd_type_10: u8 = 0x10; const ivhd_type_11: u8 = 0x11; const ivhd_base_offset = 8; /// IVRS -> detect an AMD-Vi IOMMU. Record the control-register base from the first IVHD /// of type 0x10/0x11. Per-device entries and IVMD (the AMD analogue of RMRR) are ignored /// in v1 — the default-deny device table is what we build anyway, and QEMU emits no IVMD; /// a real machine that needs them is flagged untested on AMD regardless. fn parseIvrs(header: *const SystemDescriptorTableHeader) void { const base: [*]align(1) const u8 = @ptrCast(header); const total: usize = header.length; var off: usize = ivrs_blocks_offset; while (off + 4 <= total) { const kind = fadt(u8, base, total, off) orelse break; const length = fadt(u16, base, total, off + 2) orelse break; if (length < 4 or off + length > total) break; if (kind == ivhd_type_10 or kind == ivhd_type_11) { const iommu_base = fadt(u64, base, total, off + ivhd_base_offset) orelse 0; if (iommu_base != 0) { platform_information.iommu_present = true; platform_information.iommu_is_amd = true; platform_information.iommu_base = iommu_base; return; // first IVHD is enough; multi-unit is future work } } off += length; } } // --- helpers ---------------------------------------------------------------- /// Sum `len` bytes; an ACPI table/pointer is valid when the low 8 bits are zero. fn checksumOk(bytes: [*]const u8, len: usize) bool { var sum: u8 = 0; for (0..len) |i| sum +%= bytes[i]; return sum == 0; } /// Read a FADT field of type `T` at `off`, or null if the table is too short to /// contain it (a legal state for older FADT revisions). fn fadt(comptime T: type, base: [*]align(1) const u8, len: usize, off: usize) ?T { if (off + @sizeOf(T) > len) return null; return rd(T, base, off); } /// Decode a Generic Address Structure at `off` into a `RegisterAccess`. GAS layout: /// address_space(u8), bit_width(u8), bit_offset(u8), access_size(u8), address(u64). fn readGas(base: [*]align(1) const u8, len: usize, off: usize) ?RegisterAccess { if (off + 12 > len) return null; const address_space = rd(u8, base, off); const bit_width = rd(u8, base, off + 1); const address = rd(u64, base, off + 4); return .{ .mmio = address_space == 0, // 0 = system memory, 1 = system I/O .address = address, .width = bit_width / 8, }; } /// A PM1 control register: prefer the 64-bit-capable X_ GAS form; fall back to the /// legacy 32-bit I/O-port field. Width comes from PM1_CNT_LEN either way. fn readCntRegister(base: [*]align(1) const u8, len: usize, xoff: usize, legacy_off: usize, width: u8) RegisterAccess { if (readGas(base, len, xoff)) |g| { if (g.address != 0) return .{ .mmio = g.mmio, .address = g.address, .width = width }; } const port = fadt(u32, base, len, legacy_off) orelse 0; return .{ .mmio = false, .address = port, .width = width }; } /// Read a little-endian integer at `off` from a (possibly unaligned) byte pointer. /// x86 is little-endian and native, so an unaligned load suffices. fn rd(comptime T: type, bytes: [*]align(1) const u8, off: usize) T { const p: *align(1) const T = @ptrCast(bytes + off); return p.*; } // --- machine reboot ------------------------------------------------------------- // Restart via the FADT reset register (from `power_information` above), with legacy // fallbacks, driven through the injected `Hal`. Soft-off (ACPI S5) and suspend (S3) // are NOT here: they need the AML sleep-state (`_Sx`) values, which the kernel no // longer parses — the ring-3 acpi service owns power management (it re-parses the // blobs and writes the PM1 control register itself). Reboot stays in the kernel // because it needs no AML — only the FADT reset register and the well-known legacy // fallbacks — so it survives as a last-resort restart. See docs/power.md. /// Restart the machine. Tries the ACPI reset register first, then the two legacy /// fallbacks. Returns only if every method failed (very unlikely). pub fn reboot(hal: device_model.Hal) void { const pi = power_information; // 1. The FADT reset register, when the firmware advertises support. if (pi.reset_supported and pi.reset.present()) { writeResetRegister(hal, pi.reset, pi.reset_value); rebootDelay(); } // 2. The PCI reset-control register at port 0xCF9 (RST_CPU | SYSTEM_RST). hal.pioWrite(1, 0xCF9, 0x0E); hal.pioWrite(1, 0xCF9, 0x06); rebootDelay(); // 3. Pulse the 8042 keyboard controller's reset line. hal.pioWrite(1, 0x64, 0xFE); rebootDelay(); } fn writeResetRegister(hal: device_model.Hal, register: RegisterAccess, value: u32) void { if (register.mmio) { const p: *align(1) volatile u32 = @ptrFromInt(hal.mapMmio(register.address, 4, true)); p.* = value; } else { hal.pioWrite(register.width, @intCast(register.address), value); } } /// A short busy-wait so a reset takes effect before we fall through to the next /// method. The empty asm is an architecture-neutral barrier that keeps the loop /// from being optimised away. fn rebootDelay() void { var i: usize = 0; while (i < 50_000_000) : (i += 1) { asm volatile ("" ::: .{ .memory = true }); } }