diff --git a/.gitignore b/.gitignore index 020a5aafb..e552cb732 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,8 @@ debug.out core out/ + +# Ignore all binary and decompiled ACPI files except the ones used for tests. +*.dat +*.dsl +!phd-tests/tests/testdata/acpi/**/*.dat diff --git a/Cargo.lock b/Cargo.lock index c6320d360..c1dbcbfe6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,14 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "acpi_tables" +version = "0.2.1" +source = "git+https://github.com/oxidecomputer/acpi_tables.git?tag=v0.2.1-oxide.1#f20592bc0f2bfe9b71493de67e6b917fcea50c2e" +dependencies = [ + "zerocopy 0.8.27", +] + [[package]] name = "addr2line" version = "0.21.0" @@ -5873,6 +5881,7 @@ dependencies = [ name = "phd-tests" version = "0.1.0" dependencies = [ + "acpi_tables", "anyhow", "backoff", "byteorder", @@ -5887,6 +5896,7 @@ dependencies = [ "oximeter", "oximeter-producer", "phd-testcase", + "propolis", "propolis-client 0.1.0", "reqwest 0.13.2", "slog", @@ -6416,6 +6426,7 @@ dependencies = [ name = "propolis" version = "0.1.0" dependencies = [ + "acpi_tables", "anyhow", "async-trait", "bhyve_api 0.0.0", diff --git a/Cargo.toml b/Cargo.toml index 83c87705c..8536926bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,6 +100,7 @@ dice-verifier = { git = "https://github.com/oxidecomputer/dice-util", rev = "1d3 vm-attest = { git = "https://github.com/oxidecomputer/vm-attest", rev = "2cdd17580a4fc6c871d24797016af8dbaac9421d", default-features = false } # External dependencies +acpi_tables = "0.2.0" anyhow = "1.0" async-trait = "0.1.88" atty = "0.2.14" @@ -192,6 +193,8 @@ usdt = { version = "0.6", default-features = false } uuid = "1.3.2" zerocopy = "0.8.25" +[patch.crates-io] +acpi_tables = { git = 'https://github.com/oxidecomputer/acpi_tables.git', tag = "v0.2.1-oxide.1" } # # It's common during development to use a local copy of various complex diff --git a/bin/propolis-server/Cargo.toml b/bin/propolis-server/Cargo.toml index de5a10195..93ea82cee 100644 --- a/bin/propolis-server/Cargo.toml +++ b/bin/propolis-server/Cargo.toml @@ -84,6 +84,7 @@ proptest.workspace = true [features] default = [] +acpi-debug = ["propolis/acpi-debug"] # When building to be packaged for inclusion in the production ramdisk # (nominally an Omicron package), certain code is compiled in or out. diff --git a/bin/propolis-server/src/lib/initializer.rs b/bin/propolis-server/src/lib/initializer.rs index 89658c840..40d809ac3 100644 --- a/bin/propolis-server/src/lib/initializer.rs +++ b/bin/propolis-server/src/lib/initializer.rs @@ -107,6 +107,9 @@ pub enum MachineInitError { #[error("boot entry {0:?} refers to a device on non-zero PCI bus {1}")] BootDeviceOnDownstreamPciBus(SpecKey, u8), + #[error("failed to generate ACPI tables: {0}")] + AcpiTableError(#[from] fwcfg::formats::AcpiTablesError), + #[error("failed to insert {0} fwcfg entry")] FwcfgInsertFailed(&'static str, #[source] fwcfg::InsertError), @@ -124,6 +127,15 @@ pub enum MachineInitError { /// Arbitrary ROM limit for now const MAX_ROM_SIZE: usize = 0x20_0000; +/// End address of the 32-bit PCI MMIO window. +/// +// Value inherited from the original EDK2 static tables. +// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/PlatformPei/Platform.c#L180-L192 +// +// It should be updated to match the actual memory regions registered in the +// instance. +const PCI_MMIO32_END: usize = 0xfeef_ffff; + fn get_spec_guest_ram_limits(spec: &Spec) -> (usize, usize) { let memsize = spec.board.memory_mb as usize * MB; let lowmem = memsize.min(3 * GB); @@ -406,16 +418,25 @@ impl MachineInitializer<'_> { continue; } - let (irq, port) = match desc.num { - SerialPortNumber::Com1 => (ibmpc::IRQ_COM1, ibmpc::PORT_COM1), - SerialPortNumber::Com2 => (ibmpc::IRQ_COM2, ibmpc::PORT_COM2), - SerialPortNumber::Com3 => (ibmpc::IRQ_COM3, ibmpc::PORT_COM3), - SerialPortNumber::Com4 => (ibmpc::IRQ_COM4, ibmpc::PORT_COM4), + let (uart_name, irq, port) = match desc.num { + SerialPortNumber::Com1 => { + ("COM1", ibmpc::IRQ_COM1, ibmpc::PORT_COM1) + } + SerialPortNumber::Com2 => { + ("COM2", ibmpc::IRQ_COM2, ibmpc::PORT_COM2) + } + SerialPortNumber::Com3 => { + ("COM3", ibmpc::IRQ_COM3, ibmpc::PORT_COM3) + } + SerialPortNumber::Com4 => { + ("COM4", ibmpc::IRQ_COM4, ibmpc::PORT_COM4) + } }; - let dev = LpcUart::new(chipset.irq_pin(irq).unwrap()); + let dev = + LpcUart::new(uart_name, irq, chipset.irq_pin(irq).unwrap()); dev.set_autodiscard(true); - LpcUart::attach(&dev, &self.machine.bus_pio, port); + dev.attach(&self.machine.bus_pio, port); self.devices.insert(name.to_owned(), dev.clone()); if desc.num == SerialPortNumber::Com1 { assert!(com1.is_none()); @@ -1091,9 +1112,13 @@ impl MachineInitializer<'_> { // Set up an LPC uart for ASIC management comms from the guest. // // NOTE: SoftNpu squats on com4. - let uart = LpcUart::new(chipset.irq_pin(ibmpc::IRQ_COM4).unwrap()); + let uart = LpcUart::new( + "COM4", + ibmpc::IRQ_COM4, + chipset.irq_pin(ibmpc::IRQ_COM4).unwrap(), + ); uart.set_autodiscard(true); - LpcUart::attach(&uart, &self.machine.bus_pio, ibmpc::PORT_COM4); + uart.attach(&self.machine.bus_pio, ibmpc::PORT_COM4); self.devices .insert(SpecKey::Name("softnpu-uart".to_string()), uart.clone()); @@ -1416,8 +1441,42 @@ impl MachineInitializer<'_> { Ok(Some(order.finish())) } + fn generate_acpi_tables( + &self, + cpus: u8, + ) -> Result { + let (lowmem, _) = get_spec_guest_ram_limits(self.spec); + let generators: Vec<_> = self + .devices + .values() + .filter_map(|dev| dev.as_dsdt_generator()) + .collect(); + + // The values for pci_window_32 and pci_window_64 are set based on the + // original EDK2 ACPI tables, and currently don't exactly match the + // ranges defined in build_instance(). + // + // Propolis doesn't verify if an MMIO operation happens in an address + // reserved for MMIO, so this doesn't cause problems for now, but the + // PCI windows should be updated to match what's reserved in + // build_instance(). + let pci_window_32 = fwcfg::formats::PciWindow::new( + lowmem as u64, + PCI_MMIO32_END as u64, + )?; + + let config = &fwcfg::formats::AcpiConfig { + num_cpus: cpus, + pci_window_32, + pci_window_64: fwcfg::formats::PciWindow::empty(), + dsdt_generators: &generators, + }; + let acpi_tables = fwcfg::formats::AcpiTablesBuilder::new(config); + Ok(acpi_tables.build()) + } + /// Initialize qemu `fw_cfg` device, and populate it with data including CPU - /// count, SMBIOS tables, and attached RAM-FB device. + /// count, SMBIOS and ACPI tables, and attached RAM-FB device. /// /// Should not be called before [`Self::initialize_rom()`]. pub fn initialize_fwcfg( @@ -1462,6 +1521,19 @@ impl MachineInitializer<'_> { .insert_named("etc/e820", e820_entry) .map_err(|e| MachineInitError::FwcfgInsertFailed("e820", e))?; + let acpi_entries = self.generate_acpi_tables(cpus)?; + fwcfg.insert_named("etc/acpi/tables", acpi_entries.tables).map_err( + |e| MachineInitError::FwcfgInsertFailed("acpi/tables", e), + )?; + fwcfg + .insert_named("etc/acpi/rsdp", acpi_entries.rsdp) + .map_err(|e| MachineInitError::FwcfgInsertFailed("acpi/rsdp", e))?; + fwcfg + .insert_named("etc/table-loader", acpi_entries.table_loader) + .map_err(|e| { + MachineInitError::FwcfgInsertFailed("table-loader", e) + })?; + let ramfb = ramfb::RamFb::create( self.log.new(slog::o!("component" => "ramfb")), ); diff --git a/bin/propolis-standalone/Cargo.toml b/bin/propolis-standalone/Cargo.toml index 137affeb2..7bcd39059 100644 --- a/bin/propolis-standalone/Cargo.toml +++ b/bin/propolis-standalone/Cargo.toml @@ -44,3 +44,4 @@ pbind.workspace = true [features] default = [] crucible = ["propolis/crucible-full", "propolis/oximeter", "crucible-client-types"] +acpi-debug = ["propolis/acpi-debug"] diff --git a/bin/propolis-standalone/src/main.rs b/bin/propolis-standalone/src/main.rs index 3b3eec631..7b1d038a2 100644 --- a/bin/propolis-standalone/src/main.rs +++ b/bin/propolis-standalone/src/main.rs @@ -44,6 +44,15 @@ const PAGE_OFFSET: u64 = 0xfff; // Arbitrary ROM limit for now const MAX_ROM_SIZE: usize = 0x20_0000; +/// End address of the 32-bit PCI MMIO window. +/// +// Value inherited from the original EDK2 static tables. +// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/PlatformPei/Platform.c#L180-L192 +// +// It should be updated to match the actual memory regions registered in the +// instance. +const PCI_MMIO32_END: usize = 0xfeef_ffff; + const MIN_RT_THREADS: usize = 8; const BASE_RT_THREADS: usize = 4; @@ -1071,6 +1080,40 @@ fn generate_bootorder( Ok(Some(order.finish())) } +fn generate_acpi_tables( + cpus: u8, + lowmem: usize, + inventory: &Inventory, +) -> anyhow::Result { + let generators: Vec<_> = inventory + .devs + .values() + .filter_map(|dev| dev.as_dsdt_generator()) + .collect(); + + // The values for pci_window_32 and pci_window_64 are set based on the + // original EDK2 ACPI tables, and currently don't exactly match the + // ranges defined in build_machined(). + // + // Propolis doesn't verify if an MMIO operation happens in an address + // reserved for MMIO, so this doesn't cause problems for now, but the + // PCI windows should be updated to match what's reserved in + // build_machine(). + let pci_window_32 = + fwcfg::formats::PciWindow::new(lowmem as u64, PCI_MMIO32_END as u64) + .context("invalid PCI window range")?; + + let config = &fwcfg::formats::AcpiConfig { + num_cpus: cpus, + pci_window_32, + pci_window_64: fwcfg::formats::PciWindow::empty(), + dsdt_generators: &generators, + }; + let acpi_tables = fwcfg::formats::AcpiTablesBuilder::new(config); + + Ok(acpi_tables.build()) +} + fn setup_instance( config: config::Config, from_restore: bool, @@ -1183,10 +1226,26 @@ fn setup_instance( guard.inventory.register(&hpet); // UARTs - let com1 = LpcUart::new(chipset_lpc.irq_pin(ibmpc::IRQ_COM1).unwrap()); - let com2 = LpcUart::new(chipset_lpc.irq_pin(ibmpc::IRQ_COM2).unwrap()); - let com3 = LpcUart::new(chipset_lpc.irq_pin(ibmpc::IRQ_COM3).unwrap()); - let com4 = LpcUart::new(chipset_lpc.irq_pin(ibmpc::IRQ_COM4).unwrap()); + let com1 = LpcUart::new( + "COM1", + ibmpc::IRQ_COM1, + chipset_lpc.irq_pin(ibmpc::IRQ_COM1).unwrap(), + ); + let com2 = LpcUart::new( + "COM2", + ibmpc::IRQ_COM2, + chipset_lpc.irq_pin(ibmpc::IRQ_COM2).unwrap(), + ); + let com3 = LpcUart::new( + "COM3", + ibmpc::IRQ_COM3, + chipset_lpc.irq_pin(ibmpc::IRQ_COM3).unwrap(), + ); + let com4 = LpcUart::new( + "COM4", + ibmpc::IRQ_COM4, + chipset_lpc.irq_pin(ibmpc::IRQ_COM4).unwrap(), + ); com1_sock.spawn( Arc::clone(&com1) as Arc, @@ -1200,10 +1259,10 @@ fn setup_instance( com4.set_autodiscard(true); let pio = &machine.bus_pio; - LpcUart::attach(&com1, pio, ibmpc::PORT_COM1); - LpcUart::attach(&com2, pio, ibmpc::PORT_COM2); - LpcUart::attach(&com3, pio, ibmpc::PORT_COM3); - LpcUart::attach(&com4, pio, ibmpc::PORT_COM4); + com1.attach(pio, ibmpc::PORT_COM1); + com2.attach(pio, ibmpc::PORT_COM2); + com3.attach(pio, ibmpc::PORT_COM3); + com4.attach(pio, ibmpc::PORT_COM4); guard.inventory.register_instance(&com1, "com1"); guard.inventory.register_instance(&com2, "com2"); guard.inventory.register_instance(&com3, "com3"); @@ -1425,6 +1484,18 @@ fn setup_instance( let e820_entry = generate_e820(machine, log).expect("can build E820 table"); fwcfg.insert_named("etc/e820", e820_entry).unwrap(); + let acpi_entries = generate_acpi_tables(cpus, lowmem, &guard.inventory) + .expect("can build ACPI tables"); + fwcfg + .insert_named("etc/acpi/tables", acpi_entries.tables) + .context("Failed to insert ACPI tables")?; + fwcfg + .insert_named("etc/acpi/rsdp", acpi_entries.rsdp) + .context("Failed to insert ACPI RSDP")?; + fwcfg + .insert_named("etc/table-loader", acpi_entries.table_loader) + .context("Failed to insert ACPI table-loader")?; + fwcfg.attach(pio, &machine.acc_mem); guard.inventory.register(&fwcfg); diff --git a/lib/propolis/Cargo.toml b/lib/propolis/Cargo.toml index d092121f6..721ec6ee0 100644 --- a/lib/propolis/Cargo.toml +++ b/lib/propolis/Cargo.toml @@ -24,6 +24,7 @@ tokio = { workspace = true, features = ["full"] } futures.workspace = true paste.workspace = true pin-project-lite.workspace = true +acpi_tables.workspace = true anyhow.workspace = true rgb_frame.workspace = true rfb.workspace = true @@ -65,6 +66,7 @@ rand.workspace = true default = [] crucible-full = ["crucible", "crucible-client-types", "oximeter", "nexus-client"] falcon = ["libloading", "p9ds", "dlpi", "ispf", "rand", "softnpu", "viona_api/falcon"] +acpi-debug = [] # TODO until crucible#1280 is addressed, enabling Nexus notifications is done # through a feature flag. diff --git a/lib/propolis/src/firmware/acpi/aml.rs b/lib/propolis/src/firmware/acpi/aml.rs new file mode 100644 index 000000000..3a9633304 --- /dev/null +++ b/lib/propolis/src/firmware/acpi/aml.rs @@ -0,0 +1,206 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Collection of AML helpers and wrappers. + +use acpi_tables::aml; + +/// Creates an IO port with a fixed port number. +/// +/// The AML IO operation takes a min and max range of acceptable port numbers. +/// To create a fixed IO port allocation, min and max must be set to the same +/// value, which can look confusing. +/// +/// Relocatable IO ports should be created using a similar wrapper. +/// +/// The value for alignment is irrelevant when min and max are the same, but is +/// kept here to keep the ACPI tables consistent with the original EDK2 values. +/// +/// ACPI rev. 6.6 section 6.4.2.5 "I/O Port Descriptor" +pub fn io_port(port: u16, alignment: u8, length: u8) -> aml::IO { + aml::IO::new(port, port, alignment, length) +} + +// Flags used to defined ACPI methods concurrency control. +// +// See ACPI section 19.6.84 "Method (Declare Control Method)" for authoritative +// information about these flags. + +/// Declare the ASL method marked as "Serialized", meaning it is not safe for +/// use by multiple concurrent threads. +pub const SERIALIZED: bool = true; + +/// Declare the ASL method marked as "NotSerialized", meaning it is safe for +/// concurrent access (does not declare objects internally, etc) +pub const NOT_SERIALIZED: bool = false; + +/// Constructors for ACPI paths defined in the ACPI specification. +/// +/// ACPI rev. 6.6 section 5.3 "ACPI Namespace" describes the (limited) syntax +/// for names; you may want to read before adding or editing items in this +/// module. +pub mod paths { + use acpi_tables::aml; + + macro_rules! path { + ($fn:ident, $name:expr) => { + pub fn $fn() -> aml::Path { + aml::Path::new($name) + } + }; + } + + // Object that evaluates to a device's address on its parent bus. + // + // ACPI rev. 6.6 section 6.1.1 "_ADR (Address)" + path!(adr, "_ADR"); + + // PCI bus number set up by the platform boot firmware. + // + // ACPI rev. 6.6 section 6.5.5 "_BBN (Base Bus Number)" + path!(bbn, "_BBN"); + + // Object that evaluates to a device's Plug and Play-compatible ID list. + // + // ACPI rev. 6.6 section 6.1.2 "_CID (Compatible ID)" + path!(cid, "_CID"); + + // Object that specifies a device's current resource settings, or a control + // method that generates such an object. + // + // ACPI rev. 6.6 section 6.2.2 "_CRS (Current Resource Settings)" + path!(crs, "_CRS"); + + // Object that associates a logical software name (for example, COM1) with + // a device. + // + // ACPI rev. 6.6 section 6.1.4 "_DDN (DOS Device Name)" + path!(ddn, "_DDN"); + + // Control method that disables a device. + // + // ACPI rev. 6.6 section 6.2.3 "_DIS (Disable)" + path!(dis, "_DIS"); + + // Object that evaluates to a device's Plug and Play hardware ID. + // + // ACPI rev. 6.6 section 6.1.5 "_HID (Hardware ID)" + path!(hid, "_HID"); + + // An object that specifies a device's possible resource settings, or a + // control method that generates such an object. + // + // ACPI rev. 6.6 section 6.2.13 "_PRS (Possible Resource Settings)" + path!(prs, "_PRS"); + + // Object that specifies the PCI interrupt routing table. + // + // ACPI rev. 6.6 section 6.2.14 "_PRT (PCI Routing Table)" + path!(prt, "_PRT"); + + // Control method that sets a device's settings. + // + // ACPI rev. 6.6 section 6.2.17 "_SRS (Set Resource Settings)" + path!(srs, "_SRS"); + + // Control method that returns a device's status. + // + // ACPI rev. 6.6 section 6.3.7 "_STA (Device Status)" + path!(sta, "_STA"); + + // Object that specifies a device's unique persistent ID, or a control + // method that generates it. + // + // ACPI rev. 6.6 section 6.1.12 "_UID (Unique ID)" + path!(uid, "_UID"); +} + +/// Constructors for ACPI names defined in the ACPI specification. +/// +/// Refer to [paths] for more information. +pub mod names { + use super::paths; + use acpi_tables::{aml, Aml}; + + macro_rules! name { + ($fn:ident) => { + pub fn $fn(inner: &dyn Aml) -> aml::Name { + aml::Name::new(paths::$fn(), inner) + } + }; + } + + name!(adr); + name!(bbn); + name!(cid); + name!(crs); + name!(ddn); + name!(hid); + name!(prs); + name!(sta); + name!(uid); +} + +/// Constructors for ACPI methods defined in the ACPI specification. +/// +/// Refer to [paths] for more information. +pub mod methods { + use super::paths; + use acpi_tables::{aml, Aml}; + + macro_rules! method { + ($fn:ident) => { + pub fn $fn<'a>( + args: u8, + serialized: bool, + children: Vec<&'a dyn Aml>, + ) -> aml::Method<'a> { + aml::Method::new(paths::$fn(), args, serialized, children) + } + }; + } + + method!(crs); + method!(dis); + method!(prs); + method!(prt); + method!(srs); + method!(sta); +} + +/// Device ID and Plug and Play (`PNP`) device codes used throughout ACPI tables. +/// UEFI and ACPI use standardized IDs as described in https://uefi.org/PNP_ACPI_Registry, +/// which itself points to reserved device IDs at +/// https://uefi.org/sites/default/files/resources/devids%20%285%29.txt +pub mod devids { + // --Interrupt Controllers-- + pub const AT_INT_CONTROLLER: &'static str = "PNP0000"; + + // --Timers-- + pub const AT_TIMER: &'static str = "PNP0100"; + + // --DMA-- + pub const AT_DMA_CONTROLLER: &'static str = "PNP0200"; + + // --Keyboards-- + pub const IBM_ENHANCED_KEYBOARD: &'static str = "PNP0303"; + pub const MICROSOFT_RESERVED_KEYBOARD: &'static str = "PNP030B"; + + // --Serial Devices-- + pub const COM_PORT_16550A: &'static str = "PNP0501"; + + // --Peripheral Buses-- + pub const PCI_BUS: &'static str = "PNP0A03"; + + // --Real Time Clock, BIOS, System board devices-- + pub const AT_SPEAKER_SOUND: &'static str = "PNP0800"; + pub const AT_REAL_TIME_CLOCK: &'static str = "PNP0B00"; + pub const GENERAL_ID: &'static str = "PNP0C02"; + pub const MATH_COPROCESSOR: &'static str = "PNP0C04"; + pub const PCI_INT_LINK: &'static str = "PNP0C0F"; + + // --QEMU--- + // https://www.qemu.org/docs/master/specs/pvpanic.html + pub const QEMU_PVPANIC: &'static str = "QEMU0001"; +} diff --git a/lib/propolis/src/firmware/acpi/dsdt.rs b/lib/propolis/src/firmware/acpi/dsdt.rs new file mode 100644 index 000000000..6da20ed0c --- /dev/null +++ b/lib/propolis/src/firmware/acpi/dsdt.rs @@ -0,0 +1,1318 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Generates a DSDT and SSDT ACPI tables for an instance. +//! +//! The [`Dsdt`] struct implements the `Aml` trait of the `acpi_tables` crate +//! and can write the AML bytecode to any AmlSink, like a `Vec`. +//! +//! The AML code can also be generated by the objects being represented in the +//! DSDT table, which is often a better practice as it keeps internal +//! configuration and AML representation close to each other. +//! +//! Structs that implement the [`DsdtGenerator`] trait can be passed to +//! [`DsdtConfig`] and their AML code will be added to the scope they selected. + +// The DSDT and SSDT tables generated here are kept consistent with the +// original EDK2 static tables. +// +// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiTables/Dsdt.asl +// +// It may be better in the future to move the AML code generation logic closer +// to the devices they represent. For example, the _SB scope could be created +// by the I440FxHostBridge struct, the LPC by Piix3Lpc etc., but they currently +// lack all the information necessary to generate their portion of the tables. +// +// This pattern is already used for some devices when possible. + +use super::aml::{devids, methods, names, paths, *}; +use super::{ + GPE0_BLK_ADDR, GPE0_BLK_LEN, IO_APIC_ADDR, IO_APIC_LEN, LOCAL_APIC_ADDR, + LOCAL_APIC_LEN, PCI_LINK_IRQS, +}; +use crate::hw::{chipset::i440fx, pci, qemu}; +use acpi_tables::{aml, sdt::Sdt, Aml, AmlSink}; + +// The DSDT and SSDT table headers are currently kept the same as the ones +// used in the original tables from EDK2. They can be update to Propolis values +// in the future. +fn dsdt_sdt_edk2_style() -> Sdt { + Sdt::new(*b"DSDT", 36, 1, *b"INTEL ", *b"OVMF ", 0x4) +} + +fn ssdt_sdt_edk2_style() -> Sdt { + // For SSDTs, the OEM table ID needs to be different for each table. Refer + // to ACPI rev. 6.6 section 5.2.11.2 "Secondary System Description Table + // (SSDT)" for more information. + // + // The SSDT provided by EDK2 can also be removed entirely in the future + // because it only holds unsupported sleep states (S2 and S3) and FWDT + // memory region. + Sdt::new(*b"SSDT", 36, 1, *b"REDHAT", *b"OVMF ", 0x1) +} + +/// The ACPI scope in which DsdtGenerators are placed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DsdtScope { + SystemBus, // \_SB scope. + PciRoot, // \_SB.PCI0 scope. + Lpc, // \_SB.PCI0.LPC scope. +} + +/// An implementer of DsdtGenerator is able to generate AML code to be loaded +/// into the DSDT ACPI table. +pub trait DsdtGenerator: Aml { + /// Returns the scope of the DSDT table in which the generated AML code + /// should be placed. + fn dsdt_scope(&self) -> DsdtScope; +} + +/// Wraps a list of DsdtGenerators to help generate their AML code in places +/// where a `&dyn Aml` is needed. +struct DsdtGeneratorAml<'a> { + generators: &'a [&'a dyn DsdtGenerator], + scope: DsdtScope, +} + +impl<'a> DsdtGeneratorAml<'a> { + fn new(generators: &'a [&'a dyn DsdtGenerator], scope: DsdtScope) -> Self { + Self { generators, scope } + } +} + +impl<'a> Aml for DsdtGeneratorAml<'a> { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + self.generators + .iter() + .filter(|&&g| g.dsdt_scope() == self.scope) + .for_each(|&g| g.to_aml_bytes(sink)); + } +} + +/// Values for the PM1a_CNT.SLP_TYP register to enter different sleep states. +/// +/// +/// +/// These states are handled in `Piix3PM` via the `pmreg_write` method. +/// Currently, only the S0->S5 transition is handled explicitly, but S0->S0 is +/// also handled properly as a no-op, and so the value of 5 is never checked +/// directly. +/// +/// Transitions to S3 and S4 are inherited from the original EDK2 tables and +/// should probably be removed in the future. +const PM1A_CNT_SLP_TYP_S0: u8 = 5; +const PM1A_CNT_SLP_TYP_S3: u8 = 1; +const PM1A_CNT_SLP_TYP_S4: u8 = 2; +const PM1A_CNT_SLP_TYP_S5: u8 = 0; + +pub struct DsdtConfig<'a> { + pub generators: &'a [&'a dyn DsdtGenerator], +} + +/// The DSDT table is part of the fixed ACPI tables and is used to describe +/// system resources. +/// +/// +pub struct Dsdt<'a> { + config: DsdtConfig<'a>, +} + +impl<'a> Dsdt<'a> { + pub fn new(config: DsdtConfig<'a>) -> Self { + Self { config } + } +} + +impl<'a> Aml for Dsdt<'a> { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + let mut dsdt = Vec::new(); + + // This is an artifact inserted into the AML code to keep the DSDT + // table exactly the same as the static EDK2 tables used previously. + // It's not functionally necessary and can be removed in the future. + aml::If::new( + &aml::ZERO, + vec![&aml::External::new( + "\\_SB_.PCI0._CRS.FWDT".into(), + aml::ExternalObjectType::OperationRegion, + None, + )], + ) + .to_aml_bytes(&mut dsdt); + + // Sleep states. + SleepState::new("_S0_", PM1A_CNT_SLP_TYP_S0).to_aml_bytes(&mut dsdt); + SleepState::new("_S5_", PM1A_CNT_SLP_TYP_S5).to_aml_bytes(&mut dsdt); + + // System bus namespace (\_SB). + aml::Scope::new( + "_SB_".into(), + vec![ + &PciRootBridge { generators: self.config.generators }, + &DsdtGeneratorAml::new( + self.config.generators, + DsdtScope::SystemBus, + ), + ], + ) + .to_aml_bytes(&mut dsdt); + + // DSDT table. + let mut sdt = dsdt_sdt_edk2_style(); + sdt.append_slice(dsdt.as_slice()); + sdt.to_aml_bytes(sink); + } +} + +/// Describe a sleep state. +struct SleepState<'a> { + state: &'a str, + pm1a_cnt: u8, +} + +impl<'a> SleepState<'a> { + fn new(state: &'a str, pm1a_cnt: u8) -> Self { + Self { state, pm1a_cnt } + } +} + +impl<'a> Aml for SleepState<'a> { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + aml::Name::new( + self.state.into(), + &aml::Package::new(vec![ + &self.pm1a_cnt, // PM1a_CNT.SLP_TYP value to enter this sleep state. + &aml::ZERO, // PM1b_CNT.SLP_TYP value. PM1b is not currently used in Propolis. + &aml::ZERO, // Reserved. + &aml::ZERO, // Reserved. + ]), + ) + .to_aml_bytes(sink); + } +} + +/// PCI root bridge namespace (\_SB.PCI0). +struct PciRootBridge<'a> { + generators: &'a [&'a dyn DsdtGenerator], +} + +impl<'a> Aml for PciRootBridge<'a> { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + aml::Device::new( + "PCI0".into(), + vec![ + &names::hid(&aml::EISAName::new(devids::PCI_BUS)), + &names::adr(&aml::ZERO), + &names::bbn(&aml::ZERO), + &names::uid(&aml::ZERO), + &PciRootBridgeCrs {}, + &PciRootBridgePrt {}, + &PciRootBridgeLpc { generators: self.generators }, + &DsdtGeneratorAml::new(self.generators, DsdtScope::PciRoot), + ], + ) + .to_aml_bytes(sink); + } +} + +/// Bus number range for the PCI0 root bridge. +const PCI_BUS_START: u16 = 0x00; +const PCI_BUS_END: u16 = 0xff; + +/// MMIO address region used for legacy VGA devices. +/// +/// +const LEGACY_VGA_BASE: u32 = 0x000a_0000; +const LEGACY_VGA_LIMIT: u32 = 0x000b_ffff; + +// Byte offset for different fields that are referenced by other structures. +const ADDRESS_SPACE_32_MIN_OFFSET: usize = 0x0a; +const ADDRESS_SPACE_32_MAX_OFFSET: usize = 0x0e; +const ADDRESS_SPACE_32_LEN_OFFSET: usize = 0x16; + +const ADDRESS_SPACE_64_MIN_OFFSET: usize = 0x0e; +const ADDRESS_SPACE_64_MAX_OFFSET: usize = 0x16; +const ADDRESS_SPACE_64_LEN_OFFSET: usize = 0x26; + +const INTERRUPT_LIST_OFFSET: usize = 0x05; + +/// _CRS method for the PCI0 device (\_SB.PCI0._CRS). +/// +/// Refer to section 4.3 of the PCI Firmware Specification for more +/// information. +struct PciRootBridgeCrs {} + +// This implementation currently follows the original static EDK2 tables. It +// can be simplified to return a single ResourceTemplate with all the final +// values already populated instead of dynamically updating them based on the +// values read from FWDT. +impl Aml for PciRootBridgeCrs { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + // \_SB.PCI0.CRES + // + // This ResourceTemplate contains the base values for the PCI I/O ports + // and MMIO region reservations. + let mut cres: Vec<&dyn Aml> = Vec::new(); + + // PCI device numbers that belong to this bridge. + let bus_number = + aml::AddressSpace::new_bus_number(PCI_BUS_START, PCI_BUS_END); + cres.push(&bus_number); + + // Legacy PCI configuration I/O ports. + let pci_config_io_ports = io_port( + pci::bits::PORT_PCI_CONFIG_ADDR, + 1, + (pci::bits::LEN_PCI_CONFIG_ADDR + pci::bits::LEN_PCI_CONFIG_DATA) + as u8, + ); + cres.push(&pci_config_io_ports); + + // I/O ports below the PCI config ports (0x0000-0x0cf7). + let pci_io_ports_low = aml::AddressSpace::new_io( + 0x0000, + pci::bits::PORT_PCI_CONFIG_ADDR - 1, + None, + ); + cres.push(&pci_io_ports_low); + + // IO ports above the PCI config ports (0x0d00-0xffff). + let pci_io_ports_high = aml::AddressSpace::new_io( + pci::bits::PORT_PCI_CONFIG_ADDR + + pci::bits::LEN_PCI_CONFIG_ADDR + + pci::bits::LEN_PCI_CONFIG_DATA, + 0xffff, + None, + ); + cres.push(&pci_io_ports_high); + + // Legacy VGA MMIO region. + let legacy_vga = aml::AddressSpace::new_memory( + aml::AddressSpaceCacheable::Cacheable, + true, + LEGACY_VGA_BASE, + LEGACY_VGA_LIMIT, + None, + ); + cres.push(&legacy_vga); + + // The _CRS method needs to reference the 32-bit PCI MMIO region to + // update its range based on the instance configuration. The reference + // is based on the byte offset of this AddressSpace inside the CRES + // ResourceTemplate. + let mmio32_offset = aml_len(&cres); + let mmio32 = aml::AddressSpace::new_memory( + aml::AddressSpaceCacheable::NotCacheable, + true, + // These values are inherited from the original EDK2 static tables + // and they are only placeholders. On boot, the actual address + // range is read from FWDT and _CRS overwrites this address space. + 0xf800_0000_u32, + 0xfffb_ffff_u32, + None, + ); + cres.push(&mmio32); + + aml::Name::new("CRES".into(), &aml::ResourceTemplate::new(cres)) + .to_aml_bytes(sink); + + // \_SB.PCI0.CR64 + // + // This ResourceTemplate contains the 64-bit PCI MMIO region. The _CRS + // method concatenates it with CRES, if necessary, based on the + // instance configuration. + let mut cr64: Vec<&dyn Aml> = Vec::new(); + + // The _CRS method needs to reference the 64-bit PCI MMIO region to + // update its range based on the instance configuration. The reference + // is based on the byte offset of this AddressSpace inside the CR64 + // ResourceTemplate. + let mmio64_offset = aml_len(&cr64); + let mmio64 = aml::AddressSpace::new_memory( + aml::AddressSpaceCacheable::Cacheable, + true, + // These values are inherited from the original EDK2 static tables + // and they are only placeholders. On boot, the actual address + // range is read from FWDT and _CRS overwrites this address space. + 0x0080_0000_0000_u64, + 0x0fff_ffff_ffff_u64, + None, + ); + cr64.push(&mmio64); + + aml::Name::new("CR64".into(), &aml::ResourceTemplate::new(cr64)) + .to_aml_bytes(sink); + + // \_SB.PCI0._CRS + // + // This method returns a ResourceTemplate describing the PCI root + // bridge resources. + // + // It reads the FWDT OperationRegion that is declared in the SSDT. This + // region is populated by Propolis in lib/propolis/src/hw/qemu/fwcfg.rs + // and it stores the 32-bit and 64-bit MMIO regions reserved for PCI + // devices. + methods::crs( + 0, + SERIALIZED, + vec![ + // Create references to values in the FWDT OperationRegion. + &aml::Field::new( + "FWDT".into(), + aml::FieldAccessType::QWord, + aml::FieldLockRule::NoLock, + aml::FieldUpdateRule::Preserve, + vec![ + aml::FieldEntry::Named(*b"P0S_", 64), + aml::FieldEntry::Named(*b"P0E_", 64), + aml::FieldEntry::Named(*b"P0L_", 64), + aml::FieldEntry::Named(*b"P1S_", 64), + aml::FieldEntry::Named(*b"P1E_", 64), + aml::FieldEntry::Named(*b"P1L_", 64), + ], + ), + &aml::Field::new( + "FWDT".into(), + aml::FieldAccessType::DWord, + aml::FieldLockRule::NoLock, + aml::FieldUpdateRule::Preserve, + vec![ + aml::FieldEntry::Named(*b"P0SL", 32), + aml::FieldEntry::Named(*b"P0SH", 32), + aml::FieldEntry::Named(*b"P0EL", 32), + aml::FieldEntry::Named(*b"P0EH", 32), + aml::FieldEntry::Named(*b"P0LL", 32), + aml::FieldEntry::Named(*b"P0LH", 32), + aml::FieldEntry::Named(*b"P1SL", 32), + aml::FieldEntry::Named(*b"P1SH", 32), + aml::FieldEntry::Named(*b"P1EL", 32), + aml::FieldEntry::Named(*b"P1EH", 32), + aml::FieldEntry::Named(*b"P1LL", 32), + aml::FieldEntry::Named(*b"P1LH", 32), + ], + ), + // Create references to values in the mmio32 AddressSpace. + &aml::CreateDWordField::new( + &aml::Path::new("PS32"), + &aml::Path::new("CRES"), + &(mmio32_offset + ADDRESS_SPACE_32_MIN_OFFSET), + ), + &aml::CreateDWordField::new( + &aml::Path::new("PE32"), + &aml::Path::new("CRES"), + &(mmio32_offset + ADDRESS_SPACE_32_MAX_OFFSET), + ), + &aml::CreateDWordField::new( + &aml::Path::new("PL32"), + &aml::Path::new("CRES"), + &(mmio32_offset + ADDRESS_SPACE_32_LEN_OFFSET), + ), + // Update the values of mmio32 based on the FWDT. + &aml::Store::new( + &aml::Path::new("PS32"), // mmio32.min + &aml::Path::new("P0SL"), // FWDT.32bit.min (low bits) + ), + &aml::Store::new( + &aml::Path::new("PE32"), // mmio32.max + &aml::Path::new("P0EL"), // FWDT.32bit.max (low bits) + ), + &aml::Store::new( + &aml::Path::new("PL32"), // mmo32.len + &aml::Path::new("P0LL"), // FWDT.32bit.len (low bits) + ), + // Check if a 64-bit MMIO region is needed. + &aml::If::new( + &aml::LogicalAnd::new( + &aml::Equal::new( + &aml::Path::new("P1SL"), // FWDT.64bit.min (low bits) + &aml::ZERO, + ), + &aml::Equal::new( + &aml::Path::new("P1SH"), // FWDT.64bit.min (high bits) + &aml::ZERO, + ), + ), + // Only use CRES if FWDT.64bit.min is zero... + vec![&aml::Return::new(&aml::Path::new("CRES"))], + ), + // ...otherwise concatenate CRES and CR64. + &aml::Else::new(vec![ + // Create references to values in the mmio64 AddressSpace. + &aml::CreateQWordField::new( + &aml::Path::new("PS64"), + &aml::Path::new("CR64"), + &(mmio64_offset + ADDRESS_SPACE_64_MIN_OFFSET), + ), + &aml::CreateQWordField::new( + &aml::Path::new("PE64"), + &aml::Path::new("CR64"), + &(mmio64_offset + ADDRESS_SPACE_64_MAX_OFFSET), + ), + &aml::CreateQWordField::new( + &aml::Path::new("PL64"), + &aml::Path::new("CR64"), + &(mmio64_offset + ADDRESS_SPACE_64_LEN_OFFSET), + ), + // Update the values of mmio64 based on the FWDT. + &aml::Store::new( + &aml::Path::new("PS64"), // mmio64.min + &aml::Path::new("P1S_"), // FWDT.64bit.min + ), + &aml::Store::new( + &aml::Path::new("PE64"), // mmio64.max + &aml::Path::new("P1E_"), // FWDT.64bit.max + ), + &aml::Store::new( + &aml::Path::new("PL64"), // mmio64.max + &aml::Path::new("P1L_"), // FWDT.64bit.max + ), + // Concatenate CRES and CR64. + &aml::ConcatRes::new( + &aml::Local(0), + &aml::Path::new("CRES"), + &aml::Path::new("CR64"), + ), + &aml::Return::new(&aml::Local(0)), + ]), + ], + ) + .to_aml_bytes(sink); + } +} + +fn aml_len(vec: &[&dyn Aml]) -> usize { + let mut sink = Vec::new(); + for aml in vec { + aml.to_aml_bytes(&mut sink); + } + sink.len() +} + +/// Number of devices in the PCI0 root bridge to link to the interrupt +/// controller. +/// +/// Value inherited from the original EDK2 static tables. Future work may +/// update them to match Propolis's expectations, or replace legacy PCI PIC +/// interrupt routing for APIC. +const PCI_DEVICES: u8 = 16; +const PCI_INT_PINS: u8 = 4; + +/// _PRT method for the PCI0 device (\_SB.PCI0._PRT) +/// +/// +struct PciRootBridgePrt {} + +impl Aml for PciRootBridgePrt { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + let sources = ["^LPC_.LNKA", "^LPC_.LNKB", "^LPC_.LNKC", "^LPC_.LNKD"]; + let sources_len: u8 = sources.len() as u8; + + let mut ptr_entries = Vec::new(); + for device in 0..PCI_DEVICES { + for pin in 0..PCI_INT_PINS { + let source = if device == 1 && pin == 0 { + // Device 1, Pin 0 requires special handling. + // https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiTables/Dsdt.asl#L200-L220 + "^LPC_.LNKS" + } else { + // PCI devices are connected in a crossing pattern to + // evenly distribute load across the interrupt lines. + // + // ┌──────────┐ ┌──────────┐ ┌──────────┐ + // │PCI Device│ │PCI Device│ │PCI Device│ + // │ #1 │ │ #2 │ │ #3 │ + // ┌───────────────┐ │ │ │ │ │ │ + // │ IO/APIC │ │ INTA │ │ │ │ │ + // │ │┌───┼────*─────┼┐┌───┼────*─────┼┐┌───┼────*─────┼─-- + // │ LNKA ├┼──┐│ INTB │││ │ │││ │ │ + // │ LNKB ├┼─┐└┼────*─────┼┼┘┌──┼────*─────┼┼┘┌──┼────*─────┼─-- + // │ LNKC ├┼┐│ │ INTC ││ │ │ ││ │ │ │ + // │ LNKD ├┘│└─┼────*─────┼┼─┘┌─┼────*─────┼┼─┘┌─┼────*─────┼─-- + // └───────────────┘ │ │ INTD ││ │ │ ││ │ │ │ + // └──┼────*─────┼┼──┘┌┼────*─────┼┼──┘┌┼────*─────┼─-- + // └──────────┘└───┘└──────────┘└───┘└──────────┘ + // + let idx = (device + pin + sources_len - 1) % sources_len; + sources[idx as usize] + }; + + ptr_entries.push(PrtEntry { device, pin, source }); + } + } + let ptr = ptr_entries.iter().map(|p| p as &dyn Aml).collect(); + + methods::prt( + 0, + NOT_SERIALIZED, + vec![&aml::Return::new(&aml::Package::new(ptr))], + ) + .to_aml_bytes(sink); + } +} + +/// Low-word of an _ADR value that refers to all PCI functions. +/// +/// +const PCI_ADR_ALL_FUNC: u32 = 0xffff; + +/// Representation of an entry in the _PRT table. +struct PrtEntry<'a> { + device: u8, + pin: u8, + source: &'a str, +} + +impl<'a> Aml for PrtEntry<'a> { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + let addr: aml::DWord = ((self.device as u32) << 16) | PCI_ADR_ALL_FUNC; + + aml::Package::new(vec![ + &addr, + &self.pin, + &aml::Path::new(self.source), + &aml::ZERO, + ]) + .to_aml_bytes(sink); + } +} + +// These IO ports are handled in-kernel by bhyve. +// https://github.com/freebsd/freebsd-src/blob/d66fec481bfd65cbabb6c12a410d76843e76083e/sys/amd64/vmm/vmm_ioport.c#L46-L61 +const IO_ICU1: u16 = 0x20; +const IO_ICU2: u16 = 0xa0; +const IO_ELCR1: u16 = 0x4d0; +const IO_TIMER1: u16 = 0x40; +const IO_RTC: u16 = 0x70; +const NMISC_PORT: u16 = 0x61; + +/// PCI to ISA bridge for the PCI0 device (\_SB.PCI0.LPC). +/// +/// Refer to the original _PRT table from EDK2 for more details on what is being +/// defined here. +/// +/// +struct PciRootBridgeLpc<'a> { + generators: &'a [&'a dyn DsdtGenerator], +} + +// This table is currently kept the same as the original EDK2 static table, but +// it could be modernized to remove devices that are not used by Propolis. +impl<'a> Aml for PciRootBridgeLpc<'a> { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + aml::Device::new( + "LPC_".into(), + vec![ + &names::adr(&0x0001_0000_u64), + &Lnk::new("LNKS", 0), + // PCI Interrupt Routing Configuration Registers, PIRQRC[A:D]. + &aml::OpRegion::new( + "PRR0".into(), + aml::OpRegionSpace::PCIConfig, + &i440fx::PIR_OFFSET, + &i440fx::PIR_LEN, + ), + &aml::Field::new( + "PRR0".into(), + aml::FieldAccessType::Any, + aml::FieldLockRule::NoLock, + aml::FieldUpdateRule::Preserve, + vec![ + aml::FieldEntry::Named(*b"PIRA", 8), + aml::FieldEntry::Named(*b"PIRB", 8), + aml::FieldEntry::Named(*b"PIRC", 8), + aml::FieldEntry::Named(*b"PIRD", 8), + ], + ), + // _STA method for LNKA, LNKB, LNKC, and LNKD. + &aml::Method::new( + "PSTA".into(), + 1, + NOT_SERIALIZED, + vec![ + &aml::If::new( + &aml::And::new( + &aml::ZERO, + &aml::Arg(0), + &i440fx::PIR_MASK_DISABLE, + ), + vec![&aml::Return::new(&0x09_u64)], + ), + &aml::Else::new(vec![&aml::Return::new(&0x0B_u64)]), + ], + ), + // _CRS method for LNKA, LNKB, LNKC, and LNKD. + &aml::Method::new( + "PCRS".into(), + 1, + SERIALIZED, + vec![ + &aml::Name::new( + "BUF0".into(), + &aml::ResourceTemplate::new(vec![ + &aml::Interrupt::new( + true, + false, + false, + true, + vec![0x00], + ), + ]), + ), + &aml::CreateDWordField::new( + &aml::Path::new("IRQW"), + &aml::Path::new("BUF0"), + &INTERRUPT_LIST_OFFSET, + ), + &aml::If::new( + &aml::LogicalNot::new(&aml::And::new( + &aml::ZERO, + &aml::Arg(0), + &i440fx::PIR_MASK_DISABLE, + )), + vec![&aml::Store::new( + &aml::Path::new("IRQW"), + &aml::Arg(0), + )], + ), + &aml::Return::new(&aml::Path::new("BUF0")), + ], + ), + // _PRS resource for LNKA, LNKB, LNKC, and LNKD. + &aml::Name::new( + "PPRS".into(), + &aml::ResourceTemplate::new(vec![&aml::Interrupt::new( + true, + false, + false, + true, + PCI_LINK_IRQS + .iter() + .filter(|i| **i != i440fx::SCI_IRQ) // The SCI has special handling LNKS. + .map(|i| *i as u32) + .collect(), + )]), + ), + &Lnk::new("LNKA", 1), + &Lnk::new("LNKB", 2), + &Lnk::new("LNKC", 3), + &Lnk::new("LNKD", 4), + // Programmable Interrupt Controller (PIC). + &aml::Device::new( + "PIC_".into(), + vec![ + &names::hid(&aml::EISAName::new( + devids::AT_INT_CONTROLLER, + )), + &names::crs(&aml::ResourceTemplate::new(vec![ + &io_port(IO_ICU1, 0x00, 0x02), + &io_port(IO_ICU2, 0x00, 0x02), + &io_port(IO_ELCR1, 0x00, 0x02), + &aml::IrqNoFlags::new(2), + ])), + ], + ), + // ISA DMA. + // + // This is a legacy device inherited from the original EDK2 + // table. Its IO ports are not actually handled anywhere. + // It can be removed in the future . + &aml::Device::new( + "DMAC".into(), + vec![ + &names::hid(&aml::EISAName::new( + devids::AT_DMA_CONTROLLER, + )), + &names::crs(&aml::ResourceTemplate::new(vec![ + &io_port(0x0000, 0x00, 0x10), + &io_port(0x0081, 0x00, 0x03), + &io_port(0x0087, 0x00, 0x01), + &io_port(0x0089, 0x00, 0x03), + &io_port(0x008f, 0x00, 0x01), + &io_port(0x00c0, 0x00, 0x20), + &aml::Dma::new( + aml::DmaChannelSpeed::Compatibility, + aml::DmaMasterStatus::NotMaster, + aml::DmaTransferType::Transfer8, + vec![4], + ), + ])), + ], + ), + // 8254 Timer. + &aml::Device::new( + "TMR_".into(), + vec![ + &names::hid(&aml::EISAName::new(devids::AT_TIMER)), + &names::crs(&aml::ResourceTemplate::new(vec![ + &io_port(IO_TIMER1, 0x00, 0x04), + &aml::IrqNoFlags::new(0), + ])), + ], + ), + // Real Time Clock. + &aml::Device::new( + "RTC_".into(), + vec![ + &names::hid(&aml::EISAName::new( + devids::AT_REAL_TIME_CLOCK, + )), + &names::crs(&aml::ResourceTemplate::new(vec![ + &io_port(IO_RTC, 0x00, 0x02), + &aml::IrqNoFlags::new(8), + ])), + ], + ), + // PCAT Speaker. + &aml::Device::new( + "SPKR".into(), + vec![ + &names::hid(&aml::EISAName::new( + devids::AT_SPEAKER_SOUND, + )), + &names::crs(&aml::ResourceTemplate::new(vec![ + &io_port(NMISC_PORT, 0x01, 0x01), + ])), + ], + ), + // Floating Point Coprocessor. + // + // This is a legacy device inherited from the original EDK2 + // table. Its IO ports are not actually handled anywhere. + // It can be removed in the future . + &aml::Device::new( + "FPU_".into(), + vec![ + &names::hid(&aml::EISAName::new( + devids::MATH_COPROCESSOR, + )), + &names::crs(&aml::ResourceTemplate::new(vec![ + &io_port(0x00f0, 0x00, 0x10), + &aml::IrqNoFlags::new(13), + ])), + ], + ), + // Generic motherboard devices and pieces that don't fit + // anywhere else. + // + // This device and the remark above were inherited from the + // original EDK2 table. Most IO ports declared here are not + // actually handled anywhere and are probably just being + // reserved to force the guest OS to use upper addresses. + &aml::Device::new( + "XTRA".into(), + vec![ + &names::hid(&aml::EISAName::new(devids::GENERAL_ID)), + &names::uid(&aml::ONE), + &names::crs(&aml::ResourceTemplate::new(vec![ + &io_port(0x0010, 0x00, 0x10), + &io_port(0x0022, 0x00, 0x1e), + &io_port(0x0044, 0x00, 0x1c), + &io_port(0x0062, 0x00, 0x02), + &io_port(0x0065, 0x00, 0x0b), + &io_port(0x0072, 0x00, 0x0e), + &io_port(0x0080, 0x00, 0x01), + &io_port(0x0084, 0x00, 0x03), + &io_port(0x0088, 0x00, 0x01), + &io_port(0x008c, 0x00, 0x03), + &io_port(0x0090, 0x00, 0x10), + &io_port(0x00a2, 0x00, 0x1e), + &io_port(0x00e0, 0x00, 0x10), + &io_port(0x01e0, 0x00, 0x10), + &io_port(0x0160, 0x00, 0x10), + &io_port(0x0370, 0x00, 0x02), + &io_port( + qemu::debug::QEMU_DEBUG_IOPORT, + 0x00, + 0x01, + ), + &io_port(0x0440, 0x00, 0x10), + // QEMU GPE0 BLK. + &io_port(GPE0_BLK_ADDR, 0x00, GPE0_BLK_LEN), + // PMBLK1. + &io_port( + i440fx::PMBASE_DEFAULT, + 0x00, + i440fx::PMBASE_LEN as u8, + ), + // IO APIC. + &aml::Memory32Fixed::new( + false, + IO_APIC_ADDR, + IO_APIC_LEN, + ), + // LAPIC. + &aml::Memory32Fixed::new( + false, + LOCAL_APIC_ADDR, + LOCAL_APIC_LEN, + ), + ])), + ], + ), + &DsdtGeneratorAml::new(self.generators, DsdtScope::Lpc), + // QEMU panic device. + // + // This device could be generated by the QemuPvpanic struct and + // passed as a DsdtGenerator, but it's currently only present + // if the enable_isa configuration is enabled for the VM. + // + // For now, it is always generated here to maintain consistency + // with the original EDK2 static tables. + &aml::Device::new( + "PEVT".into(), + vec![ + &names::hid(&devids::QEMU_PVPANIC), + &names::crs(&aml::ResourceTemplate::new(vec![ + &io_port(qemu::pvpanic::IOPORT, 0x01, 0x01), + ])), + &aml::OpRegion::new( + "PEOR".into(), + aml::OpRegionSpace::SystemIO, + &qemu::pvpanic::IOPORT, + &aml::ONE, + ), + &aml::Field::new( + "PEOR".into(), + aml::FieldAccessType::Byte, + aml::FieldLockRule::NoLock, + aml::FieldUpdateRule::Preserve, + vec![aml::FieldEntry::Named(*b"PEPT", 8)], + ), + &names::sta(&0x0f_u64), + &aml::Method::new( + "RDPT".into(), + 0, + NOT_SERIALIZED, + vec![ + &aml::Store::new( + &aml::Local(0), + &aml::Path::new("PEPT"), + ), + &aml::Return::new(&aml::Local(0)), + ], + ), + &aml::Method::new( + "WRPT".into(), + 1, + NOT_SERIALIZED, + vec![&aml::Store::new( + &aml::Path::new("PEPT"), + &aml::Arg(0), + )], + ), + ], + ), + ], + ) + .to_aml_bytes(sink); + } +} + +/// Represents a PCI IRQ link in the LPC device. +struct Lnk<'a> { + name: &'a str, + uid: u32, +} + +impl<'a> Lnk<'a> { + fn new(name: &'a str, uid: u32) -> Self { + Self { name, uid } + } +} + +impl<'a> Lnk<'a> { + // AML code for the special SCI link. + // + fn to_aml_bytes_sci(&self, sink: &mut dyn AmlSink) { + aml::Device::new( + "LNKS".into(), + vec![ + &names::hid(&aml::EISAName::new(devids::PCI_INT_LINK)), + &names::uid(&aml::ZERO), + &names::sta(&0x0b_u64), + &methods::srs(1, NOT_SERIALIZED, vec![]), + &methods::dis(0, NOT_SERIALIZED, vec![]), + &names::prs(&aml::ResourceTemplate::new(vec![ + &aml::Interrupt::new(true, false, false, true, vec![0x09]), + ])), + &methods::crs( + 0, + NOT_SERIALIZED, + vec![&aml::Return::new(&paths::prs())], + ), + ], + ) + .to_aml_bytes(sink); + } +} + +impl<'a> Aml for Lnk<'a> { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + if self.uid == 0 { + self.to_aml_bytes_sci(sink); + return; + } + + let pir = aml::Path::new(&format!( + "PIR{}", + self.name.chars().last().unwrap() + )); + + aml::Device::new( + aml::Path::new(self.name), + vec![ + &names::hid(&aml::EISAName::new(devids::PCI_INT_LINK)), + &names::uid(&self.uid), + &methods::sta( + 0, + NOT_SERIALIZED, + vec![&aml::Return::new(&aml::MethodCall::new( + "PSTA".into(), + vec![&pir], + ))], + ), + &methods::dis( + 0, + NOT_SERIALIZED, + vec![&aml::Or::new(&pir, &pir, &i440fx::PIR_MASK_DISABLE)], + ), + &methods::crs( + 0, + NOT_SERIALIZED, + vec![&aml::Return::new(&aml::MethodCall::new( + "PCRS".into(), + vec![&pir], + ))], + ), + &methods::prs( + 0, + NOT_SERIALIZED, + vec![&aml::Return::new(&aml::MethodCall::new( + "PPRS".into(), + vec![], + ))], + ), + &methods::srs( + 1, + NOT_SERIALIZED, + vec![ + &aml::CreateDWordField::new( + &aml::Path::new("IRQW"), + &aml::Arg(0), + &0x05_u64, + ), + &aml::Store::new(&pir, &aml::Path::new("IRQW")), + ], + ), + ], + ) + .to_aml_bytes(sink); + } +} + +/// Length in bytes of the SSDT header. Used to calculate the offset of other +/// fields. +/// +/// +const SSDT_HEADER_LEN: usize = 36; + +/// Byte offset of the FWDT OperationRegion offset address field in the SSDT +/// table. This filed is updated in fwcfg.rs during table generation. +/// +/// SSDT header (36 bytes) + External operation prefix (1 byte) + +/// OperationRegion prefix (1 byte) + OperationRegion name (4 bytes) + +/// OperationRegion space (1 byte) + DWordPrefix (1 byte) +/// +/// +pub const SSDT_FWDT_ADDR_OFFSET: usize = SSDT_HEADER_LEN + 8; + +/// Number of bytes used to store the offset address value in the FWDT +/// OperationRegion. Size of a DWord. +pub const SSDT_FWDT_ADDR_LEN: usize = 4; + +/// The SSDT table is an extension to DSDT table and can be used to extend +/// resources defined in the DSDT. +/// +/// +pub struct Ssdt { + /// Offset of the area reserved for the FWDT OperationRegion data in the + /// overall ACPI tables storage. + fwdt_offset: usize, +} + +impl Ssdt { + pub fn new(fwdt_offset: usize) -> Self { + Self { fwdt_offset } + } +} + +// This implementation follows the original static EDK2 tables. +// +// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiPlatformDxe/Qemu.c#L426-L466 +// +// It can probably be further simplified or eliminated entirely in the future. +impl Aml for Ssdt { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + let mut ssdt = Vec::new(); + + // The FWDT OperationRegion is used to pass dynamic information about + // the instance from the platform to the virtual machine. The main + // information provided are the 32-bit and 64-bit PCI MMIO ranges. + // + // On boot, the \_SB.PCI0._CRS method reads FWDT and adjusts the PCI + // bus configuration based on the data it holds. + // + // This process can be removed if \_SB.PCI0._CRS is modified to return + // a static ResourceTemplate that is already populated with the right + // VM data. + aml::OpRegion::new( + "FWDT".into(), + aml::OpRegionSpace::SystemMemory, + &DWord::new(self.fwdt_offset as u32), + &DWord::new(0x30), + ) + .to_aml_bytes(&mut ssdt); + + // Sleep states. + // + // These sleep states are kept for consistency with the original static + // EDK2 tables. Propolis doesn't handle these state properly, so they + // should be removed in the future. + // + // These values don't use the SleepState struct to keep the generated + // AML code the same as the original EDK tables. + aml::Name::new( + "\\_S3_".into(), + &aml::Package::new(vec![ + &Byte::new(PM1A_CNT_SLP_TYP_S3), + &Byte::new(0), + &Byte::new(0), + &Byte::new(0), + ]), + ) + .to_aml_bytes(&mut ssdt); + + aml::Name::new( + "\\_S4_".into(), + &aml::Package::new(vec![ + &Byte::new(PM1A_CNT_SLP_TYP_S4), + &Byte::new(0), + &Byte::new(0), + &Byte::new(0), + ]), + ) + .to_aml_bytes(&mut ssdt); + + let mut sdt = ssdt_sdt_edk2_style(); + sdt.append_slice(ssdt.as_slice()); + sdt.to_aml_bytes(sink); + } +} + +// Provides consistent DWord AML values. The acpi_tables crate minimizes +// integers to the smallest word size that the number fits, but the original +// EDK2-defined ACPI tables used wider-than-necessary integers in some places. +// +// We retained this quirk to avoid unnecessary differences in ACPI tables when +// having Propolis generate them, but future ACPI table versions have no +// particular need to keep this. +struct DWord { + value: u32, +} + +impl DWord { + fn new(value: u32) -> Self { + Self { value } + } +} + +impl Aml for DWord { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + sink.byte(0x0c); // DWordPrefix + sink.dword(self.value); + } +} + +// Provides consistent Byte AML values. The acpi_tables crate minimizes +// integers to the smallest word size that the number fits, but the original +// EDK2-defined ACPI tables used wider-than-necessary integers in some places. +// +// We retained this quirk to avoid unnecessary differences in ACPI tables when +// having Propolis generate them, but future ACPI table versions have no +// particular need to keep this. +struct Byte { + value: u8, +} + +impl Byte { + fn new(value: u8) -> Self { + Self { value } + } +} + +impl Aml for Byte { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + sink.byte(0x0a); // BytePrefix + sink.byte(self.value); + } +} + +#[cfg(test)] +mod test { + use super::*; + + struct MockDsdtGenerator { + scope: DsdtScope, + } + impl DsdtGenerator for MockDsdtGenerator { + fn dsdt_scope(&self) -> DsdtScope { + self.scope + } + } + impl Aml for MockDsdtGenerator { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + aml::Name::new("TEST".into(), &format!("{:?}", self.scope)) + .to_aml_bytes(sink); + } + } + + #[test] + fn dsdt_generator_aml() { + let generators: Vec<&dyn DsdtGenerator> = vec![ + &MockDsdtGenerator { scope: DsdtScope::SystemBus }, + &MockDsdtGenerator { scope: DsdtScope::PciRoot }, + &MockDsdtGenerator { scope: DsdtScope::Lpc }, + ]; + + let mut got = Vec::new(); + let mut expected = Vec::new(); + + // Filter by SystemBus. + DsdtGeneratorAml::new(&generators, DsdtScope::SystemBus) + .to_aml_bytes(&mut got); + aml::Name::new("TEST".into(), &"SystemBus").to_aml_bytes(&mut expected); + assert_eq!(expected, got); + + got.clear(); + expected.clear(); + + // Filter by PciRoot. + DsdtGeneratorAml::new(&generators, DsdtScope::PciRoot) + .to_aml_bytes(&mut got); + aml::Name::new("TEST".into(), &"PciRoot").to_aml_bytes(&mut expected); + assert_eq!(expected, got); + + got.clear(); + expected.clear(); + + // Filter by Lpc. + DsdtGeneratorAml::new(&generators, DsdtScope::Lpc) + .to_aml_bytes(&mut got); + aml::Name::new("TEST".into(), &"Lpc").to_aml_bytes(&mut expected); + assert_eq!(expected, got); + } + + #[test] + fn dsdt_valid_aml() { + let config = DsdtConfig { + generators: &[ + &MockDsdtGenerator { scope: DsdtScope::SystemBus }, + &MockDsdtGenerator { scope: DsdtScope::PciRoot }, + &MockDsdtGenerator { scope: DsdtScope::Lpc }, + ], + }; + let dsdt = Dsdt::new(config); + let mut aml = Vec::new(); + dsdt.to_aml_bytes(&mut aml); + + // Look for key elements. + assert!(aml.windows(4).any(|w| w == b"_SB_")); + assert!(aml.windows(4).any(|w| w == b"PCI0")); + assert!(aml.windows(4).any(|w| w == b"_PRT")); + assert!(aml.windows(4).any(|w| w == b"LPC_")); + } + + #[test] + fn field_references() { + let mut sink = Vec::new(); + + // Validate DWord AddressSpace min, max, and len offsets. + let min = 0xf800_0000_u32; + let max = 0xfffb_ffff_u32; + let len = max - min + 1; + aml::AddressSpace::new_memory( + aml::AddressSpaceCacheable::NotCacheable, + true, + min, + max, + None, + ) + .to_aml_bytes(&mut sink); + assert_eq!( + sink[ADDRESS_SPACE_32_MIN_OFFSET + ..(ADDRESS_SPACE_32_MIN_OFFSET + 4)], + min.to_le_bytes() + ); + assert_eq!( + sink[ADDRESS_SPACE_32_MAX_OFFSET + ..(ADDRESS_SPACE_32_MAX_OFFSET + 4)], + max.to_le_bytes() + ); + assert_eq!( + sink[ADDRESS_SPACE_32_LEN_OFFSET + ..(ADDRESS_SPACE_32_LEN_OFFSET + 4)], + len.to_le_bytes() + ); + + sink.clear(); + + // Validate QWord AddressSpace min, max, and len offsets. + let min = 0x0080_0000_0000_u64; + let max = 0x0fff_ffff_ffff_u64; + let len = max - min + 1; + aml::AddressSpace::new_memory( + aml::AddressSpaceCacheable::Cacheable, + true, + min, + max, + None, + ) + .to_aml_bytes(&mut sink); + assert_eq!( + sink[ADDRESS_SPACE_64_MIN_OFFSET + ..(ADDRESS_SPACE_64_MIN_OFFSET + 8)], + min.to_le_bytes() + ); + assert_eq!( + sink[ADDRESS_SPACE_64_MAX_OFFSET + ..(ADDRESS_SPACE_64_MAX_OFFSET + 8)], + max.to_le_bytes() + ); + assert_eq!( + sink[ADDRESS_SPACE_64_LEN_OFFSET + ..(ADDRESS_SPACE_64_LEN_OFFSET + 8)], + len.to_le_bytes() + ); + + sink.clear(); + + // Validate Interrupt interrupt list offset. + aml::Interrupt::new(true, false, false, true, vec![0x05]) + .to_aml_bytes(&mut sink); + assert_eq!( + sink[INTERRUPT_LIST_OFFSET..(INTERRUPT_LIST_OFFSET + 4)], + 0x05_u32.to_le_bytes() + ); + + sink.clear(); + + // Validate FWDT offset address field offset. + Ssdt::new(0xabc).to_aml_bytes(&mut sink); + assert_eq!( + sink[SSDT_FWDT_ADDR_OFFSET + ..(SSDT_FWDT_ADDR_OFFSET + SSDT_FWDT_ADDR_LEN)], + 0xabc_u32.to_le_bytes() + ); + } +} diff --git a/lib/propolis/src/firmware/acpi/facs.rs b/lib/propolis/src/firmware/acpi/facs.rs new file mode 100644 index 000000000..6a6bb6a36 --- /dev/null +++ b/lib/propolis/src/firmware/acpi/facs.rs @@ -0,0 +1,31 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Generates a FACS ACPI table for an instance. +//! +//! The [`Facs`] struct implements the `Aml` trait of the `acpi_tables` crate +//! and can write the AML bytecode to any AmlSink, like a `Vec`. + +use acpi_tables::{facs, Aml, AmlSink}; + +/// The FACS table stores information about the firmware. +/// +/// +pub struct Facs {} + +impl Facs { + pub fn new() -> Self { + Self {} + } +} + +// The acpi_tables crate generates version 1 of the FACS table while the +// original static EDK2 table was version 0. The only difference is the +// addition of the X_Firmware_Waking_Vector field, which is not used by +// Propolis. +impl Aml for Facs { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + facs::FACS::new().to_aml_bytes(sink); + } +} diff --git a/lib/propolis/src/firmware/acpi/fadt.rs b/lib/propolis/src/firmware/acpi/fadt.rs new file mode 100644 index 000000000..8c2e63e00 --- /dev/null +++ b/lib/propolis/src/firmware/acpi/fadt.rs @@ -0,0 +1,187 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Generates a FADT/FACP ACPI table for an instance. +//! +//! The [`Fadt`] struct implements the `Aml` trait of the `acpi_tables` crate +//! and can write the AML bytecode to any AmlSink, like a `Vec`. + +use super::{GPE0_BLK_ADDR, GPE0_BLK_LEN, OEM_ID, OEM_REVISION, OEM_TABLE_ID}; +use crate::hw::{chipset::i440fx, pci}; +use acpi_tables::{ + // Use version 3 to keep FADT table consistent with the original EDK2 + // static tables. The acpi_tables crate also generates the MADT table using + // revision 1, which is only compatible with FADT up to version 3. + // + // https://github.com/fwts/fwts/blob/3e05ba9c2640a85cac1f408a423d25e712677fa1/src/acpi/madt/madt.c#L30 + fadt_3::{FADTBuilder, Flags}, + gas::{AccessSize, AddressSpace, GAS}, + Aml, + AmlSink, +}; + +// Byte offset and length of fields that need to be referenced during table +// generation. +pub const FADT_FACS_OFFSET: usize = 36; +pub const FADT_FACS_LEN: usize = 4; + +pub const FADT_DSDT_OFFSET: usize = 40; +pub const FADT_DSDT_LEN: usize = 4; + +pub const FADT_X_DSDT_OFFSET: usize = 140; +pub const FADT_X_DSDT_LEN: usize = 8; + +// Values used to populate the FADT table. +const PM1A_CNT_BLK_ADDR: u16 = i440fx::PMBASE_DEFAULT + 0x04; +const PM_TMR_BLK_ADDR: u16 = i440fx::PMBASE_DEFAULT + 0x08; + +const PM1A_EVT_BLK_LEN: u8 = 4; +const PM1A_CNT_BLK_LEN: u8 = 2; +const PM_TMR_BLK_LEN: u8 = 4; + +// Represents a bit flag for the FADT IA-PC boot architecture flags. +// +// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#ia-pc-boot-architecture-flags +bitflags! { + pub struct FadtIaPcBootArchFlags: u16 { + const LEGACY_DEVICES = 1 << 0; + const ARCH_8042 = 1 << 1; + const VGA_NOT_PRESENT = 1 << 2; + const MSI_NOT_SUPPORTED = 1 << 3; + const PCIE_ASPM_CONTROLS = 1 << 4; + const CMOS_RTC_NOT_PRESENT = 1 << 5; + } +} + +/// The FADT table stores fixed hardware ACPI information. +/// +/// +pub struct Fadt { + facs_offset: u32, + dsdt_offset: u32, +} + +impl Fadt { + pub fn new(facs_offset: u32, dsdt_offset: u32) -> Self { + Self { facs_offset, dsdt_offset } + } +} + +impl Aml for Fadt { + /// Generates AML code for the FADT table. Refer to ACPI specification sec. + /// 5.2.9 "Fixed ACPI Description Table (FADT)" for more information about + /// the individual fields. + /// + /// The current values are retained from the original EDK2 static tables. + /// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiTables/Platform.h#L25-L56 + /// + /// fwts reports 1 high failure for this table: + /// - fadt: FADT X_GPE0_BLK Access width 0x00 but it should be 1 (byte access). + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + let mut fadt = FADTBuilder::new(*OEM_ID, *OEM_TABLE_ID, OEM_REVISION) + .firmware_ctrl_32(self.facs_offset) + .dsdt_32(self.dsdt_offset) + .dsdt_64(self.dsdt_offset as u64) + .flag(Flags::Wbinvd) + .flag(Flags::ProcC1) + .flag(Flags::SlpButton) + .flag(Flags::RtcS4) + .flag(Flags::TmrValExt) + .flag(Flags::ResetRegSup); + + fadt.sci_int = (i440fx::SCI_IRQ as u16).into(); + // Propolis doesn't currently handle this I/O port, but its value is + // retained from the original EDK2 tables for consistency. It should be + // set to zero in the future to disable System Management mode. + fadt.smi_cmd = 0xb2.into(); + fadt.acpi_enable = 0xf1; + fadt.acpi_disable = 0xf0; + + fadt.pm1a_evt_blk = (i440fx::PMBASE_DEFAULT as u32).into(); + fadt.pm1a_cnt_blk = (PM1A_CNT_BLK_ADDR as u32).into(); + fadt.pm_tmr_blk = (PM_TMR_BLK_ADDR as u32).into(); + fadt.gpe0_blk = (GPE0_BLK_ADDR as u32).into(); + + fadt.pm1_evt_len = PM1A_EVT_BLK_LEN; + fadt.pm1_cnt_len = PM1A_CNT_BLK_LEN; + fadt.pm_tmr_len = PM_TMR_BLK_LEN; + fadt.gpe0_blk_len = GPE0_BLK_LEN; + + // Disable C2 support. From the ACPI spec.: + // "A value > 100 indicates the system does not support a C2 state." + fadt.p_lvl2_lat = 101.into(); + + // Disable C3 support. From the ACPI spec.: + // "A value > 1000 indicates the system does not support a C3 state." + fadt.p_lvl3_lat = 1001.into(); + + let iapc_boot_arch = FadtIaPcBootArchFlags::empty(); + fadt.iapc_boot_arch = iapc_boot_arch.bits().into(); + + fadt.reset_reg = GAS::new( + AddressSpace::SystemIo, + u8::BITS as u8, + 0, + AccessSize::Undefined, + pci::bits::PORT_ACPI_RESET_ADDR, + ); + fadt.reset_value = pci::bits::PORT_ACPI_RESET_VALUE; + + fadt.x_pm1a_evt_blk = GAS::new( + AddressSpace::SystemIo, + PM1A_EVT_BLK_LEN * 8, + 0, + AccessSize::Undefined, + i440fx::PMBASE_DEFAULT as u64, + ); + fadt.x_pm1a_cnt_blk = GAS::new( + AddressSpace::SystemIo, + PM1A_CNT_BLK_LEN * 8, + 0, + AccessSize::Undefined, + PM1A_CNT_BLK_ADDR as u64, + ); + + fadt.x_pm_tmr_blk = GAS::new( + AddressSpace::SystemIo, + PM_TMR_BLK_LEN * 8, + 0, + AccessSize::Undefined, + PM_TMR_BLK_ADDR as u64, + ); + + fadt.x_gpe0_blk = GAS::new( + AddressSpace::SystemIo, + GPE0_BLK_LEN * 8, + 0, + AccessSize::Undefined, + GPE0_BLK_ADDR as u64, + ); + + fadt.finalize().to_aml_bytes(sink); + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn field_references() { + let mut sink = Vec::new(); + Fadt::new(0x0abc, 0x0def).to_aml_bytes(&mut sink); + assert_eq!( + sink[FADT_FACS_OFFSET..(FADT_FACS_OFFSET + FADT_FACS_LEN)], + 0x0abc_u32.to_le_bytes() + ); + assert_eq!( + sink[FADT_DSDT_OFFSET..(FADT_DSDT_OFFSET + FADT_DSDT_LEN)], + 0x0000_u32.to_le_bytes() // Calling dsdt_64 zeros the 32-bit field. + ); + assert_eq!( + sink[FADT_X_DSDT_OFFSET..(FADT_X_DSDT_OFFSET + FADT_X_DSDT_LEN)], + 0x0def_u64.to_le_bytes() + ); + } +} diff --git a/lib/propolis/src/firmware/acpi/file_sink.rs b/lib/propolis/src/firmware/acpi/file_sink.rs new file mode 100644 index 000000000..ff49de710 --- /dev/null +++ b/lib/propolis/src/firmware/acpi/file_sink.rs @@ -0,0 +1,32 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! AmlSink that writes AML bytecode to a file. + +use acpi_tables::AmlSink; +use std::fs::File; +use std::io::prelude::*; + +pub struct FileSink { + f: File, +} + +/// AmlSink to write AML bytecode to a file. +/// +/// Panics if the file can't be created or written to. +impl FileSink { + pub fn new(file: &str) -> Self { + let path = std::path::Path::new(file); + let prefix = path.parent().unwrap(); + std::fs::create_dir_all(prefix).unwrap(); + let f = File::create(file).unwrap(); + Self { f } + } +} + +impl AmlSink for FileSink { + fn byte(&mut self, byte: u8) { + self.f.write_all(&[byte]).unwrap(); + } +} diff --git a/lib/propolis/src/firmware/acpi/madt.rs b/lib/propolis/src/firmware/acpi/madt.rs new file mode 100644 index 000000000..41114fbd8 --- /dev/null +++ b/lib/propolis/src/firmware/acpi/madt.rs @@ -0,0 +1,105 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Generates a MADT/APIC ACPI table for an instance. +//! +//! The [`Madt`] struct implements the `Aml` trait of the `acpi_tables` crate +//! and can write the AML bytecode to any AmlSink, like a `Vec`. + +use super::{ + IO_APIC_ADDR, LOCAL_APIC_ADDR, OEM_ID, OEM_REVISION, OEM_TABLE_ID, + PCI_LINK_IRQS, +}; +use acpi_tables::{madt, Aml, AmlSink}; + +const IO_APIC_ID: u8 = 0x02; +const IO_APIC_GSI_BASE: u32 = 0x0000; + +const TIMER_IRQ: u8 = 0; +const TIMER_GSI: u32 = 2; + +const LOCAL_APIC_INT_NUMBER: u8 = 1; + +pub struct MadtConfig { + pub num_cpus: u8, +} + +/// The MADT/APIC table describes the interrupts for the entire system. +/// +/// +pub struct Madt<'a> { + config: &'a MadtConfig, +} + +impl<'a> Madt<'a> { + pub fn new(config: &'a MadtConfig) -> Self { + Self { config } + } +} + +// Values retained from the original EDK2 static tables. +// +// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiTables/Madt.aslc +// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiPlatformDxe/Qemu.c#L58 +// +// fwts reports 3 medium failures for this table: +// - madt: LAPIC has no matching processor UID 0 +// - madt: LAPIC has no matching processor UID 1 +// - madt: LAPICNMI has no matching processor UID 255 +impl<'a> Aml for Madt<'a> { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + let mut table = madt::MADT::new( + *OEM_ID, + *OEM_TABLE_ID, + OEM_REVISION, + madt::LocalInterruptController::Address(LOCAL_APIC_ADDR), + ) + .pc_at_compat(); + + // Processor Local APIC. + // https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#processor-local-apic-structure + for i in 0..self.config.num_cpus { + table.add_structure(madt::ProcessorLocalApic::new( + i, + i, + madt::EnabledStatus::Enabled, + )); + } + + // I/O APIC. + // https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#i-o-apic-structure + table.add_structure(madt::IoApic::new( + IO_APIC_ID, + IO_APIC_ADDR, + IO_APIC_GSI_BASE, + )); + + // Interrupt Source Overrides. + // https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#interrupt-source-override-structure + table.add_structure(madt::InterruptSourceOverride::new( + TIMER_IRQ, TIMER_GSI, + )); + + // Set level-triggered and active high for all PCI link targets. + PCI_LINK_IRQS.iter().for_each(|&i| { + table.add_structure( + madt::InterruptSourceOverride::new(i, i as u32) + .level_triggered() + .active_high(), + ); + }); + + // Local APIC NMI. + // https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#local-apic-nmi-structure + // + // Supporting more than 255 vCPUs will require additional work. + // Refer to propolis#956 for more information. + table.add_structure(madt::ProcessorLocalApicNmi::new( + 0xff, // Apply to all processors. + LOCAL_APIC_INT_NUMBER, + )); + + table.to_aml_bytes(sink); + } +} diff --git a/lib/propolis/src/firmware/acpi/mod.rs b/lib/propolis/src/firmware/acpi/mod.rs new file mode 100644 index 000000000..e3a41165b --- /dev/null +++ b/lib/propolis/src/firmware/acpi/mod.rs @@ -0,0 +1,82 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! ACPI table and AML bytecode generation. +//! +//! Historically, Propolis used static ACPI tables generated by EDK2. This +//! required making code changes in two separate repositories to ensure the +//! tables matched the expected hardware platform. +//! +//! Later releases of EDK2 dropped support for the built-in tables and required +//! the hypervisor to generate their own tables and load them into the guest +//! firmware using the QEMU Firmware Configuration Device (fw_cfg). +//! +//! This module contains the code that generates these ACPI tables, but it is +//! currently focused on retaining compatibility with the original static EDK2 +//! tables. The original tables can be found in the EDK2 fork repository. +//! +//! https://github.com/oxidecomputer/edk2/tree/propolis/edk2-stable202105/OvmfPkg/AcpiTables + +use crate::hw::chipset::i440fx; + +pub mod aml; +pub mod dsdt; +pub mod facs; +pub mod fadt; +pub mod file_sink; +pub mod madt; +pub mod rsdp; +pub mod xsdt; + +pub use dsdt::{ + Dsdt, DsdtConfig, DsdtGenerator, DsdtScope, Ssdt, SSDT_FWDT_ADDR_LEN, + SSDT_FWDT_ADDR_OFFSET, +}; +pub use facs::Facs; +pub use fadt::{ + Fadt, FADT_DSDT_LEN, FADT_DSDT_OFFSET, FADT_FACS_LEN, FADT_FACS_OFFSET, + FADT_X_DSDT_LEN, FADT_X_DSDT_OFFSET, +}; +pub use file_sink::FileSink; +pub use madt::{Madt, MadtConfig}; +pub use rsdp::{ + Rsdp, RSDP_EXTENDED_CHECKSUM_OFFSET, RSDP_EXTENDED_TABLE_LEN, + RSDP_V1_CHECKSUM_OFFSET, RSDP_V1_TABLE_LEN, RSDP_XSDT_ADDR_LEN, + RSDP_XSDT_ADDR_OFFSET, +}; +pub use xsdt::{Xsdt, XSDT_HEADER_LEN}; + +// Values used to reference table checksums to recompute them after values are +// changed during table generation. +pub const TABLE_HEADER_CHECKSUM_OFFSET: usize = 9; +pub const TABLE_HEADER_CHECKSUM_LEN: usize = 1; + +// Internal values shared across tables. + +// Values inherited from the original EDK2 static tables. They could be set to +// Propolis-specific values in the future. +const OEM_ID: &[u8; 6] = b"OVMF "; +const OEM_TABLE_ID: &[u8; 8] = b"OVMFEDK2"; +const OEM_REVISION: u32 = 0x20130221; + +// IRQs used to handle PCI interrupts. +const PCI_LINK_IRQS: [u8; 4] = [0x05, i440fx::SCI_IRQ, 0x0a, 0x0b]; + +// VIOAPIC_BASE in bhyve. +const IO_APIC_ADDR: u32 = 0xfec0_0000; +// VIOAPIC_SIZE in bhyve. +const IO_APIC_LEN: u32 = 0x1000; + +// DEFAULT_APIC_BASE in bhyve source code. +const LOCAL_APIC_ADDR: u32 = 0xfee0_0000; +// PAGE_SIZE (4096) in bhyve, but this value was inherited from EDK2. +const LOCAL_APIC_LEN: u32 = 0x10_0000; + +// Register used for PCI hotplug. This value was chosen to match QEMU and the +// original EDK2 ACPI tables. +// +// Refer to the `piix4_acpi_system_hot_add_init` function in QEMU for more +// information. +const GPE0_BLK_ADDR: u16 = 0xafe0; +const GPE0_BLK_LEN: u8 = 4; diff --git a/lib/propolis/src/firmware/acpi/rsdp.rs b/lib/propolis/src/firmware/acpi/rsdp.rs new file mode 100644 index 000000000..487d131da --- /dev/null +++ b/lib/propolis/src/firmware/acpi/rsdp.rs @@ -0,0 +1,69 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Generates a RSDP ACPI table for an instance. +//! +//! The [`Rsdp`] struct implements the `Aml` trait of the `acpi_tables` crate +//! and can write the AML bytecode to any AmlSink, like a `Vec`. + +use super::OEM_ID; +use acpi_tables::{rsdp, Aml, AmlSink}; + +// Byte offset and length of fields that need to be referenced during table +// generation. +pub const RSDP_XSDT_ADDR_OFFSET: usize = 24; +pub const RSDP_XSDT_ADDR_LEN: usize = 8; + +// The RSDP table has two checksums fields. +// +// - RSDP_V1_CHECKSUM_* points to the original checksum field defined in the +// ACPI 1.0 specification. +// +// - RSDP_EXTENDED_CHECKSUM_* points to the new checksum field that includes +// the entire table. +pub const RSDP_V1_CHECKSUM_OFFSET: usize = 8; +pub const RSDP_V1_TABLE_LEN: usize = 20; + +pub const RSDP_EXTENDED_CHECKSUM_OFFSET: usize = 32; +pub const RSDP_EXTENDED_TABLE_LEN: usize = 36; + +/// The RSDP table is the root table the operating system loads first to +/// discover the other tables. +/// +/// +pub struct Rsdp { + xsdt_addr: u64, +} + +impl Rsdp { + pub fn new(xsdt_addr: u64) -> Self { + Self { xsdt_addr } + } +} + +impl Aml for Rsdp { + // OVMF ignores the RSDP table loaded via fw_cfg and instead it generates + // its own, so changes here will not appear to the guest when using OVMF. + // + // https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiPlatformDxe/QemuFwCfgAcpi.c#L891-L899 + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + rsdp::Rsdp::new(*OEM_ID, self.xsdt_addr).to_aml_bytes(sink); + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn field_references() { + let mut sink = Vec::new(); + Rsdp::new(0x0abc_def0).to_aml_bytes(&mut sink); + assert_eq!( + sink[RSDP_XSDT_ADDR_OFFSET + ..(RSDP_XSDT_ADDR_OFFSET + RSDP_XSDT_ADDR_LEN)], + 0x0abc_def0_u64.to_le_bytes() + ); + } +} diff --git a/lib/propolis/src/firmware/acpi/xsdt.rs b/lib/propolis/src/firmware/acpi/xsdt.rs new file mode 100644 index 000000000..a88bce821 --- /dev/null +++ b/lib/propolis/src/firmware/acpi/xsdt.rs @@ -0,0 +1,40 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Generates an XSDT ACPI table for an instance. +//! +//! The [`Xsdt`] struct implements the `Aml` trait of the `acpi_tables` crate +//! and can write the AML bytecode to any AmlSink, like a `Vec`. + +use super::{OEM_ID, OEM_REVISION, OEM_TABLE_ID}; +use acpi_tables::{xsdt, Aml, AmlSink}; + +// Byte offset and length of fields that need to be referenced during table +// generation. +pub const XSDT_HEADER_LEN: usize = 36; + +/// The XSDT table provides the addresses of additional tables. +/// +/// +pub struct Xsdt { + entries: Vec, +} + +impl Xsdt { + pub fn new(entries: Vec) -> Self { + Self { entries } + } +} + +impl Aml for Xsdt { + // OVMF ignores the XSDT table loaded via fw_cfg and instead it generates + // its own, so changes here will not appear to the guest when using OVMF. + // + // https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiPlatformDxe/QemuFwCfgAcpi.c#L891-L899 + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + let mut table = xsdt::XSDT::new(*OEM_ID, *OEM_TABLE_ID, OEM_REVISION); + self.entries.iter().for_each(|e| table.add_entry(*e)); + table.to_aml_bytes(sink); + } +} diff --git a/lib/propolis/src/firmware/mod.rs b/lib/propolis/src/firmware/mod.rs index 460e395ee..e1e598d38 100644 --- a/lib/propolis/src/firmware/mod.rs +++ b/lib/propolis/src/firmware/mod.rs @@ -2,4 +2,5 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +pub mod acpi; pub mod smbios; diff --git a/lib/propolis/src/hw/chipset/i440fx.rs b/lib/propolis/src/hw/chipset/i440fx.rs index beb336324..beafb08a1 100644 --- a/lib/propolis/src/hw/chipset/i440fx.rs +++ b/lib/propolis/src/hw/chipset/i440fx.rs @@ -133,14 +133,14 @@ impl IrqConfig { } } -const PIR_OFFSET: usize = 0x60; -const PIR_LEN: usize = 4; -const PIR_END: usize = PIR_OFFSET + PIR_LEN; +pub const PIR_OFFSET: usize = 0x60; +pub const PIR_LEN: usize = 4; +pub const PIR_END: usize = PIR_OFFSET + PIR_LEN; -const PIR_MASK_DISABLE: u8 = 0x80; +pub const PIR_MASK_DISABLE: u8 = 0x80; const PIR_MASK_IRQ: u8 = 0x0f; -const SCI_IRQ: u8 = 0x9; +pub const SCI_IRQ: u8 = 0x9; fn valid_pir_irq(irq: u8) -> bool { // Existing ACPI tables allow 3-7, 9-12, 14-15 @@ -553,8 +553,8 @@ impl MigrateMulti for Piix3Lpc { const PMCFG_OFFSET: usize = 0x40; const PMCFG_LEN: usize = 0x98; -const PMBASE_DEFAULT: u16 = 0xb000; -const PMBASE_LEN: u16 = 0x40; +pub const PMBASE_DEFAULT: u16 = 0xb000; +pub const PMBASE_LEN: u16 = 0x40; const SMBBASE_DEFAULT: u16 = 0xb100; // const SMBBASE_LEN: u16 = 0x40; diff --git a/lib/propolis/src/hw/pci/bits.rs b/lib/propolis/src/hw/pci/bits.rs index e0b6f5c24..624caa5c2 100644 --- a/lib/propolis/src/hw/pci/bits.rs +++ b/lib/propolis/src/hw/pci/bits.rs @@ -78,6 +78,20 @@ pub const LEN_PCI_CONFIG_ADDR: u16 = 4; pub const PORT_PCI_CONFIG_DATA: u16 = 0xcfc; pub const LEN_PCI_CONFIG_DATA: u16 = 4; +/// I/O port used for the ACPI reset register. +/// +/// Although not PCI-specific, the I/O port used by PIIX3 for the reset +/// register unfortunately falls within the PCI configuration address range. +/// +/// Refer to the following materials for more information: +/// - ACPI sec. 5.2.9 "Fixed ACPI Description Table (FADT)". +/// - ACPI sec. 4.8.4.6 "Reset Register". +/// - Linux source code, functions `native_machine_emergency_restart()` and +/// `acpi_reboot()`. +/// - PIIX3 datasheet sec. 2.5.4.3 "RC-Reset Control Register" +pub const PORT_ACPI_RESET_ADDR: u64 = 0xcf9; +pub const PORT_ACPI_RESET_VALUE: u8 = 0b0110; // Reset CPU + System Reset + /// The minimum number of buses a single ECAM region can address. The PCIe spec /// requires that at least one bit of the ECAM address space be used to specify /// a bus number (see PCIe base spec rev 5.0 table 7-1). diff --git a/lib/propolis/src/hw/ps2/ctrl.rs b/lib/propolis/src/hw/ps2/ctrl.rs index a201a685d..a7ad9cbe8 100644 --- a/lib/propolis/src/hw/ps2/ctrl.rs +++ b/lib/propolis/src/hw/ps2/ctrl.rs @@ -8,11 +8,13 @@ use std::mem::replace; use std::sync::{Arc, Mutex}; use crate::common::*; +use crate::firmware::acpi; use crate::hw::ibmpc; use crate::intr_pins::IntrPin; use crate::migrate::*; use crate::pio::{PioBus, PioFn}; +use acpi_tables::{aml, Aml, AmlSink}; use rfb::proto::KeyEvent; use super::keyboard::KeyEventRep; @@ -605,6 +607,9 @@ impl Lifecycle for PS2Ctrl { fn migrate(&self) -> Migrator<'_> { Migrator::Single(self) } + fn as_dsdt_generator(&self) -> Option<&dyn acpi::DsdtGenerator> { + Some(self) + } } impl MigrateSingle for PS2Ctrl { fn export( @@ -1090,6 +1095,34 @@ impl Default for PS2Mouse { } } +impl acpi::DsdtGenerator for PS2Ctrl { + fn dsdt_scope(&self) -> acpi::DsdtScope { + acpi::DsdtScope::Lpc + } +} + +impl Aml for PS2Ctrl { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + aml::Device::new( + "PS2K".into(), + vec![ + &acpi::aml::names::hid(&aml::EISAName::new( + acpi::aml::devids::IBM_ENHANCED_KEYBOARD, + )), + &acpi::aml::names::cid(&aml::EISAName::new( + acpi::aml::devids::MICROSOFT_RESERVED_KEYBOARD, + )), + &acpi::aml::names::crs(&aml::ResourceTemplate::new(vec![ + &acpi::aml::io_port(ibmpc::PORT_PS2_DATA, 0x00, 0x01), + &acpi::aml::io_port(ibmpc::PORT_PS2_CMD_STATUS, 0x00, 0x01), + &aml::IrqNoFlags::new(ibmpc::IRQ_PS2_PRI), + ])), + ], + ) + .to_aml_bytes(sink); + } +} + pub mod migrate { use crate::migrate::*; diff --git a/lib/propolis/src/hw/qemu/debug.rs b/lib/propolis/src/hw/qemu/debug.rs index c55bc74ff..46ed3d1eb 100644 --- a/lib/propolis/src/hw/qemu/debug.rs +++ b/lib/propolis/src/hw/qemu/debug.rs @@ -8,7 +8,7 @@ use crate::chardev::{BlockingSource, BlockingSourceConsumer, ConsumerCell}; use crate::common::*; use crate::pio::{PioBus, PioFn}; -const QEMU_DEBUG_IOPORT: u16 = 0x0402; +pub const QEMU_DEBUG_IOPORT: u16 = 0x0402; const QEMU_DEBUG_IDENT: u8 = 0xe9; pub struct QemuDebugPort { diff --git a/lib/propolis/src/hw/qemu/fwcfg.rs b/lib/propolis/src/hw/qemu/fwcfg.rs index a86fd0515..5faf87f8f 100644 --- a/lib/propolis/src/hw/qemu/fwcfg.rs +++ b/lib/propolis/src/hw/qemu/fwcfg.rs @@ -1055,7 +1055,9 @@ mod test { pub mod formats { use super::Entry; + use crate::firmware::acpi; use crate::hw::pci; + use acpi_tables::Aml; use zerocopy::{Immutable, IntoBytes}; /// A type for a range described in an E820 map entry. @@ -1301,4 +1303,555 @@ pub mod formats { assert_eq!(&expected[..], &entries[..]); } } + + #[derive(thiserror::Error, Debug)] + pub enum AcpiTablesError { + #[error( + "invalid PCI window range, base address ({0:#04x}) must be lower than end ({1:#04x})" + )] + InvalidPCIWindowRange(u64, u64), + } + + /// Instance configuration that are relevant when building the ACPI tables. + pub struct AcpiConfig<'a> { + pub num_cpus: u8, + pub pci_window_32: PciWindow, + pub pci_window_64: PciWindow, + pub dsdt_generators: &'a [&'a dyn acpi::DsdtGenerator], + } + + /// An inclusive range of address to be used for PCI MMIO. + #[derive(PartialEq)] + pub struct PciWindow { + base: u64, + end: u64, + } + impl PciWindow { + pub fn new(base: u64, end: u64) -> Result { + // Prevent the creation of zero and one-byte length windows. + // In theory they are valid windows (except for the empty window), + // but in practice they are not windows we expect to create with + // this constructor.. + if base >= end { + return Err(AcpiTablesError::InvalidPCIWindowRange(base, end)); + } + Ok(Self { base, end }) + } + + pub fn empty() -> Self { + Self { base: 0, end: 0 } + } + + pub fn len(&self) -> u64 { + if *self == PciWindow::empty() { + return 0; + } + // Values are checked on creation to ensure the subtraction doesn't + // underflow. + self.end - self.base + 1 + } + } + + #[cfg(test)] + mod test_pci_window { + use super::*; + + #[test] + fn basic() { + let w = PciWindow::new(0, 0); + assert!(w.is_err()); + + let w = PciWindow::new(100, 100); + assert!(w.is_err()); + + let w = PciWindow::empty(); + assert_eq!(w.len(), 0); + + let w = PciWindow::new(0, 100).unwrap(); + assert_eq!(w.len(), 101); + } + } + + /// The resulting values to be loaded into QEMU fw_cfg data. + pub struct AcpiTables { + pub tables: Entry, + pub rsdp: Entry, + pub table_loader: Entry, + } + + const FW_CFG_ACPI_TABLES_PATH: &str = "etc/acpi/tables"; + const FW_CFG_ACPI_RSDP_PATH: &str = "etc/acpi/rsdp"; + + /// Builds ACPI tables for an instance and provide three blobs of data that + /// can be loaded into the instance as QEMU firmware configuration: the + /// tables themselves, the RSDP table, and a [`TableLoader`] with commands + /// to run when the instance boots. + /// + /// The ACPI tables are organized in a hierarchy, with some tables having + /// fields that hold the address of another table. + /// + /// ```text + /// ┌─────────┐ ┌─────────┐ ┌─────────────┐ + /// │ RSDP │ ┌─▶│ XSDT │ ┌──▶│ FADT │ + /// ├─────────┤ │ ├─────────┤ │ ├─────────────┤ ┌──────────┐ + /// │ Pointer │ │ │ Entry │──┘ │ ........... │ ┌─▶│ FACS │ + /// ├─────────┤ │ ├─────────┤ │FIRMWARE_CTRL│─┘ └──────────┘ + /// │ Pointer │─┘ │ Entry │ │ ........... │ ┌──────────┐ + /// └─────────┘ ├─────────┤ │ DSDT │─┬─▶│ DSDT │ + /// │ ... │ │ ........... │ │ ├──────────┤ + /// ├─────────┤ │ X_DSDT │─┘ │Definition│ + /// │ Entry │───┐ │ ........... │ │ Blocks │ + /// ├─────────┤ │ └─────────────┘ └──────────┘ + /// │ Entry │─┐ │ ┌──────────┐ + /// └─────────┘ │ └─▶│ SSDT │ + /// │ ├──────────┤ + /// │ │Definition│ + /// │ │ Blocks │ + /// │ └──────────┘ + /// │ ┌────────────────────┐ + /// └───▶│ MADT │ + /// ├────────────────────┤ + /// │Interrupt Controller│ + /// │ Structures │ + /// └────────────────────┘ + /// ``` + /// Adapted from + /// + /// These addresses are only know at boot time, so each reference has a + /// corresponding [`AddPointerCommand`] that the firmware executes on boot. + /// And since the table has been modified, they also need a + /// [`AddChecksumCommand`] to recalculate the final table checksum. + pub struct AcpiTablesBuilder<'a> { + config: &'a AcpiConfig<'a>, + tables: Vec, + rsdp: Vec, + loader: TableLoader, + } + + impl<'a> AcpiTablesBuilder<'a> { + pub fn new(config: &'a AcpiConfig) -> Self { + Self { + config, + tables: Vec::new(), + rsdp: Vec::new(), + loader: TableLoader::new(), + } + } + + pub fn build(mut self) -> AcpiTables { + self.loader.add_allocate( + FW_CFG_ACPI_TABLES_PATH, + 64, + AllocZone::High, + ); + self.loader.add_allocate( + FW_CFG_ACPI_RSDP_PATH, + 16, + AllocZone::FSeg, + ); + + let facs_offset = self.add_facs(); + let dsdt_offset = self.add_dsdt(); + let fadt_offset = self.add_fadt(facs_offset, dsdt_offset); + + let madt_offset = self.add_madt(); + let ssdt_offset = self.add_ssdt(); + + // OVMF actually ignores the XSDT and RSDP tables provided via + // fw_cfg and always generates its own versions, but include them + // here regardless for completeness. + // + // https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiPlatformDxe/QemuFwCfgAcpi.c#L891-L899 + let xsdt_entries = vec![fadt_offset, madt_offset, ssdt_offset]; + let xsdt_offset = self.add_xsdt(xsdt_entries); + + self.add_rsdp(xsdt_offset); + + AcpiTables { + tables: Entry::Bytes(self.tables), + rsdp: Entry::Bytes(self.rsdp), + table_loader: self.loader.finish(), + } + } + + fn add_facs(&mut self) -> usize { + let facs = acpi::Facs::new(); + let facs_offset = self.tables.len(); + facs.to_aml_bytes(&mut self.tables); + + #[cfg(feature = "acpi-debug")] + facs.to_aml_bytes(&mut acpi::FileSink::new("./acpi/facs.dat")); + + facs_offset + } + + fn add_dsdt(&mut self) -> usize { + let dsdt_config = + acpi::DsdtConfig { generators: self.config.dsdt_generators }; + let dsdt = acpi::Dsdt::new(dsdt_config); + let dsdt_offset = self.tables.len(); + dsdt.to_aml_bytes(&mut self.tables); + + #[cfg(feature = "acpi-debug")] + dsdt.to_aml_bytes(&mut acpi::FileSink::new("./acpi/dsdt.dat")); + + dsdt_offset + } + + fn add_ssdt(&mut self) -> usize { + // Add data for the FWDT OperationRegion declared in the SSDT + // table. + let fwdt_data_offset = self.tables.len(); + [ + self.config.pci_window_32.base, + self.config.pci_window_32.end, + self.config.pci_window_32.len(), + self.config.pci_window_64.base, + self.config.pci_window_64.end, + self.config.pci_window_64.len(), + ] + .iter() + .for_each(|data| { + self.tables.extend_from_slice(&data.to_le_bytes()); + }); + + let ssdt = acpi::Ssdt::new(fwdt_data_offset); + let ssdt_offset = self.tables.len(); + ssdt.to_aml_bytes(&mut self.tables); + + #[cfg(feature = "acpi-debug")] + ssdt.to_aml_bytes(&mut acpi::FileSink::new("./acpi/ssdt.dat")); + + // Mark the FWDT OperationRegion offset field as a pointer to the + // FWDT data. + self.loader.add_pointer( + FW_CFG_ACPI_TABLES_PATH, + FW_CFG_ACPI_TABLES_PATH, + (ssdt_offset + acpi::SSDT_FWDT_ADDR_OFFSET) as u32, + acpi::SSDT_FWDT_ADDR_LEN as u8, + ); + + // Recalculate checksum after changes. + self.reset_checksum(ssdt_offset); + + ssdt_offset + } + + fn add_fadt( + &mut self, + facs_offset: usize, + dsdt_offset: usize, + ) -> usize { + let fadt = acpi::Fadt::new(facs_offset as u32, dsdt_offset as u32); + let fadt_offset = self.tables.len(); + fadt.to_aml_bytes(&mut self.tables); + + #[cfg(feature = "acpi-debug")] + fadt.to_aml_bytes(&mut acpi::FileSink::new("./acpi/fadt.dat")); + + // Mark the fields that reference other tables as pointers. + [ + (acpi::FADT_FACS_OFFSET, acpi::FADT_FACS_LEN), // FADT -> FACS + (acpi::FADT_DSDT_OFFSET, acpi::FADT_DSDT_LEN), // FADT -> DSDT + (acpi::FADT_X_DSDT_OFFSET, acpi::FADT_X_DSDT_LEN), // FADT -> X_DSDT + ] + .iter() + .for_each(|&(offset, size)| { + self.loader.add_pointer( + FW_CFG_ACPI_TABLES_PATH, + FW_CFG_ACPI_TABLES_PATH, + (fadt_offset + offset) as u32, + size as u8, + ); + }); + + // Recalculate checksum after changes. + self.reset_checksum(fadt_offset); + + fadt_offset + } + + fn add_madt(&mut self) -> usize { + let madt_config = + &acpi::MadtConfig { num_cpus: self.config.num_cpus }; + let madt = acpi::Madt::new(madt_config); + let madt_offset = self.tables.len(); + madt.to_aml_bytes(&mut self.tables); + + #[cfg(feature = "acpi-debug")] + madt.to_aml_bytes(&mut acpi::FileSink::new("./acpi/madt.dat")); + + madt_offset + } + + fn add_xsdt(&mut self, entries: Vec) -> usize { + let xsdt = + acpi::Xsdt::new(entries.iter().map(|e| *e as u64).collect()); + let xsdt_offset = self.tables.len(); + xsdt.to_aml_bytes(&mut self.tables); + + #[cfg(feature = "acpi-debug")] + xsdt.to_aml_bytes(&mut acpi::FileSink::new("./acpi/xsdt.dat")); + + // Mark the table entry fields as pointers. + for i in 0..entries.len() { + // Each entry offset in the overall tables data is: + // XSDT offset + XSDT header length + + // 8 * the number of entries before it. + let offset = xsdt_offset + acpi::XSDT_HEADER_LEN + 8 * i; + + self.loader.add_pointer( + FW_CFG_ACPI_TABLES_PATH, + FW_CFG_ACPI_TABLES_PATH, + offset as u32, + size_of::() as u8, + ); + } + + // Recalculate checksum after changes. + self.reset_checksum(xsdt_offset); + + xsdt_offset + } + + fn add_rsdp(&mut self, xsdt_offset: usize) { + let rsdp = acpi::Rsdp::new(xsdt_offset as u64); + rsdp.to_aml_bytes(&mut self.rsdp); + + #[cfg(feature = "acpi-debug")] + rsdp.to_aml_bytes(&mut acpi::FileSink::new("./acpi/rsdp.dat")); + + // Mark the field with the XSDT address as pointer. + self.loader.add_pointer( + FW_CFG_ACPI_RSDP_PATH, + FW_CFG_ACPI_TABLES_PATH, + acpi::RSDP_XSDT_ADDR_OFFSET as u32, + acpi::RSDP_XSDT_ADDR_LEN as u8, + ); + + // Recalculate checksums after changes. + self.reset_rsdp_checksum( + acpi::RSDP_V1_CHECKSUM_OFFSET, + acpi::RSDP_V1_TABLE_LEN, + ); + self.reset_rsdp_checksum( + acpi::RSDP_EXTENDED_CHECKSUM_OFFSET, + acpi::RSDP_EXTENDED_TABLE_LEN, + ); + } + + /// Add a command to recompute a RSDP table checksum on boot. + /// + /// It is used when the table is modified during generation or it has + /// commands that will modify them on boot. + /// + /// Must be called after all modifications have been done. + fn reset_rsdp_checksum( + &mut self, + checksum_offset: usize, + table_len: usize, + ) { + let checksum_end = + checksum_offset + acpi::TABLE_HEADER_CHECKSUM_LEN; + + // Zero existing checksum so it doesn't affect the new value. + self.rsdp[checksum_offset..checksum_end].fill(0); + self.loader.add_checksum( + FW_CFG_ACPI_RSDP_PATH, + checksum_offset as u32, + 0, + table_len as u32, + ); + } + + /// Add a command to recompute a table's checksum on boot. + /// + /// It is used when the table is modified during generation or it has + /// commands that will modify them on boot. + /// + /// Must be called after all modifications have been done. + fn reset_checksum(&mut self, table_offset: usize) { + let checksum_start = + table_offset + acpi::TABLE_HEADER_CHECKSUM_OFFSET; + let checksum_end = table_offset + + acpi::TABLE_HEADER_CHECKSUM_OFFSET + + acpi::TABLE_HEADER_CHECKSUM_LEN; + + // Zero existing checksum so it doesn't affect the new value. + self.tables[checksum_start..checksum_end].fill(0); + self.loader.add_checksum( + FW_CFG_ACPI_TABLES_PATH, + checksum_start as u32, + table_offset as u32, + (self.tables.len() - table_offset) as u32, + ); + } + } + + const TABLE_LOADER_FILESZ: usize = 56; + const TABLE_LOADER_COMMAND_SIZE: usize = 128; + + /// Stores commands that will be executed by the EDK2 firmware when the + /// ACPI tables are loaded. + /// + /// Refer to the EDK2 source code for more information on the commands. + /// + /// + pub struct TableLoader { + commands: Vec, + } + + impl TableLoader { + pub fn new() -> Self { + Self { commands: Vec::new() } + } + + pub fn add_allocate( + &mut self, + file: &str, + align: u32, + zone: AllocZone, + ) { + assert!(align.is_power_of_two()); + + let cmd = AllocateCommand { + file: LoaderFileName::new(file), + align, + zone, + }; + self.write_command(CommandType::Allocate, cmd.as_bytes()); + } + + pub fn add_pointer( + &mut self, + dest_file: &str, + src_file: &str, + offset: u32, + size: u8, + ) { + assert!(matches!(size, 1 | 2 | 4 | 8)); + + let cmd = AddPointerCommand { + dest_file: LoaderFileName::new(dest_file), + src_file: LoaderFileName::new(src_file), + offset, + size, + }; + self.write_command(CommandType::AddPointer, cmd.as_bytes()); + } + + pub fn add_checksum( + &mut self, + file: &str, + result_offset: u32, + start: u32, + length: u32, + ) { + let cmd = AddChecksumCommand { + file: LoaderFileName::new(file), + result_offset, + start, + length, + }; + self.write_command(CommandType::AddChecksum, cmd.as_bytes()); + } + + pub fn finish(self) -> Entry { + Entry::Bytes(self.commands) + } + + fn write_command(&mut self, cmd_type: CommandType, payload: &[u8]) { + let start = self.commands.len(); + self.commands.resize(start + TABLE_LOADER_COMMAND_SIZE, 0); + + let cmd_bytes = (cmd_type as u32).to_le_bytes(); + self.commands[start..start + 4].copy_from_slice(&cmd_bytes); + + let payload_start = start + 4; + let payload_end = payload_start + payload.len(); + assert!(payload_end <= start + TABLE_LOADER_COMMAND_SIZE); + self.commands[payload_start..payload_end].copy_from_slice(payload); + } + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoBytes, Immutable)] + #[repr(u8)] + pub enum AllocZone { + High = 0x1, + FSeg = 0x2, + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoBytes, Immutable)] + #[repr(u32)] + enum CommandType { + Allocate = 1, + AddPointer = 2, + AddChecksum = 3, + #[allow(dead_code)] + WritePointer = 4, + } + + #[derive(Clone, IntoBytes, Immutable)] + #[repr(C)] + struct LoaderFileName([u8; TABLE_LOADER_FILESZ]); + impl LoaderFileName { + fn new(name: &str) -> Self { + let bytes = name.as_bytes(); + assert!(bytes.len() < TABLE_LOADER_FILESZ); + + let mut buf = [0u8; TABLE_LOADER_FILESZ]; + buf[..bytes.len()].copy_from_slice(bytes); + Self(buf) + } + } + + #[derive(IntoBytes, Immutable)] + #[repr(C, packed)] + struct AllocateCommand { + file: LoaderFileName, + align: u32, + zone: AllocZone, + } + + #[derive(IntoBytes, Immutable)] + #[repr(C, packed)] + struct AddPointerCommand { + dest_file: LoaderFileName, + src_file: LoaderFileName, + offset: u32, + size: u8, + } + + #[derive(IntoBytes, Immutable)] + #[repr(C, packed)] + struct AddChecksumCommand { + file: LoaderFileName, + result_offset: u32, + start: u32, + length: u32, + } + + #[cfg(test)] + mod test_table_loader { + use super::*; + + #[test] + fn basic() { + let mut loader = TableLoader::new(); + loader.add_allocate("rsdp", 16, AllocZone::FSeg); + loader.add_allocate("tables", 64, AllocZone::High); + loader.add_pointer("rsdp", "tables", 16, 4); + loader.add_checksum("rsdp", 8, 0, 20); + + let Entry::Bytes(bytes) = loader.finish() else { + panic!("expected Bytes entry"); + }; + + assert_eq!(bytes.len(), TABLE_LOADER_COMMAND_SIZE * 4); + assert_eq!(bytes[0], CommandType::Allocate as u8); + assert_eq!(bytes[128], CommandType::Allocate as u8); + assert_eq!(bytes[256], CommandType::AddPointer as u8); + assert_eq!(bytes[384], CommandType::AddChecksum as u8); + } + } } diff --git a/lib/propolis/src/hw/qemu/pvpanic.rs b/lib/propolis/src/hw/qemu/pvpanic.rs index 093d658e4..4c9f80b2c 100644 --- a/lib/propolis/src/hw/qemu/pvpanic.rs +++ b/lib/propolis/src/hw/qemu/pvpanic.rs @@ -34,6 +34,10 @@ pub struct PanicCounts { pub const DEVICE_NAME: &str = "qemu-pvpanic"; +/// IO port for the pvpanic device. +/// This value is chosen to match the convention in QEMU. +pub const IOPORT: u16 = 0x505; + /// Indicates that a guest panic has happened and should be processed by the /// host const HOST_HANDLED: u8 = 0b01; @@ -48,8 +52,6 @@ mod probes { } impl QemuPvpanic { - const IOPORT: u16 = 0x505; - pub fn create(log: slog::Logger) -> Arc { Arc::new(Self { counts: Mutex::new(PanicCounts { @@ -65,7 +67,7 @@ impl QemuPvpanic { let piodev = self.clone(); let piofn = Arc::new(move |_port: u16, rwo: RWOp| piodev.pio_rw(rwo)) as Arc; - pio.register(Self::IOPORT, 1, piofn).unwrap(); + pio.register(IOPORT, 1, piofn).unwrap(); } /// Returns the current panic counts reported by the guest. diff --git a/lib/propolis/src/hw/uart/lpc.rs b/lib/propolis/src/hw/uart/lpc.rs index 5c1714e1c..b3701afc4 100644 --- a/lib/propolis/src/hw/uart/lpc.rs +++ b/lib/propolis/src/hw/uart/lpc.rs @@ -7,10 +7,13 @@ use std::sync::{Arc, Mutex}; use super::uart16550::{migrate, Uart}; use crate::chardev::*; use crate::common::*; +use crate::firmware::acpi; use crate::intr_pins::IntrPin; use crate::migrate::*; use crate::pio::{PioBus, PioFn}; +use acpi_tables::{aml, Aml, AmlSink}; + // Low Pin Count UART pub const REGISTER_LEN: usize = 8; @@ -36,20 +39,30 @@ impl UartState { } pub struct LpcUart { + name: &'static str, + irq: u8, state: Mutex, + port: Mutex>, notify_readable: NotifierCell, notify_writable: NotifierCell, } impl LpcUart { - pub fn new(irq_pin: Box) -> Arc { + pub fn new( + name: &'static str, + irq: u8, + irq_pin: Box, + ) -> Arc { Arc::new(Self { + name, + irq, state: Mutex::new(UartState { uart: Uart::new(), irq_pin, auto_discard: true, paused: false, }), + port: Mutex::new(None), notify_readable: NotifierCell::new(), notify_writable: NotifierCell::new(), }) @@ -59,6 +72,9 @@ impl LpcUart { let piofn = Arc::new(move |_port: u16, rwo: RWOp| this.pio_rw(rwo)) as Arc; bus.register(port, REGISTER_LEN as u16, piofn).unwrap(); + + let mut current_port = self.port.lock().unwrap(); + *current_port = Some(port); } fn pio_rw(&self, rwo: RWOp) { assert!(rwo.offset() < REGISTER_LEN); @@ -172,6 +188,10 @@ impl Lifecycle for LpcUart { let mut state = self.state.lock().unwrap(); state.paused = false; } + + fn as_dsdt_generator(&self) -> Option<&dyn acpi::DsdtGenerator> { + Some(self) + } } impl MigrateSingle for LpcUart { fn export( @@ -194,3 +214,47 @@ impl MigrateSingle for LpcUart { Ok(()) } } + +impl acpi::DsdtGenerator for LpcUart { + fn dsdt_scope(&self) -> acpi::DsdtScope { + acpi::DsdtScope::Lpc + } +} + +impl Aml for LpcUart { + // This AML code is inherited from the original EDK2 static tables. + // + // The original tables only defined COM1 and COM2, even though VMs often + // have 4 serial ports. + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + let port = match *self.port.lock().unwrap() { + Some(p) => p, + None => panic!("expected UART device to be connected"), + }; + + #[allow(clippy::wildcard_in_or_patterns)] + let uid: u32 = match self.name { + "COM1" => 1, + "COM2" => 2, + "COM3" | "COM4" | _ => { + return; + } + }; + + aml::Device::new( + aml::Path::new(&format!("UAR{}", uid)), + vec![ + &acpi::aml::names::hid(&aml::EISAName::new( + acpi::aml::devids::COM_PORT_16550A, + )), + &acpi::aml::names::ddn(&self.name), + &acpi::aml::names::uid(&uid), + &acpi::aml::names::crs(&aml::ResourceTemplate::new(vec![ + &acpi::aml::io_port(port, 1, REGISTER_LEN as u8), + &aml::Irq::new(true, false, false, self.irq), + ])), + ], + ) + .to_aml_bytes(sink); + } +} diff --git a/lib/propolis/src/lifecycle.rs b/lib/propolis/src/lifecycle.rs index c138ae7c0..01622ebfe 100644 --- a/lib/propolis/src/lifecycle.rs +++ b/lib/propolis/src/lifecycle.rs @@ -6,6 +6,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use futures::future::{self, BoxFuture}; +use crate::firmware::acpi; use crate::migrate::Migrator; /// General trait for emulated devices in the system. @@ -96,6 +97,17 @@ pub trait Lifecycle: Send + Sync + 'static { fn migrate(&'_ self) -> Migrator<'_> { Migrator::Empty } + + /// Returns this device as a [`DsdtGenerator`] if it contributes to the + /// DSDT ACPI table. + /// + /// Devices that implement [`DsdtGenerator`] should override this method + /// to return `Some(self)` so they can be automatically discovered. + /// + /// [`DsdtGenerator`]: crate::firmware::acpi::DsdtGenerator + fn as_dsdt_generator(&self) -> Option<&dyn acpi::DsdtGenerator> { + None + } } /// Indicator for tracking [Lifecycle] states. diff --git a/phd-tests/framework/src/test_vm/config.rs b/phd-tests/framework/src/test_vm/config.rs index 9412923b2..f65643c80 100644 --- a/phd-tests/framework/src/test_vm/config.rs +++ b/phd-tests/framework/src/test_vm/config.rs @@ -357,11 +357,19 @@ impl<'dr> VmConfig<'dr> { assert!(_old.is_none()); } - let _old = spec.components.insert( - "com1".into(), - Component::SerialPort(SerialPort { num: SerialPortNumber::Com1 }), - ); - assert!(_old.is_none()); + // Create the same serial ports as Omicron and propolis-cli to generate + // consistent ACPI tables. + for (name, port) in [ + ("com1", SerialPortNumber::Com1), + ("com2", SerialPortNumber::Com2), + ("com3", SerialPortNumber::Com3), + ("com4", SerialPortNumber::Com4), + ] { + let _old = spec.components.insert( + name.into(), + Component::SerialPort(SerialPort { num: port }), + ); + } if let Some(boot_order) = boot_order.as_ref() { let _old = spec.components.insert( diff --git a/phd-tests/tests/Cargo.toml b/phd-tests/tests/Cargo.toml index 3b0dc3ba9..568b24ee2 100644 --- a/phd-tests/tests/Cargo.toml +++ b/phd-tests/tests/Cargo.toml @@ -8,6 +8,7 @@ test = false doctest = false [dependencies] +acpi_tables.workspace = true anyhow.workspace = true backoff.workspace = true byteorder.workspace = true @@ -23,6 +24,7 @@ oximeter-producer.workspace = true oximeter.workspace = true phd-testcase.workspace = true propolis-client.workspace = true +propolis.workspace = true reqwest.workspace = true slog-term.workspace = true slog.workspace = true diff --git a/phd-tests/tests/src/firmware.rs b/phd-tests/tests/src/firmware.rs new file mode 100644 index 000000000..5dd44c34f --- /dev/null +++ b/phd-tests/tests/src/firmware.rs @@ -0,0 +1,266 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use phd_testcase::*; + +// This test verifies that the ACPI tables generated for a VM match the tables +// we expect to find. +// +// The test tables are read from versioned directories in `testdata/acpi`. Use +// `propolis-server` with the `acpi-debug` feature enabled and create a VM with +// 2 vCPUs to generate new reference tables. +// +// ``` +// cargo build --bin propolis-server --features acpi-debug +// ``` +// +// `propolis-standalone` creates devices with slightly different names, so the +// tables it generates may not match the ones generated in PHD tests. Always +// use `propolis-server` to create reference tables. +// +// The generated ACPI tables will be written to a directory called `acpi` in +// the working directory in which `propolis-server` is running. You can copy +// them over to `testdata/acpi`. +// +// To debug test failures, feed the hex ACPI table representation from the +// failed assertion message to `xxd -r -p` and save the output to a file. +// +// ``` +// $ cargo xtask phd run +// ... +// assertion `left == right` failed: expected FACP table to match +// left: "46414350f400000003..." +// right: "46414350f400000003..." +// ... +// +// $ echo '46414350f400000003...' | xxd -r -p > facp.dat +// ``` +// +// You can compare the binary file directly against the expected file in +// `testdata/acpi` or decompile the AML code into ASL using the `iasl` tool +// from the ACPICA project. +// +// ``` +// iasl -d facp.dat +// ``` +#[phd_testcase] +async fn acpi_tables_generation(ctx: &TestCtx) { + use propolis::firmware::acpi; + use std::fmt::Write; + + let mut vm = ctx + .spawn_vm(ctx.vm_config_builder("acpi_tables_generation").cpus(2), None) + .await?; + + if !vm.guest_os_kind().is_linux() { + phd_skip!("requires Linux"); + } + + vm.launch().await?; + vm.wait_to_boot().await?; + + const ACPI_TABLES_PATH: &str = "/sys/firmware/acpi/tables"; + + // These are the tables that Linux makes available in the /sys path above. + // + // We don't need to check the RSDP and XSDT tables because OVMF always + // overwrites them with its own version. OVMF also introduces new tables, + // such as BGRT, and we don't need to test them either. + let expected_madt = include_bytes!("../testdata/acpi/v0/madt.dat").to_vec(); + let expected_dsdt = include_bytes!("../testdata/acpi/v0/dsdt.dat").to_vec(); + let expected_fadt = include_bytes!("../testdata/acpi/v0/fadt.dat").to_vec(); + let expected_ssdt = include_bytes!("../testdata/acpi/v0/ssdt.dat").to_vec(); + + // TablePatch represents a range that will be copied over from the expected + // table. These usually represent runtime values that can change at + // runtime, such as addresses to other tables and checksums. + struct TablePatch { + offset: usize, + length: usize, + } + + struct TestCase<'a> { + table: &'a str, + expect: Vec, + patches: Vec, + } + + for case in [ + TestCase { table: "APIC", expect: expected_madt, patches: vec![] }, + TestCase { table: "DSDT", expect: expected_dsdt, patches: vec![] }, + TestCase { + table: "FACP", + expect: expected_fadt, + patches: vec![ + TablePatch { + offset: acpi::TABLE_HEADER_CHECKSUM_OFFSET, + length: acpi::TABLE_HEADER_CHECKSUM_LEN, + }, + TablePatch { + offset: acpi::FADT_FACS_OFFSET, + length: acpi::FADT_FACS_LEN, + }, + TablePatch { + offset: acpi::FADT_DSDT_OFFSET, + length: acpi::FADT_DSDT_LEN, + }, + TablePatch { + offset: acpi::FADT_X_DSDT_OFFSET, + length: acpi::FADT_X_DSDT_LEN, + }, + ], + }, + TestCase { + table: "SSDT", + expect: expected_ssdt, + patches: vec![ + TablePatch { + offset: acpi::TABLE_HEADER_CHECKSUM_OFFSET, + length: acpi::TABLE_HEADER_CHECKSUM_LEN, + }, + TablePatch { + offset: acpi::SSDT_FWDT_ADDR_OFFSET, + length: acpi::SSDT_FWDT_ADDR_LEN, + }, + ], + }, + ] { + let cmd = format!("xxd -p -c0 {0}/{1}", ACPI_TABLES_PATH, case.table); + let mut out: String = + vm.run_shell_command(&cmd).await?.split_whitespace().collect(); + + let mut expected_hex = String::new(); + for b in case.expect.iter() { + write!(expected_hex, "{:02x}", b).unwrap(); + } + + for p in &case.patches { + let start = p.offset * 2; // Each byte is represented by 2 characters. + let end = start + p.length * 2; + let r = start..end; + out.replace_range(r.clone(), &expected_hex[r.clone()]); + } + + assert_eq!(expected_hex, out, "expected {} table to match", case.table); + } + + // Verify FACS table have the expected version. + // The generated FACS table has version equal to 1. + const FACS_VERSION_OFFSET: u8 = 32; + const FACS_VERSION_LEN: u8 = 1; + let facs_version_cmd = format!( + "xxd -s {1} -l {2} -c {2} -p {3}/{0}", + "FACS", FACS_VERSION_OFFSET, FACS_VERSION_LEN, ACPI_TABLES_PATH + ); + let facs_version = vm.run_shell_command(&facs_version_cmd).await?; + + assert_eq!(facs_version, "01"); +} + +// This test uses ACPI tools to verify that the ACPI tables generated by +// Propolis are functional. +// +// It requires a guest OS image with the following tools installed: +// - acpidump +// - iasl +// - fwts +// +// fwts doesn't build very easily on Alpine, so a Debian/Ubuntu based image is +// recommended. +// +// The ACPICA tools can be installed from packages: +// apt install acpica-tools acpi +// +// fwts can be installed from source: +// https://github.com/fwts/fwts/blob/master/README +#[phd_testcase] +async fn acpi_tables_parse(ctx: &TestCtx) { + let mut vm = ctx + .spawn_vm( + ctx.vm_config_builder("acpi_tables_parse").cpus(2), // Ensure fwts results are consistent. + None, + ) + .await?; + + if !vm.guest_os_kind().is_linux() { + phd_skip!("requires Linux"); + } + + vm.launch().await?; + vm.wait_to_boot().await?; + + // Skip test if guest doesn't have the necessary tools installed. + let required_tools = ["acpidump", "iasl", "fwts"]; + for tool in required_tools.iter() { + if vm.run_shell_command(&format!("which {}", tool)).await.is_err() { + phd_skip!(format!("guest doesn't have {} installed", tool)); + } + } + + let expected_files = ["apic", "dsdt", "facp", "facs", "ssdt"]; + + // Verify we can dump the expected tables. + vm.run_shell_command("acpidump -b").await.expect("acpidump"); + + let ls = vm.run_shell_command("ls *.dat").await?; + for file in expected_files.iter() { + let expect = format!("{}.dat", file); + assert!(ls.contains(&expect), "expected file {} to exist", expect); + } + + // Verify ACPI tables can be parsed and disassembled. + for file in expected_files.iter() { + vm.run_shell_command(&format!("iasl -we -d {}.dat", file)) + .await + .unwrap_or_else(|_| panic!("failed to disassemble {}.dat", file)); + + let expect = format!("{}.dsl", file); + let ls = vm.run_shell_command(&format!("ls {}", expect)).await?; + assert!(ls.contains(&expect), "expected file {} to exist", expect); + } + + // Verify fwts results. + vm.run_shell_command("fwts --acpicompliance --acpitests; true").await?; + let fwts_results: Vec<_> = vm + .run_shell_command("tail -n 2 results.log | head -n 1") + .await? + .split('|') + .map(|s| s.replace(" ", "")) + .collect(); + + // The current ACPI tables generate (num_cpus + 2) errors and 1 warning. + // + // Test Failure Summary + // ================================================================================ + // + // Critical failures: NONE + // + // High failures: 1 + // fadt: FADT X_GPE0_BLK Access width 0x00 but it should be 1 (byte access). + // + // Medium failures: 3 + // madt: LAPIC has no matching processor UID 0 + // madt: LAPIC has no matching processor UID 1 + // madt: LAPICNMI has no matching processor UID 255 + // + // Low failures: NONE + // + // Other failures: NONE + let expexted_fwts_results = ["", "", "4", "0", "1"]; + assert_eq!( + fwts_results[2], expexted_fwts_results[2], + "expected {} fwts failures, got {}", + expexted_fwts_results[2], fwts_results[2], + ); + assert_eq!( + fwts_results[3], expexted_fwts_results[3], + "expected {} fwts aborts, got {}", + expexted_fwts_results[3], fwts_results[3] + ); + assert_eq!( + fwts_results[4], expexted_fwts_results[4], + "expected {} fwts warnings, got {}", + expexted_fwts_results[4], fwts_results[4], + ); +} diff --git a/phd-tests/tests/src/lib.rs b/phd-tests/tests/src/lib.rs index d70a4735b..2e061051a 100644 --- a/phd-tests/tests/src/lib.rs +++ b/phd-tests/tests/src/lib.rs @@ -8,6 +8,7 @@ mod boot_order; mod cpuid; mod crucible; mod disk; +mod firmware; mod framework; mod hw; mod hyperv; diff --git a/phd-tests/tests/testdata/acpi/v0/dsdt.dat b/phd-tests/tests/testdata/acpi/v0/dsdt.dat new file mode 100644 index 000000000..fa6c52316 Binary files /dev/null and b/phd-tests/tests/testdata/acpi/v0/dsdt.dat differ diff --git a/phd-tests/tests/testdata/acpi/v0/facs.dat b/phd-tests/tests/testdata/acpi/v0/facs.dat new file mode 100644 index 000000000..63282ea5e Binary files /dev/null and b/phd-tests/tests/testdata/acpi/v0/facs.dat differ diff --git a/phd-tests/tests/testdata/acpi/v0/fadt.dat b/phd-tests/tests/testdata/acpi/v0/fadt.dat new file mode 100644 index 000000000..36a1ba66b Binary files /dev/null and b/phd-tests/tests/testdata/acpi/v0/fadt.dat differ diff --git a/phd-tests/tests/testdata/acpi/v0/madt.dat b/phd-tests/tests/testdata/acpi/v0/madt.dat new file mode 100644 index 000000000..2dc831fb3 Binary files /dev/null and b/phd-tests/tests/testdata/acpi/v0/madt.dat differ diff --git a/phd-tests/tests/testdata/acpi/v0/rsdp.dat b/phd-tests/tests/testdata/acpi/v0/rsdp.dat new file mode 100644 index 000000000..27e123ff3 Binary files /dev/null and b/phd-tests/tests/testdata/acpi/v0/rsdp.dat differ diff --git a/phd-tests/tests/testdata/acpi/v0/ssdt.dat b/phd-tests/tests/testdata/acpi/v0/ssdt.dat new file mode 100644 index 000000000..862d89be3 Binary files /dev/null and b/phd-tests/tests/testdata/acpi/v0/ssdt.dat differ diff --git a/phd-tests/tests/testdata/acpi/v0/xsdt.dat b/phd-tests/tests/testdata/acpi/v0/xsdt.dat new file mode 100644 index 000000000..d242cd662 Binary files /dev/null and b/phd-tests/tests/testdata/acpi/v0/xsdt.dat differ