From 1b0f52470692f25c3b8445db356ac07cea4d3932 Mon Sep 17 00:00:00 2001 From: Amey Narkhede Date: Mon, 29 Dec 2025 00:41:38 +0000 Subject: [PATCH 01/47] Add fw_cfg table-loader helpers for ACPI table generation Add a TableLoader builder that can be used to generate the etc/table-loader file to be passed to guest firmware via fw_cfg. The etc/table-loader file in fw_cfg contains the sequence of fixed size linker/loader commands that can be used to instruct guest to allcoate memory for set of fw_cfg files(e.g. ACPI tables), link allocated memory by patching pointers and calculate the ACPI checksum. Signed-off-by: Amey Narkhede --- lib/propolis/src/hw/qemu/fwcfg.rs | 238 ++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) diff --git a/lib/propolis/src/hw/qemu/fwcfg.rs b/lib/propolis/src/hw/qemu/fwcfg.rs index 5bad160e0..1e439a123 100644 --- a/lib/propolis/src/hw/qemu/fwcfg.rs +++ b/lib/propolis/src/hw/qemu/fwcfg.rs @@ -1057,6 +1057,7 @@ mod test { pub mod formats { use super::Entry; use crate::hw::pci; + use thiserror::Error; use zerocopy::{Immutable, IntoBytes}; /// A type for a range described in an E820 map entry. @@ -1302,4 +1303,241 @@ pub mod formats { assert_eq!(&expected[..], &entries[..]); } } + + pub const TABLE_LOADER_FILESZ: usize = 56; + pub const TABLE_LOADER_COMMAND_SIZE: usize = 128; + + #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoBytes, Immutable)] + #[repr(u8)] + pub enum AllocZone { + High = 0x1, + FSeg = 0x2, + } + + #[derive(Debug, Error)] + pub enum TableLoaderError { + #[error( + "file name too long: {len} bytes exceeds max of {}", + TABLE_LOADER_FILESZ - 1 + )] + FileNameTooLong { len: usize }, + + #[error("invalid pointer size: {0} (must be 1, 2, 4, or 8)")] + InvalidPointerSize(u8), + + #[error("alignment must be a power of two, got {0}")] + InvalidAlignment(u32), + } + + #[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) -> Result { + let bytes = name.as_bytes(); + if bytes.len() >= TABLE_LOADER_FILESZ { + return Err(TableLoaderError::FileNameTooLong { + len: bytes.len(), + }); + } + + let mut buf = [0u8; TABLE_LOADER_FILESZ]; + buf[..bytes.len()].copy_from_slice(bytes); + Ok(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, + } + + #[must_use = "call .finish() to get the table-loader entry"] + 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, + ) -> Result<(), TableLoaderError> { + if !align.is_power_of_two() { + return Err(TableLoaderError::InvalidAlignment(align)); + } + + let cmd = AllocateCommand { + file: LoaderFileName::new(file)?, + align, + zone, + }; + + self.write_command(CommandType::Allocate, cmd.as_bytes()); + Ok(()) + } + + pub fn add_pointer( + &mut self, + dest_file: &str, + src_file: &str, + offset: u32, + size: u8, + ) -> Result<(), TableLoaderError> { + if !matches!(size, 1 | 2 | 4 | 8) { + return Err(TableLoaderError::InvalidPointerSize(size)); + } + + 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()); + Ok(()) + } + + pub fn add_checksum( + &mut self, + file: &str, + result_offset: u32, + start: u32, + length: u32, + ) -> Result<(), TableLoaderError> { + let cmd = AddChecksumCommand { + file: LoaderFileName::new(file)?, + result_offset, + start, + length, + }; + + self.write_command(CommandType::AddChecksum, cmd.as_bytes()); + Ok(()) + } + + 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); + } + } + + pub const TABLE_LOADER_FWCFG_NAME: &str = "etc/table-loader"; + pub const ACPI_TABLES_FWCFG_NAME: &str = "etc/acpi/tables"; + pub const ACPI_RSDP_FWCFG_NAME: &str = "etc/acpi/rsdp"; + + #[cfg(test)] + mod test_table_loader { + use super::*; + + #[test] + fn struct_sizes() { + assert_eq!( + std::mem::size_of::(), + TABLE_LOADER_FILESZ + ); + assert_eq!( + std::mem::size_of::(), + TABLE_LOADER_FILESZ + 5 + ); + assert_eq!( + std::mem::size_of::(), + TABLE_LOADER_FILESZ * 2 + 5 + ); + assert_eq!( + std::mem::size_of::(), + TABLE_LOADER_FILESZ + 12 + ); + } + + #[test] + fn basic() { + let mut loader = TableLoader::new(); + loader.add_allocate("rsdp", 16, AllocZone::FSeg).unwrap(); + loader.add_allocate("tables", 64, AllocZone::High).unwrap(); + loader.add_pointer("rsdp", "tables", 16, 4).unwrap(); + loader.add_checksum("rsdp", 8, 0, 20).unwrap(); + + 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); + } + + #[test] + fn validation() { + let mut loader = TableLoader::new(); + + let long_name = "a".repeat(TABLE_LOADER_FILESZ); + assert!(matches!( + loader.add_allocate(&long_name, 64, AllocZone::High), + Err(TableLoaderError::FileNameTooLong { .. }) + )); + + assert!(matches!( + loader.add_allocate("test", 3, AllocZone::High), + Err(TableLoaderError::InvalidAlignment(3)) + )); + + assert!(matches!( + loader.add_pointer("a", "b", 0, 3), + Err(TableLoaderError::InvalidPointerSize(3)) + )); + } + } } From 5b463440696a751b78c42daa43c42c31644f06d8 Mon Sep 17 00:00:00 2001 From: Amey Narkhede Date: Mon, 29 Dec 2025 02:55:38 +0000 Subject: [PATCH 02/47] Add RSDT, XSDT and RSDP tables Add builders to generate basic ACPI tables RSDP(ACPI 2.0+) that points to XSDT, XSDT with 64-bit table pointers and RSDT with 32-bit table pointers that would work with the table-loader mechanism in fw_cfg. These tables are used to describe the ACPI table hierarchy to guest firmware. The builders produce raw table data bytes with placeholder addresses and checksums that are fixed up by firmware using table-loader commands. Signed-off-by: Amey Narkhede --- lib/propolis/src/firmware/acpi/mod.rs | 9 ++ lib/propolis/src/firmware/acpi/tables.rs | 154 +++++++++++++++++++++++ lib/propolis/src/firmware/mod.rs | 1 + lib/propolis/src/hw/qemu/fwcfg.rs | 19 +++ 4 files changed, 183 insertions(+) create mode 100644 lib/propolis/src/firmware/acpi/mod.rs create mode 100644 lib/propolis/src/firmware/acpi/tables.rs diff --git a/lib/propolis/src/firmware/acpi/mod.rs b/lib/propolis/src/firmware/acpi/mod.rs new file mode 100644 index 000000000..2396ce64d --- /dev/null +++ b/lib/propolis/src/firmware/acpi/mod.rs @@ -0,0 +1,9 @@ +// 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. + +pub mod tables; + +pub use tables::{Rsdt, Rsdp, Xsdt}; diff --git a/lib/propolis/src/firmware/acpi/tables.rs b/lib/propolis/src/firmware/acpi/tables.rs new file mode 100644 index 000000000..679d9127b --- /dev/null +++ b/lib/propolis/src/firmware/acpi/tables.rs @@ -0,0 +1,154 @@ +// 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 builders. +//! +//! This module provides builders for generating ACPI tables that are used +//! to describe the system configuration to guest firmware. + +use std::mem::size_of; + +use zerocopy::{Immutable, IntoBytes}; + +pub const ACPI_TABLE_HEADER_SIZE: usize = 36; +const ACPI_TABLE_LENGTH_OFFSET: usize = 4; + +#[derive(Copy, Clone, IntoBytes, Immutable)] +#[repr(C, packed)] +struct AcpiTableHeader { + signature: [u8; 4], + length: u32, + revision: u8, + checksum: u8, + oem_id: [u8; 6], + oem_table_id: [u8; 8], + oem_revision: u32, + creator_id: [u8; 4], + creator_revision: u32, +} + +impl AcpiTableHeader { + fn new(signature: [u8; 4], revision: u8) -> Self { + Self { + signature, + length: 0, + revision, + checksum: 0, + oem_id: *b"OXIDE\0", + oem_table_id: *b"PROPOLIS", + oem_revision: 1, + creator_id: *b"OXDE", + creator_revision: 1, + } + } +} + +#[must_use = "call .finish() to get the table bytes"] +pub struct Rsdt { + data: Vec, +} + +impl Rsdt { + pub fn new() -> Self { + let header = AcpiTableHeader::new(*b"RSDT", 1); + let mut data = vec![0u8; ACPI_TABLE_HEADER_SIZE]; + data[..ACPI_TABLE_HEADER_SIZE].copy_from_slice(header.as_bytes()); + Self { data } + } + + pub fn add_entry(&mut self) -> u32 { + let offset = self.data.len() as u32; + self.data.extend_from_slice(&[0u8; size_of::()]); + offset + } + + pub fn finish(mut self) -> Vec { + let length = self.data.len() as u32; + self.data[ACPI_TABLE_LENGTH_OFFSET + ..ACPI_TABLE_LENGTH_OFFSET + size_of::()] + .copy_from_slice(&length.to_le_bytes()); + self.data + } +} + +#[must_use = "call .finish() to get the table bytes"] +pub struct Xsdt { + data: Vec, +} + +impl Xsdt { + pub fn new() -> Self { + let header = AcpiTableHeader::new(*b"XSDT", 1); + let mut data = vec![0u8; ACPI_TABLE_HEADER_SIZE]; + data[..ACPI_TABLE_HEADER_SIZE].copy_from_slice(header.as_bytes()); + Self { data } + } + + pub fn add_entry(&mut self) -> u32 { + let offset = self.data.len() as u32; + self.data.extend_from_slice(&[0u8; size_of::()]); + offset + } + + pub fn finish(mut self) -> Vec { + let length = self.data.len() as u32; + self.data[ACPI_TABLE_LENGTH_OFFSET + ..ACPI_TABLE_LENGTH_OFFSET + size_of::()] + .copy_from_slice(&length.to_le_bytes()); + self.data + } +} + +pub const RSDP_SIZE: usize = 36; +pub const RSDP_V1_SIZE: usize = 20; + +const RSDP_SIGNATURE_OFFSET: usize = 0; +const RSDP_SIGNATURE_LEN: usize = 8; +pub const RSDP_CHECKSUM_OFFSET: usize = 8; +const RSDP_OEMID_OFFSET: usize = 9; +const RSDP_OEMID_LEN: usize = 6; +const RSDP_REVISION_OFFSET: usize = 15; +const RSDP_LENGTH_OFFSET: usize = 20; +pub const RSDP_XSDT_ADDR_OFFSET: usize = 24; +pub const RSDP_EXT_CHECKSUM_OFFSET: usize = 32; + +#[must_use = "call .finish() to get the RSDP bytes"] +pub struct Rsdp { + data: Vec, +} + +impl Rsdp { + pub fn new() -> Self { + let mut data = vec![0u8; RSDP_SIZE]; + data[RSDP_SIGNATURE_OFFSET..RSDP_SIGNATURE_OFFSET + RSDP_SIGNATURE_LEN] + .copy_from_slice(b"RSD PTR "); + data[RSDP_OEMID_OFFSET..RSDP_OEMID_OFFSET + RSDP_OEMID_LEN] + .copy_from_slice(b"OXIDE\0"); + data[RSDP_REVISION_OFFSET] = 2; + data[RSDP_LENGTH_OFFSET..RSDP_LENGTH_OFFSET + size_of::()] + .copy_from_slice(&(RSDP_SIZE as u32).to_le_bytes()); + Self { data } + } + + pub fn finish(self) -> Vec { + self.data + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn basic() { + let mut xsdt = Xsdt::new(); + xsdt.add_entry(); + let xsdt_data = xsdt.finish(); + assert_eq!(&xsdt_data[0..4], b"XSDT"); + + let rsdp = Rsdp::new(); + let rsdp_data = rsdp.finish(); + assert_eq!(&rsdp_data[0..8], b"RSD PTR "); + } +} 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/qemu/fwcfg.rs b/lib/propolis/src/hw/qemu/fwcfg.rs index 1e439a123..1dc1e4d74 100644 --- a/lib/propolis/src/hw/qemu/fwcfg.rs +++ b/lib/propolis/src/hw/qemu/fwcfg.rs @@ -1476,6 +1476,8 @@ pub mod formats { pub const ACPI_TABLES_FWCFG_NAME: &str = "etc/acpi/tables"; pub const ACPI_RSDP_FWCFG_NAME: &str = "etc/acpi/rsdp"; + pub use crate::firmware::acpi::{Rsdt, Rsdp, Xsdt}; + #[cfg(test)] mod test_table_loader { use super::*; @@ -1540,4 +1542,21 @@ pub mod formats { )); } } + + #[cfg(test)] + mod test_acpi_tables { + use super::*; + + #[test] + fn basic() { + let mut xsdt = Xsdt::new(); + xsdt.add_entry(); + let xsdt_data = xsdt.finish(); + assert_eq!(&xsdt_data[0..4], b"XSDT"); + + let rsdp = Rsdp::new(); + let rsdp_data = rsdp.finish(); + assert_eq!(&rsdp_data[0..8], b"RSD PTR "); + } + } } From d9f8ac4061e8ad3dc9ea3d47493052b2b07fd29b Mon Sep 17 00:00:00 2001 From: Amey Narkhede Date: Mon, 29 Dec 2025 03:38:55 +0000 Subject: [PATCH 03/47] Add FADT and DSDT table generation FADT describes fixed hardware features and points to the DSDT. The builder supports both standard and HW-reduced ACPI modes. DSDT contains AML bytecode describing system hardware. The builder provides methods to append AML data which could be populated by an AML generation mechanism in subsequent commits. Signed-off-by: Amey Narkhede --- lib/propolis/src/firmware/acpi/mod.rs | 2 +- lib/propolis/src/firmware/acpi/tables.rs | 184 +++++++++++++++++++++++ lib/propolis/src/hw/qemu/fwcfg.rs | 2 +- 3 files changed, 186 insertions(+), 2 deletions(-) diff --git a/lib/propolis/src/firmware/acpi/mod.rs b/lib/propolis/src/firmware/acpi/mod.rs index 2396ce64d..e10642b4d 100644 --- a/lib/propolis/src/firmware/acpi/mod.rs +++ b/lib/propolis/src/firmware/acpi/mod.rs @@ -6,4 +6,4 @@ pub mod tables; -pub use tables::{Rsdt, Rsdp, Xsdt}; +pub use tables::{Dsdt, Fadt, Rsdt, Rsdp, Xsdt}; diff --git a/lib/propolis/src/firmware/acpi/tables.rs b/lib/propolis/src/firmware/acpi/tables.rs index 679d9127b..46bc6fef0 100644 --- a/lib/propolis/src/firmware/acpi/tables.rs +++ b/lib/propolis/src/firmware/acpi/tables.rs @@ -136,6 +136,182 @@ impl Rsdp { } } +pub struct Dsdt { + data: Vec, +} + +impl Dsdt { + pub fn new() -> Self { + let header = AcpiTableHeader::new(*b"DSDT", 2); + let mut data = vec![0u8; ACPI_TABLE_HEADER_SIZE]; + data[..ACPI_TABLE_HEADER_SIZE].copy_from_slice(header.as_bytes()); + Self { data } + } + + pub fn append_aml(&mut self, aml: &[u8]) { + self.data.extend_from_slice(aml); + } + + pub fn finish(mut self) -> Vec { + let length = self.data.len() as u32; + self.data[ACPI_TABLE_LENGTH_OFFSET + ..ACPI_TABLE_LENGTH_OFFSET + size_of::()] + .copy_from_slice(&length.to_le_bytes()); + self.data + } +} + +pub const FADT_SIZE: usize = 276; +pub const FADT_REVISION: u8 = 6; +pub const FADT_MINOR_REVISION: u8 = 5; + +const FADT_FLAG_WBINVD: u32 = 1 << 0; +const FADT_FLAG_C1_SUPPORTED: u32 = 1 << 2; +const FADT_FLAG_SLP_BUTTON: u32 = 1 << 5; +const FADT_FLAG_TMR_VAL_EXT: u32 = 1 << 8; +const FADT_FLAG_RESET_REG_SUP: u32 = 1 << 10; +const FADT_FLAG_APIC_PHYSICAL: u32 = 1 << 19; +pub const FADT_FLAG_HW_REDUCED_ACPI: u32 = 1 << 20; + +pub const FADT_OFF_FACS32: usize = 36; +pub const FADT_OFF_DSDT32: usize = 40; +pub const FADT_OFF_DSDT64: usize = 140; +const FADT_OFF_SCI_INT: usize = 46; +const FADT_OFF_PM1A_EVT_BLK: usize = 56; +const FADT_OFF_PM1A_CNT_BLK: usize = 64; +const FADT_OFF_PM_TMR_BLK: usize = 76; +const FADT_OFF_PM1_EVT_LEN: usize = 88; +const FADT_OFF_PM1_CNT_LEN: usize = 89; +const FADT_OFF_PM_TMR_LEN: usize = 91; +const FADT_OFF_IAPC_BOOT_ARCH: usize = 109; +const FADT_OFF_FLAGS: usize = 112; +const FADT_OFF_RESET_REG: usize = 116; +const FADT_OFF_RESET_VALUE: usize = 128; +const FADT_OFF_MINOR_REV: usize = 131; +const FADT_OFF_X_PM1A_EVT_BLK: usize = 148; +const FADT_OFF_X_PM1A_CNT_BLK: usize = 172; +const FADT_OFF_X_PM_TMR_BLK: usize = 208; +const FADT_OFF_HYPERVISOR_ID: usize = 268; + +const GAS_OFF_SPACE_ID: usize = 0; +const GAS_OFF_BIT_WIDTH: usize = 1; +const GAS_OFF_ACCESS_WIDTH: usize = 3; +const GAS_OFF_ADDRESS: usize = 4; +const GAS_ADDRESS_LEN: usize = 8; +const GAS_SPACE_SYSTEM_IO: u8 = 1; +const GAS_ACCESS_BYTE: u8 = 1; +const GAS_ACCESS_WORD: u8 = 2; +const GAS_ACCESS_DWORD: u8 = 3; + +const ACPI_RESET_REG_PORT: u64 = 0xcf9; +const ACPI_RESET_VALUE: u8 = 0x06; + +const IAPC_BOOT_ARCH_LEGACY_DEVICES: u16 = 1 << 0; +const IAPC_BOOT_ARCH_8042: u16 = 1 << 1; + +const PIIX4_PM_BASE: u32 = 0xb000; +const PIIX4_PM1A_CNT_OFF: u32 = 4; +const PIIX4_PM_TMR_OFF: u32 = 8; +const PIIX4_PM1_EVT_LEN: u8 = 4; +const PIIX4_PM1_CNT_LEN: u8 = 2; +const PIIX4_PM_TMR_LEN: u8 = 4; +const PIIX4_SCI_IRQ: u16 = 9; + +const HYPERVISOR_ID: &[u8] = b"OXIDE"; + +pub struct Fadt { + data: Vec, +} + +impl Fadt { + pub fn new() -> Self { + let header = AcpiTableHeader::new(*b"FACP", FADT_REVISION); + let mut data = vec![0u8; FADT_SIZE]; + data[..ACPI_TABLE_HEADER_SIZE].copy_from_slice(header.as_bytes()); + data[ACPI_TABLE_LENGTH_OFFSET + ..ACPI_TABLE_LENGTH_OFFSET + size_of::()] + .copy_from_slice(&(FADT_SIZE as u32).to_le_bytes()); + + data[FADT_OFF_SCI_INT..FADT_OFF_SCI_INT + size_of::()] + .copy_from_slice(&PIIX4_SCI_IRQ.to_le_bytes()); + + data[FADT_OFF_PM1A_EVT_BLK..FADT_OFF_PM1A_EVT_BLK + size_of::()] + .copy_from_slice(&PIIX4_PM_BASE.to_le_bytes()); + data[FADT_OFF_PM1A_CNT_BLK..FADT_OFF_PM1A_CNT_BLK + size_of::()] + .copy_from_slice(&(PIIX4_PM_BASE + PIIX4_PM1A_CNT_OFF).to_le_bytes()); + data[FADT_OFF_PM_TMR_BLK..FADT_OFF_PM_TMR_BLK + size_of::()] + .copy_from_slice(&(PIIX4_PM_BASE + PIIX4_PM_TMR_OFF).to_le_bytes()); + + data[FADT_OFF_PM1_EVT_LEN] = PIIX4_PM1_EVT_LEN; + data[FADT_OFF_PM1_CNT_LEN] = PIIX4_PM1_CNT_LEN; + data[FADT_OFF_PM_TMR_LEN] = PIIX4_PM_TMR_LEN; + + let boot_arch = IAPC_BOOT_ARCH_LEGACY_DEVICES | IAPC_BOOT_ARCH_8042; + data[FADT_OFF_IAPC_BOOT_ARCH + ..FADT_OFF_IAPC_BOOT_ARCH + size_of::()] + .copy_from_slice(&boot_arch.to_le_bytes()); + + let flags = FADT_FLAG_WBINVD + | FADT_FLAG_C1_SUPPORTED + | FADT_FLAG_SLP_BUTTON + | FADT_FLAG_TMR_VAL_EXT + | FADT_FLAG_RESET_REG_SUP + | FADT_FLAG_APIC_PHYSICAL; + data[FADT_OFF_FLAGS..FADT_OFF_FLAGS + size_of::()] + .copy_from_slice(&flags.to_le_bytes()); + + data[FADT_OFF_RESET_REG + GAS_OFF_SPACE_ID] = GAS_SPACE_SYSTEM_IO; + data[FADT_OFF_RESET_REG + GAS_OFF_BIT_WIDTH] = u8::BITS as u8; + data[FADT_OFF_RESET_REG + GAS_OFF_ACCESS_WIDTH] = GAS_ACCESS_BYTE; + data[FADT_OFF_RESET_REG + GAS_OFF_ADDRESS + ..FADT_OFF_RESET_REG + GAS_OFF_ADDRESS + GAS_ADDRESS_LEN] + .copy_from_slice(&ACPI_RESET_REG_PORT.to_le_bytes()); + data[FADT_OFF_RESET_VALUE] = ACPI_RESET_VALUE; + + data[FADT_OFF_MINOR_REV] = FADT_MINOR_REVISION; + + data[FADT_OFF_X_PM1A_EVT_BLK + GAS_OFF_SPACE_ID] = GAS_SPACE_SYSTEM_IO; + data[FADT_OFF_X_PM1A_EVT_BLK + GAS_OFF_BIT_WIDTH] = PIIX4_PM1_EVT_LEN * 8; + data[FADT_OFF_X_PM1A_EVT_BLK + GAS_OFF_ACCESS_WIDTH] = GAS_ACCESS_DWORD; + data[FADT_OFF_X_PM1A_EVT_BLK + GAS_OFF_ADDRESS + ..FADT_OFF_X_PM1A_EVT_BLK + GAS_OFF_ADDRESS + GAS_ADDRESS_LEN] + .copy_from_slice(&(PIIX4_PM_BASE as u64).to_le_bytes()); + + data[FADT_OFF_X_PM1A_CNT_BLK + GAS_OFF_SPACE_ID] = GAS_SPACE_SYSTEM_IO; + data[FADT_OFF_X_PM1A_CNT_BLK + GAS_OFF_BIT_WIDTH] = PIIX4_PM1_CNT_LEN * 8; + data[FADT_OFF_X_PM1A_CNT_BLK + GAS_OFF_ACCESS_WIDTH] = GAS_ACCESS_WORD; + data[FADT_OFF_X_PM1A_CNT_BLK + GAS_OFF_ADDRESS + ..FADT_OFF_X_PM1A_CNT_BLK + GAS_OFF_ADDRESS + GAS_ADDRESS_LEN] + .copy_from_slice( + &((PIIX4_PM_BASE + PIIX4_PM1A_CNT_OFF) as u64).to_le_bytes(), + ); + + data[FADT_OFF_X_PM_TMR_BLK + GAS_OFF_SPACE_ID] = GAS_SPACE_SYSTEM_IO; + data[FADT_OFF_X_PM_TMR_BLK + GAS_OFF_BIT_WIDTH] = PIIX4_PM_TMR_LEN * 8; + data[FADT_OFF_X_PM_TMR_BLK + GAS_OFF_ACCESS_WIDTH] = GAS_ACCESS_DWORD; + data[FADT_OFF_X_PM_TMR_BLK + GAS_OFF_ADDRESS + ..FADT_OFF_X_PM_TMR_BLK + GAS_OFF_ADDRESS + GAS_ADDRESS_LEN] + .copy_from_slice( + &((PIIX4_PM_BASE + PIIX4_PM_TMR_OFF) as u64).to_le_bytes(), + ); + + data[FADT_OFF_HYPERVISOR_ID..FADT_OFF_HYPERVISOR_ID + HYPERVISOR_ID.len()] + .copy_from_slice(HYPERVISOR_ID); + Self { data } + } + + pub fn new_reduced() -> Self { + let mut fadt = Self::new(); + fadt.data[FADT_OFF_FLAGS..FADT_OFF_FLAGS + size_of::()] + .copy_from_slice(&FADT_FLAG_HW_REDUCED_ACPI.to_le_bytes()); + fadt + } + + pub fn finish(self) -> Vec { + self.data + } +} + #[cfg(test)] mod tests { use super::*; @@ -150,5 +326,13 @@ mod tests { let rsdp = Rsdp::new(); let rsdp_data = rsdp.finish(); assert_eq!(&rsdp_data[0..8], b"RSD PTR "); + + let dsdt = Dsdt::new(); + let dsdt_data = dsdt.finish(); + assert_eq!(&dsdt_data[0..4], b"DSDT"); + + let fadt = Fadt::new(); + let fadt_data = fadt.finish(); + assert_eq!(&fadt_data[0..4], b"FACP"); } } diff --git a/lib/propolis/src/hw/qemu/fwcfg.rs b/lib/propolis/src/hw/qemu/fwcfg.rs index 1dc1e4d74..e773996ef 100644 --- a/lib/propolis/src/hw/qemu/fwcfg.rs +++ b/lib/propolis/src/hw/qemu/fwcfg.rs @@ -1476,7 +1476,7 @@ pub mod formats { pub const ACPI_TABLES_FWCFG_NAME: &str = "etc/acpi/tables"; pub const ACPI_RSDP_FWCFG_NAME: &str = "etc/acpi/rsdp"; - pub use crate::firmware::acpi::{Rsdt, Rsdp, Xsdt}; + pub use crate::firmware::acpi::{Dsdt, Fadt, Rsdt, Rsdp, Xsdt}; #[cfg(test)] mod test_table_loader { From 2ea6aaacafd8b887f034a53340c549dc76cd8306 Mon Sep 17 00:00:00 2001 From: Amey Narkhede Date: Mon, 29 Dec 2025 03:45:18 +0000 Subject: [PATCH 04/47] Add MADT table Add a builder for the Multiple APIC Description Table (MADT) that describes the system's interrupt controllers. Supports adding local APIC, I/O APIC and interrupt source overrides for describing processor and interrupt controller topology. Signed-off-by: Amey Narkhede --- lib/propolis/src/firmware/acpi/mod.rs | 2 +- lib/propolis/src/firmware/acpi/tables.rs | 108 +++++++++++++++++++++++ lib/propolis/src/hw/qemu/fwcfg.rs | 2 +- 3 files changed, 110 insertions(+), 2 deletions(-) diff --git a/lib/propolis/src/firmware/acpi/mod.rs b/lib/propolis/src/firmware/acpi/mod.rs index e10642b4d..3616ec759 100644 --- a/lib/propolis/src/firmware/acpi/mod.rs +++ b/lib/propolis/src/firmware/acpi/mod.rs @@ -6,4 +6,4 @@ pub mod tables; -pub use tables::{Dsdt, Fadt, Rsdt, Rsdp, Xsdt}; +pub use tables::{Dsdt, Fadt, Madt, Rsdt, Rsdp, Xsdt}; diff --git a/lib/propolis/src/firmware/acpi/tables.rs b/lib/propolis/src/firmware/acpi/tables.rs index 46bc6fef0..98a93c822 100644 --- a/lib/propolis/src/firmware/acpi/tables.rs +++ b/lib/propolis/src/firmware/acpi/tables.rs @@ -312,6 +312,110 @@ impl Fadt { } } +const MADT_LOCAL_APIC_ADDR_OFF: usize = ACPI_TABLE_HEADER_SIZE; +const MADT_FLAGS_OFF: usize = ACPI_TABLE_HEADER_SIZE + size_of::(); +const MADT_ENTRIES_OFF: usize = ACPI_TABLE_HEADER_SIZE + 2 * size_of::(); + +const MADT_TYPE_LOCAL_APIC: u8 = 0; +const MADT_TYPE_IO_APIC: u8 = 1; +const MADT_TYPE_INT_SRC_OVERRIDE: u8 = 2; +const MADT_TYPE_LAPIC_NMI: u8 = 4; + +const MADT_LOCAL_APIC_LEN: u8 = 8; +const MADT_IO_APIC_LEN: u8 = 12; +const MADT_INT_SRC_OVERRIDE_LEN: u8 = 10; +const MADT_LAPIC_NMI_LEN: u8 = 6; + +pub const MADT_FLAG_PCAT_COMPAT: u32 = 1; +pub const MADT_LAPIC_ENABLED: u32 = 1; + +const MADT_INT_POLARITY_ACTIVE_HIGH: u16 = 0x01; +const MADT_INT_POLARITY_ACTIVE_LOW: u16 = 0x03; +const MADT_INT_TRIGGER_EDGE: u16 = 0x04; +const MADT_INT_TRIGGER_LEVEL: u16 = 0x0c; +pub const MADT_INT_LEVEL_ACTIVE_LOW: u16 = + MADT_INT_POLARITY_ACTIVE_LOW | MADT_INT_TRIGGER_LEVEL; +pub const MADT_INT_EDGE_ACTIVE_HIGH: u16 = + MADT_INT_POLARITY_ACTIVE_HIGH | MADT_INT_TRIGGER_EDGE; +pub const MADT_INT_LEVEL_ACTIVE_HIGH: u16 = + MADT_INT_POLARITY_ACTIVE_HIGH | MADT_INT_TRIGGER_LEVEL; + +pub const ISA_BUS: u8 = 0; +pub const ISA_IRQ_TIMER: u8 = 0; +pub const ISA_IRQ_SCI: u8 = 9; +pub const GSI_TIMER: u32 = 2; +pub const GSI_SCI: u32 = 9; + +pub const ACPI_PROCESSOR_ALL: u8 = 0xff; +pub const MADT_INT_FLAGS_DEFAULT: u16 = 0; +pub const LINT1: u8 = 1; + +pub struct Madt { + data: Vec, +} + +impl Madt { + pub fn new(local_apic_addr: u32) -> Self { + let header = AcpiTableHeader::new(*b"APIC", 5); + let mut data = vec![0u8; MADT_ENTRIES_OFF]; + data[..ACPI_TABLE_HEADER_SIZE].copy_from_slice(header.as_bytes()); + data[MADT_LOCAL_APIC_ADDR_OFF + ..MADT_LOCAL_APIC_ADDR_OFF + size_of::()] + .copy_from_slice(&local_apic_addr.to_le_bytes()); + data[MADT_FLAGS_OFF..MADT_FLAGS_OFF + size_of::()] + .copy_from_slice(&MADT_FLAG_PCAT_COMPAT.to_le_bytes()); + Self { data } + } + + pub fn add_local_apic(&mut self, processor_id: u8, apic_id: u8, flags: u32) { + self.data.push(MADT_TYPE_LOCAL_APIC); + self.data.push(MADT_LOCAL_APIC_LEN); + self.data.push(processor_id); + self.data.push(apic_id); + self.data.extend_from_slice(&flags.to_le_bytes()); + } + + pub fn add_io_apic(&mut self, id: u8, addr: u32, gsi_base: u32) { + self.data.push(MADT_TYPE_IO_APIC); + self.data.push(MADT_IO_APIC_LEN); + self.data.push(id); + self.data.push(0); + self.data.extend_from_slice(&addr.to_le_bytes()); + self.data.extend_from_slice(&gsi_base.to_le_bytes()); + } + + pub fn add_int_src_override( + &mut self, + bus: u8, + source: u8, + gsi: u32, + flags: u16, + ) { + self.data.push(MADT_TYPE_INT_SRC_OVERRIDE); + self.data.push(MADT_INT_SRC_OVERRIDE_LEN); + self.data.push(bus); + self.data.push(source); + self.data.extend_from_slice(&gsi.to_le_bytes()); + self.data.extend_from_slice(&flags.to_le_bytes()); + } + + pub fn add_lapic_nmi(&mut self, processor_uid: u8, flags: u16, lint: u8) { + self.data.push(MADT_TYPE_LAPIC_NMI); + self.data.push(MADT_LAPIC_NMI_LEN); + self.data.push(processor_uid); + self.data.extend_from_slice(&flags.to_le_bytes()); + self.data.push(lint); + } + + pub fn finish(mut self) -> Vec { + let length = self.data.len() as u32; + self.data[ACPI_TABLE_LENGTH_OFFSET + ..ACPI_TABLE_LENGTH_OFFSET + size_of::()] + .copy_from_slice(&length.to_le_bytes()); + self.data + } +} + #[cfg(test)] mod tests { use super::*; @@ -334,5 +438,9 @@ mod tests { let fadt = Fadt::new(); let fadt_data = fadt.finish(); assert_eq!(&fadt_data[0..4], b"FACP"); + + let madt = Madt::new(0xFEE0_0000); + let madt_data = madt.finish(); + assert_eq!(&madt_data[0..4], b"APIC"); } } diff --git a/lib/propolis/src/hw/qemu/fwcfg.rs b/lib/propolis/src/hw/qemu/fwcfg.rs index e773996ef..447f11214 100644 --- a/lib/propolis/src/hw/qemu/fwcfg.rs +++ b/lib/propolis/src/hw/qemu/fwcfg.rs @@ -1476,7 +1476,7 @@ pub mod formats { pub const ACPI_TABLES_FWCFG_NAME: &str = "etc/acpi/tables"; pub const ACPI_RSDP_FWCFG_NAME: &str = "etc/acpi/rsdp"; - pub use crate::firmware::acpi::{Dsdt, Fadt, Rsdt, Rsdp, Xsdt}; + pub use crate::firmware::acpi::{Dsdt, Fadt, Madt, Rsdt, Rsdp, Xsdt}; #[cfg(test)] mod test_table_loader { From 17a22364b613fbe72e4709dfd0bf073fc984f1fc Mon Sep 17 00:00:00 2001 From: Amey Narkhede Date: Mon, 29 Dec 2025 22:23:57 +0000 Subject: [PATCH 05/47] Add MCFG and HPET tables Add builders for MCFG and HPET ACPI tables. MCFG describes the PCIe ECAM base address, PCIe segment group and bus number range for firmware to locate PCI Express configuration space. HPET describes the HPET hardware to the guest. The table uses the bhyve HPET hardware ID (0x80860701) and maps to the standard HPET MMIO address at 0xfed00000. Signed-off-by: Amey Narkhede --- lib/propolis/src/firmware/acpi/mod.rs | 2 +- lib/propolis/src/firmware/acpi/tables.rs | 84 ++++++++++++++++++++++++ lib/propolis/src/hw/qemu/fwcfg.rs | 2 +- 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/lib/propolis/src/firmware/acpi/mod.rs b/lib/propolis/src/firmware/acpi/mod.rs index 3616ec759..256c8f241 100644 --- a/lib/propolis/src/firmware/acpi/mod.rs +++ b/lib/propolis/src/firmware/acpi/mod.rs @@ -6,4 +6,4 @@ pub mod tables; -pub use tables::{Dsdt, Fadt, Madt, Rsdt, Rsdp, Xsdt}; +pub use tables::{Dsdt, Fadt, Hpet, Madt, Mcfg, Rsdt, Rsdp, Xsdt}; diff --git a/lib/propolis/src/firmware/acpi/tables.rs b/lib/propolis/src/firmware/acpi/tables.rs index 98a93c822..32f599d86 100644 --- a/lib/propolis/src/firmware/acpi/tables.rs +++ b/lib/propolis/src/firmware/acpi/tables.rs @@ -416,6 +416,82 @@ impl Madt { } } +const MCFG_ENTRIES_OFF: usize = ACPI_TABLE_HEADER_SIZE + 8; + +pub struct Mcfg { + data: Vec, +} + +impl Mcfg { + pub fn new() -> Self { + let header = AcpiTableHeader::new(*b"MCFG", 1); + let mut data = vec![0u8; MCFG_ENTRIES_OFF]; + data[..ACPI_TABLE_HEADER_SIZE].copy_from_slice(header.as_bytes()); + Self { data } + } + + pub fn add_allocation( + &mut self, + base_addr: u64, + segment_group: u16, + start_bus: u8, + end_bus: u8, + ) { + assert!(start_bus <= end_bus); + self.data.extend_from_slice(&base_addr.to_le_bytes()); + self.data.extend_from_slice(&segment_group.to_le_bytes()); + self.data.push(start_bus); + self.data.push(end_bus); + self.data.extend_from_slice(&[0u8; 4]); + } + + pub fn finish(mut self) -> Vec { + let length = self.data.len() as u32; + self.data[ACPI_TABLE_LENGTH_OFFSET + ..ACPI_TABLE_LENGTH_OFFSET + size_of::()] + .copy_from_slice(&length.to_le_bytes()); + self.data + } +} + +const HPET_HW_ID: u32 = 0x8086_0701; +const HPET_BASE_ADDR: u64 = 0xfed0_0000; +const HPET_DATA_SIZE: usize = 20; +const HPET_PAGE_PROTECT4: u8 = 1; + +const HPET_OFF_HW_ID: usize = ACPI_TABLE_HEADER_SIZE; +const HPET_OFF_BASE_ADDR: usize = ACPI_TABLE_HEADER_SIZE + 8; +const HPET_OFF_FLAGS: usize = ACPI_TABLE_HEADER_SIZE + 19; + +#[must_use = "call .finish() to get the HPET table bytes"] +pub struct Hpet { + data: Vec, +} + +impl Hpet { + pub fn new() -> Self { + let header = AcpiTableHeader::new(*b"HPET", 1); + let mut data = vec![0u8; ACPI_TABLE_HEADER_SIZE + HPET_DATA_SIZE]; + data[..ACPI_TABLE_HEADER_SIZE].copy_from_slice(header.as_bytes()); + + data[HPET_OFF_HW_ID..HPET_OFF_HW_ID + size_of::()] + .copy_from_slice(&HPET_HW_ID.to_le_bytes()); + data[HPET_OFF_BASE_ADDR..HPET_OFF_BASE_ADDR + size_of::()] + .copy_from_slice(&HPET_BASE_ADDR.to_le_bytes()); + data[HPET_OFF_FLAGS] = HPET_PAGE_PROTECT4; + + Self { data } + } + + pub fn finish(mut self) -> Vec { + let length = self.data.len() as u32; + self.data[ACPI_TABLE_LENGTH_OFFSET + ..ACPI_TABLE_LENGTH_OFFSET + size_of::()] + .copy_from_slice(&length.to_le_bytes()); + self.data + } +} + #[cfg(test)] mod tests { use super::*; @@ -442,5 +518,13 @@ mod tests { let madt = Madt::new(0xFEE0_0000); let madt_data = madt.finish(); assert_eq!(&madt_data[0..4], b"APIC"); + + let mcfg = Mcfg::new(); + let mcfg_data = mcfg.finish(); + assert_eq!(&mcfg_data[0..4], b"MCFG"); + + let hpet = Hpet::new(); + let hpet_data = hpet.finish(); + assert_eq!(&hpet_data[0..4], b"HPET"); } } diff --git a/lib/propolis/src/hw/qemu/fwcfg.rs b/lib/propolis/src/hw/qemu/fwcfg.rs index 447f11214..bb86005ba 100644 --- a/lib/propolis/src/hw/qemu/fwcfg.rs +++ b/lib/propolis/src/hw/qemu/fwcfg.rs @@ -1476,7 +1476,7 @@ pub mod formats { pub const ACPI_TABLES_FWCFG_NAME: &str = "etc/acpi/tables"; pub const ACPI_RSDP_FWCFG_NAME: &str = "etc/acpi/rsdp"; - pub use crate::firmware::acpi::{Dsdt, Fadt, Madt, Rsdt, Rsdp, Xsdt}; + pub use crate::firmware::acpi::{Dsdt, Fadt, Hpet, Madt, Mcfg, Rsdt, Rsdp, Xsdt}; #[cfg(test)] mod test_table_loader { From 1aac6c662fe511910e7e8d349cd6eadb83d50654 Mon Sep 17 00:00:00 2001 From: Amey Narkhede Date: Mon, 29 Dec 2025 22:32:52 +0000 Subject: [PATCH 06/47] Add FACS table Add the FACS table that provides a memory region for firmware/OS handshaking. The table includes the GlobalLock field for OS/firmware mutual exclusion during ACPI operations. We don't yet have support for GBL_EN handling[1], but expose the table to match OVMF's behaviour. [1]: https://github.com/oxidecomputer/propolis/issues/837 Signed-off-by: Amey Narkhede --- lib/propolis/src/firmware/acpi/mod.rs | 2 +- lib/propolis/src/firmware/acpi/tables.rs | 32 ++++++++++++++++++++++++ lib/propolis/src/hw/qemu/fwcfg.rs | 2 +- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/lib/propolis/src/firmware/acpi/mod.rs b/lib/propolis/src/firmware/acpi/mod.rs index 256c8f241..e26745dc0 100644 --- a/lib/propolis/src/firmware/acpi/mod.rs +++ b/lib/propolis/src/firmware/acpi/mod.rs @@ -6,4 +6,4 @@ pub mod tables; -pub use tables::{Dsdt, Fadt, Hpet, Madt, Mcfg, Rsdt, Rsdp, Xsdt}; +pub use tables::{Dsdt, Facs, Fadt, Hpet, Madt, Mcfg, Rsdt, Rsdp, Xsdt}; diff --git a/lib/propolis/src/firmware/acpi/tables.rs b/lib/propolis/src/firmware/acpi/tables.rs index 32f599d86..174d2ba58 100644 --- a/lib/propolis/src/firmware/acpi/tables.rs +++ b/lib/propolis/src/firmware/acpi/tables.rs @@ -492,6 +492,34 @@ impl Hpet { } } +pub const FACS_SIZE: usize = 64; +const FACS_SIGNATURE_OFF: usize = 0; +const FACS_LENGTH_OFF: usize = size_of::(); +const FACS_HW_SIGNATURE_OFF: usize = 2 * size_of::(); +const FACS_VERSION_OFF: usize = 32; + +pub struct Facs { + data: Vec, +} + +impl Facs { + pub fn new() -> Self { + let mut data = vec![0u8; FACS_SIZE]; + data[FACS_SIGNATURE_OFF..FACS_SIGNATURE_OFF + size_of::()] + .copy_from_slice(b"FACS"); + data[FACS_LENGTH_OFF..FACS_LENGTH_OFF + size_of::()] + .copy_from_slice(&(FACS_SIZE as u32).to_le_bytes()); + data[FACS_HW_SIGNATURE_OFF..FACS_HW_SIGNATURE_OFF + size_of::()] + .copy_from_slice(&0u32.to_le_bytes()); + data[FACS_VERSION_OFF] = 2; + Self { data } + } + + pub fn finish(self) -> Vec { + self.data + } +} + #[cfg(test)] mod tests { use super::*; @@ -526,5 +554,9 @@ mod tests { let hpet = Hpet::new(); let hpet_data = hpet.finish(); assert_eq!(&hpet_data[0..4], b"HPET"); + + let facs = Facs::new(); + let facs_data = facs.finish(); + assert_eq!(&facs_data[0..4], b"FACS"); } } diff --git a/lib/propolis/src/hw/qemu/fwcfg.rs b/lib/propolis/src/hw/qemu/fwcfg.rs index bb86005ba..05d6b6c17 100644 --- a/lib/propolis/src/hw/qemu/fwcfg.rs +++ b/lib/propolis/src/hw/qemu/fwcfg.rs @@ -1476,7 +1476,7 @@ pub mod formats { pub const ACPI_TABLES_FWCFG_NAME: &str = "etc/acpi/tables"; pub const ACPI_RSDP_FWCFG_NAME: &str = "etc/acpi/rsdp"; - pub use crate::firmware::acpi::{Dsdt, Fadt, Hpet, Madt, Mcfg, Rsdt, Rsdp, Xsdt}; + pub use crate::firmware::acpi::{Dsdt, Facs, Fadt, Hpet, Madt, Mcfg, Rsdt, Rsdp, Xsdt}; #[cfg(test)] mod test_table_loader { From 99a2d12aa9bb46e9170ecdd3ee5e429a415e4726 Mon Sep 17 00:00:00 2001 From: Amey Narkhede Date: Mon, 29 Dec 2025 05:59:02 +0000 Subject: [PATCH 07/47] Define AML opcode constants Define bytecode opcodes for AML generation per ACPI Specification Chapter 20 [1]. Includes namespace modifiers, named objects, data object prefixes, name path prefixes, local/argument references, control flow and logical/arithmetic operators. These constants will be used in subsequent commits to generate AML bytecode which would enable us to generate ACPI tables ourselves. [1]: https://uefi.org/specs/ACPI/6.5/20_AML_Specification.html Signed-off-by: Amey Narkhede --- lib/propolis/src/firmware/acpi/opcodes.rs | 116 ++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 lib/propolis/src/firmware/acpi/opcodes.rs diff --git a/lib/propolis/src/firmware/acpi/opcodes.rs b/lib/propolis/src/firmware/acpi/opcodes.rs new file mode 100644 index 000000000..2aa56ab27 --- /dev/null +++ b/lib/propolis/src/firmware/acpi/opcodes.rs @@ -0,0 +1,116 @@ +// 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/. + +//! AML opcode constants. +//! +//! See ACPI spec section 20: + +// Namespace modifier objects +pub const SCOPE_OP: u8 = 0x10; +pub const NAME_OP: u8 = 0x08; + +// Named objects (require EXT_OP_PREFIX) +pub const EXT_OP_PREFIX: u8 = 0x5B; +pub const DEVICE_OP: u8 = 0x82; // ExtOp +pub const PROCESSOR_OP: u8 = 0x83; // ExtOp +pub const POWER_RES_OP: u8 = 0x84; // ExtOp +pub const THERMAL_ZONE_OP: u8 = 0x85; // ExtOp +pub const FIELD_OP: u8 = 0x81; // ExtOp +pub const OP_REGION_OP: u8 = 0x80; // ExtOp + +// Method +pub const METHOD_OP: u8 = 0x14; + +// Data objects +pub const ZERO_OP: u8 = 0x00; +pub const ONE_OP: u8 = 0x01; +pub const ONES_OP: u8 = 0xFF; +pub const BYTE_PREFIX: u8 = 0x0A; +pub const WORD_PREFIX: u8 = 0x0B; +pub const DWORD_PREFIX: u8 = 0x0C; +pub const QWORD_PREFIX: u8 = 0x0E; +pub const STRING_PREFIX: u8 = 0x0D; +pub const BUFFER_OP: u8 = 0x11; +pub const PACKAGE_OP: u8 = 0x12; +pub const VAR_PACKAGE_OP: u8 = 0x13; + +// Name prefixes +pub const DUAL_NAME_PREFIX: u8 = 0x2E; +pub const MULTI_NAME_PREFIX: u8 = 0x2F; +pub const ROOT_PREFIX: u8 = 0x5C; // '\' +pub const PARENT_PREFIX: u8 = 0x5E; // '^' +pub const NULL_NAME: u8 = 0x00; + +// Local and argument references +pub const LOCAL0_OP: u8 = 0x60; +pub const LOCAL1_OP: u8 = 0x61; +pub const LOCAL2_OP: u8 = 0x62; +pub const LOCAL3_OP: u8 = 0x63; +pub const LOCAL4_OP: u8 = 0x64; +pub const LOCAL5_OP: u8 = 0x65; +pub const LOCAL6_OP: u8 = 0x66; +pub const LOCAL7_OP: u8 = 0x67; +pub const ARG0_OP: u8 = 0x68; +pub const ARG1_OP: u8 = 0x69; +pub const ARG2_OP: u8 = 0x6A; +pub const ARG3_OP: u8 = 0x6B; +pub const ARG4_OP: u8 = 0x6C; +pub const ARG5_OP: u8 = 0x6D; +pub const ARG6_OP: u8 = 0x6E; + +// Control flow +pub const IF_OP: u8 = 0xA0; +pub const ELSE_OP: u8 = 0xA1; +pub const WHILE_OP: u8 = 0xA2; +pub const RETURN_OP: u8 = 0xA4; +pub const BREAK_OP: u8 = 0xA5; +pub const CONTINUE_OP: u8 = 0x9F; + +// Logical operators +pub const LAND_OP: u8 = 0x90; +pub const LOR_OP: u8 = 0x91; +pub const LNOT_OP: u8 = 0x92; +pub const LEQUAL_OP: u8 = 0x93; +pub const LGREATER_OP: u8 = 0x94; +pub const LLESS_OP: u8 = 0x95; + +// Arithmetic operators +pub const ADD_OP: u8 = 0x72; +pub const SUBTRACT_OP: u8 = 0x74; +pub const MULTIPLY_OP: u8 = 0x77; +pub const DIVIDE_OP: u8 = 0x78; +pub const AND_OP: u8 = 0x7B; +pub const OR_OP: u8 = 0x7D; +pub const XOR_OP: u8 = 0x7F; +pub const NOT_OP: u8 = 0x80; +pub const SHIFT_LEFT_OP: u8 = 0x79; +pub const SHIFT_RIGHT_OP: u8 = 0x7A; + +// Miscellaneous +pub const STORE_OP: u8 = 0x70; +pub const NOTIFY_OP: u8 = 0x86; +pub const SIZEOF_OP: u8 = 0x87; +pub const INDEX_OP: u8 = 0x88; +pub const DEREF_OF_OP: u8 = 0x83; +pub const REF_OF_OP: u8 = 0x71; + +// Resource template end tag +pub const END_TAG: u8 = 0x79; + +// Operation region address space types +pub const SYSTEM_MEMORY: u8 = 0x00; +pub const SYSTEM_IO: u8 = 0x01; +pub const PCI_CONFIG: u8 = 0x02; +pub const EMBEDDED_CONTROL: u8 = 0x03; +pub const SMBUS: u8 = 0x04; +pub const CMOS: u8 = 0x05; +pub const PCI_BAR_TARGET: u8 = 0x06; + +// Field access types +pub const ACCESS_ANY: u8 = 0x00; +pub const ACCESS_BYTE: u8 = 0x01; +pub const ACCESS_WORD: u8 = 0x02; +pub const ACCESS_DWORD: u8 = 0x03; +pub const ACCESS_QWORD: u8 = 0x04; +pub const ACCESS_BUFFER: u8 = 0x05; From 0ff3a6d1ba21ed6af6debf043bbdc6145cc0e92a Mon Sep 17 00:00:00 2001 From: Amey Narkhede Date: Mon, 29 Dec 2025 06:02:44 +0000 Subject: [PATCH 08/47] Add ACPI name encoding utilities Implement NameSeg and NameString encoding per ACPI Specification Section 20.2.2 [1]. Single segments encode as 4 bytes padded with underscores, dual segments use DualNamePrefix and three or more use MultiNamePrefix with a count byte. Also implement EISA ID compression for hardware identification strings like "PNP0A08". [1]: https://uefi.org/specs/ACPI/6.4_A/20_AML_Specification.html#name-objects-encoding Signed-off-by: Amey Narkhede --- lib/propolis/src/firmware/acpi/names.rs | 193 ++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 lib/propolis/src/firmware/acpi/names.rs diff --git a/lib/propolis/src/firmware/acpi/names.rs b/lib/propolis/src/firmware/acpi/names.rs new file mode 100644 index 000000000..94c6fffde --- /dev/null +++ b/lib/propolis/src/firmware/acpi/names.rs @@ -0,0 +1,193 @@ +// 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 name encoding utilities. +//! +//! See ACPI Specification 6.4, Section 20.2.2 for Name Objects Encoding. + +use super::opcodes::{DUAL_NAME_PREFIX, MULTI_NAME_PREFIX, ROOT_PREFIX}; + +pub const MAX_NAME_SEGS: usize = 255; + +pub const NAME_SEG_SIZE: usize = 4; + +/// Encode a 4-character ACPI name segment, padding shorter names with '_'. +pub fn encode_name_seg(name: &str) -> [u8; NAME_SEG_SIZE] { + assert!(name.len() <= NAME_SEG_SIZE, "name segment too long: {}", name); + assert!(!name.is_empty(), "name segment cannot be empty"); + + let bytes = name.as_bytes(); + + let first = bytes[0]; + assert!( + first.is_ascii_uppercase() || first == b'_', + "invalid first character in name segment: {}", + name + ); + + for &c in &bytes[1..] { + assert!( + c.is_ascii_uppercase() || c.is_ascii_digit() || c == b'_', + "invalid character in name segment: {}", + name + ); + } + + let mut seg = [b'_'; NAME_SEG_SIZE]; + seg[..bytes.len()].copy_from_slice(bytes); + seg +} + +/// Encode an ACPI name path (e.g. "\\_SB_.PCI0") into the buffer. +pub fn encode_name_string(path: &str, buf: &mut Vec) { + let mut chars = path.chars().peekable(); + + if chars.peek() == Some(&'\\') { + buf.push(ROOT_PREFIX); + chars.next(); + } + + while chars.peek() == Some(&'^') { + buf.push(super::opcodes::PARENT_PREFIX); + chars.next(); + } + + let remaining: String = chars.collect(); + if remaining.is_empty() { + return; + } + + let segments: Vec<&str> = remaining.split('.').collect(); + assert!( + segments.len() <= MAX_NAME_SEGS, + "too many name segments: {}", + segments.len() + ); + + match segments.len() { + 0 => {} + 1 => { + let seg = encode_name_seg(segments[0]); + buf.extend_from_slice(&seg); + } + 2 => { + buf.push(DUAL_NAME_PREFIX); + for s in &segments { + let seg = encode_name_seg(s); + buf.extend_from_slice(&seg); + } + } + n => { + buf.push(MULTI_NAME_PREFIX); + buf.push(n as u8); + for s in &segments { + let seg = encode_name_seg(s); + buf.extend_from_slice(&seg); + } + } + } +} + +/// Encode an EISA ID string (e.g. "PNP0A08") into a 32-bit value. +pub fn encode_eisaid(id: &str) -> u32 { + assert_eq!(id.len(), 7, "EISA ID must be exactly 7 characters: {}", id); + + let bytes = id.as_bytes(); + + for (i, &c) in bytes[0..3].iter().enumerate() { + assert!( + c.is_ascii_uppercase(), + "EISA ID manufacturer code must be A-Z at position {}: {}", + i, + id + ); + } + + let c1 = bytes[0] - b'A' + 1; + let c2 = bytes[1] - b'A' + 1; + let c3 = bytes[2] - b'A' + 1; + let mfg = ((c1 as u16) << 10) | ((c2 as u16) << 5) | (c3 as u16); + + let product = + u16::from_str_radix(&id[3..7], 16).expect("invalid hex in EISA ID"); + + let mfg_bytes = mfg.to_be_bytes(); + let product_bytes = product.to_be_bytes(); + + u32::from_le_bytes([ + mfg_bytes[0], + mfg_bytes[1], + product_bytes[0], + product_bytes[1], + ]) +} + +/// EISA ID wrapper that implements AmlWriter. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EisaId(pub u32); + +impl EisaId { + pub fn from_str(id: &str) -> Self { + Self(encode_eisaid(id)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn name_seg_encoding() { + assert_eq!(encode_name_seg("_SB_"), *b"_SB_"); + assert_eq!(encode_name_seg("PCI0"), *b"PCI0"); + assert_eq!(encode_name_seg("A"), *b"A___"); + assert_eq!(encode_name_seg("AB"), *b"AB__"); + } + + #[test] + #[should_panic(expected = "name segment too long")] + fn name_seg_rejects_long() { + encode_name_seg("TOOLONG"); + } + + #[test] + #[should_panic(expected = "invalid first character")] + fn name_seg_rejects_leading_digit() { + encode_name_seg("1BAD"); + } + + #[test] + fn name_string_encoding() { + let mut buf = Vec::new(); + encode_name_string("_SB_", &mut buf); + assert_eq!(buf, b"_SB_"); + + buf.clear(); + encode_name_string("\\_SB_", &mut buf); + assert_eq!(buf, vec![ROOT_PREFIX, b'_', b'S', b'B', b'_']); + + buf.clear(); + encode_name_string("_SB_.PCI0", &mut buf); + assert_eq!(buf[0], DUAL_NAME_PREFIX); + + buf.clear(); + encode_name_string("_SB_.PCI0.ISA_", &mut buf); + assert_eq!(buf[0], MULTI_NAME_PREFIX); + assert_eq!(buf[1], 3); + } + + #[test] + fn eisaid_encoding() { + assert_eq!(encode_eisaid("PNP0A08"), 0x080AD041); + assert_eq!(encode_eisaid("PNP0A03"), 0x030AD041); + assert_eq!(encode_eisaid("PNP0501"), 0x0105D041); + assert_eq!(EisaId::from_str("PNP0A08").0, 0x080AD041); + } + + #[test] + #[should_panic(expected = "EISA ID manufacturer code must be A-Z")] + fn eisaid_rejects_lowercase() { + encode_eisaid("pnp0A08"); + } +} From 50ee02c34735d6bbea30a95c6765ea252fcacf85 Mon Sep 17 00:00:00 2001 From: Amey Narkhede Date: Mon, 29 Dec 2025 06:05:49 +0000 Subject: [PATCH 09/47] Introduce AML bytecode generation Add AML bytecode generation to mainly support dynamically generating ACPI tables and control methods. The bytecode is built in a single pass by directly writing to the output buffer. AML scopes encode their length in a 1-4 byte PkgLength field at the start[1]. Since we don't know the final size until the scope's content is fully written, reserve 4 bytes when opening a scope upfront and splice in the actual encoded length when the scope closes. This avoids complexity of having to build an in memory tree and then walk it twice to measure and serialize. The RAII guards automatically close scopes and finalize the PkgLength on drop. Those guards hold a mutable borrow on the builder so the borrow checker won't let us close a parent while a child scope is still open. The limitation of this approach is that the content has to be written in output order but that is not a big issue for the use case of VM device descriptions. [1]: ACPI Specification Section 20.2.4 https://uefi.org/specs/ACPI/6.4_A/20_AML_Specification.html#package-length-encoding Signed-off-by: Amey Narkhede --- lib/propolis/src/firmware/acpi/aml.rs | 578 ++++++++++++++++++++++++++ 1 file changed, 578 insertions(+) create mode 100644 lib/propolis/src/firmware/acpi/aml.rs diff --git a/lib/propolis/src/firmware/acpi/aml.rs b/lib/propolis/src/firmware/acpi/aml.rs new file mode 100644 index 000000000..73981fab5 --- /dev/null +++ b/lib/propolis/src/firmware/acpi/aml.rs @@ -0,0 +1,578 @@ +// 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 super::names::{encode_name_string, EisaId}; +use super::opcodes::*; + +pub trait AmlWriter { + fn write_aml(&self, buf: &mut Vec); +} + +impl AmlWriter for u8 { + fn write_aml(&self, buf: &mut Vec) { + match *self { + 0 => buf.push(ZERO_OP), + 1 => buf.push(ONE_OP), + v => { + buf.push(BYTE_PREFIX); + buf.push(v); + } + } + } +} + +impl AmlWriter for u16 { + fn write_aml(&self, buf: &mut Vec) { + if *self <= u8::MAX as u16 { + (*self as u8).write_aml(buf); + } else { + buf.push(WORD_PREFIX); + buf.extend_from_slice(&self.to_le_bytes()); + } + } +} + +impl AmlWriter for u32 { + fn write_aml(&self, buf: &mut Vec) { + if *self <= u16::MAX as u32 { + (*self as u16).write_aml(buf); + } else { + buf.push(DWORD_PREFIX); + buf.extend_from_slice(&self.to_le_bytes()); + } + } +} + +impl AmlWriter for u64 { + fn write_aml(&self, buf: &mut Vec) { + if *self <= u32::MAX as u64 { + (*self as u32).write_aml(buf); + } else { + buf.push(QWORD_PREFIX); + buf.extend_from_slice(&self.to_le_bytes()); + } + } +} + +impl AmlWriter for &str { + fn write_aml(&self, buf: &mut Vec) { + buf.push(STRING_PREFIX); + buf.extend_from_slice(self.as_bytes()); + buf.push(0); + } +} + +impl AmlWriter for String { + fn write_aml(&self, buf: &mut Vec) { + self.as_str().write_aml(buf); + } +} + +impl AmlWriter for EisaId { + fn write_aml(&self, buf: &mut Vec) { + buf.push(DWORD_PREFIX); + buf.extend_from_slice(&self.0.to_le_bytes()); + } +} + +impl AmlWriter for Vec { + fn write_aml(&self, buf: &mut Vec) { + write_buffer(buf, self); + } +} + +impl AmlWriter for &[u8] { + fn write_aml(&self, buf: &mut Vec) { + write_buffer(buf, self); + } +} + +fn write_buffer(buf: &mut Vec, data: &[u8]) { + buf.push(BUFFER_OP); + + let mut size_buf = Vec::new(); + (data.len() as u64).write_aml(&mut size_buf); + + write_pkg_length(buf, size_buf.len() + data.len()); + buf.extend_from_slice(&size_buf); + buf.extend_from_slice(data); +} + +fn encode_pkg_length(total_len: usize) -> ([u8; MAX_PKG_LENGTH_BYTES], usize) { + let mut bytes = [0u8; MAX_PKG_LENGTH_BYTES]; + let size = pkg_length_size(total_len.saturating_sub(1)); + match size { + 1 => bytes[0] = total_len as u8, + 2 => { + bytes[0] = 0x40 | ((total_len & 0x0F) as u8); + bytes[1] = ((total_len >> 4) & 0xFF) as u8; + } + 3 => { + bytes[0] = 0x80 | ((total_len & 0x0F) as u8); + bytes[1] = ((total_len >> 4) & 0xFF) as u8; + bytes[2] = ((total_len >> 12) & 0xFF) as u8; + } + 4 => { + bytes[0] = 0xC0 | ((total_len & 0x0F) as u8); + bytes[1] = ((total_len >> 4) & 0xFF) as u8; + bytes[2] = ((total_len >> 12) & 0xFF) as u8; + bytes[3] = ((total_len >> 20) & 0xFF) as u8; + } + _ => unreachable!(), + } + (bytes, size) +} + +fn write_pkg_length(buf: &mut Vec, content_len: usize) { + let pkg_size = pkg_length_size(content_len); + let (bytes, size) = encode_pkg_length(pkg_size + content_len); + buf.extend_from_slice(&bytes[..size]); +} + +fn pkg_length_size(content_len: usize) -> usize { + if content_len < 0x3F - 1 { + 1 + } else if content_len < 0xFFF - 2 { + 2 + } else if content_len < 0xFFFFF - 3 { + 3 + } else { + 4 + } +} + +const MAX_PKG_LENGTH_BYTES: usize = 4; + +#[must_use = "call .finish() to get the AML bytes"] +pub struct AmlBuilder { + buf: Vec, +} + +impl AmlBuilder { + pub fn new() -> Self { + Self { buf: Vec::new() } + } + + pub fn scope(&mut self, name: &str) -> ScopeGuard<'_> { + ScopeGuard::new(self, name) + } + + pub fn device(&mut self, name: &str) -> DeviceGuard<'_> { + DeviceGuard::new(self, name) + } + + pub fn method( + &mut self, + name: &str, + arg_count: u8, + serialized: bool, + ) -> MethodGuard<'_> { + MethodGuard::new(self, name, arg_count, serialized) + } + + pub fn name(&mut self, name: &str, value: &T) { + self.buf.push(NAME_OP); + encode_name_string(name, &mut self.buf); + value.write_aml(&mut self.buf); + } + + pub fn name_package(&mut self, name: &str, elements: &[T]) { + self.buf.push(NAME_OP); + encode_name_string(name, &mut self.buf); + write_package(&mut self.buf, elements); + } + + pub fn raw(&mut self, bytes: &[u8]) { + self.buf.extend_from_slice(bytes); + } + + pub fn return_value(&mut self, value: &T) { + self.buf.push(RETURN_OP); + value.write_aml(&mut self.buf); + } + + pub fn finish(self) -> Vec { + self.buf + } + + pub fn len(&self) -> usize { + self.buf.len() + } + + pub fn is_empty(&self) -> bool { + self.buf.is_empty() + } + + pub fn as_bytes(&self) -> &[u8] { + &self.buf + } +} + +impl Default for AmlBuilder { + fn default() -> Self { + Self::new() + } +} + +fn write_package(buf: &mut Vec, elements: &[T]) { + buf.push(PACKAGE_OP); + + let mut content = Vec::new(); + content.push(elements.len() as u8); + for elem in elements { + elem.write_aml(&mut content); + } + + write_pkg_length(buf, content.len()); + buf.extend_from_slice(&content); +} + +/// ```compile_fail +/// use propolis::firmware::acpi::AmlBuilder; +/// let mut builder = AmlBuilder::new(); +/// { +/// let mut sb = builder.scope("\\_SB_"); +/// { +/// let mut pci = sb.device("PCI0"); +/// { +/// let dev = pci.device("DEV0"); +/// } +/// sb.device("DEV1"); // error: `sb` still borrowed by `pci` +/// } +/// } +/// ``` +pub struct ScopeGuard<'a> { + builder: &'a mut AmlBuilder, + start_pos: usize, + content_start: usize, +} + +impl<'a> ScopeGuard<'a> { + fn new(builder: &'a mut AmlBuilder, name: &str) -> Self { + builder.buf.push(SCOPE_OP); + let start_pos = builder.buf.len(); + builder.buf.extend_from_slice(&[0; MAX_PKG_LENGTH_BYTES]); + encode_name_string(name, &mut builder.buf); + let content_start = builder.buf.len(); + Self { builder, start_pos, content_start } + } + + pub fn scope(&mut self, name: &str) -> ScopeGuard<'_> { + ScopeGuard::new(self.builder, name) + } + + pub fn device(&mut self, name: &str) -> DeviceGuard<'_> { + DeviceGuard::new(self.builder, name) + } + + pub fn method( + &mut self, + name: &str, + arg_count: u8, + serialized: bool, + ) -> MethodGuard<'_> { + MethodGuard::new(self.builder, name, arg_count, serialized) + } + + pub fn name(&mut self, name: &str, value: &T) { + self.builder.name(name, value); + } + + pub fn name_package(&mut self, name: &str, elements: &[T]) { + self.builder.name_package(name, elements); + } + + pub fn processor(&mut self, name: &str, proc_id: u8) { + self.builder.buf.push(EXT_OP_PREFIX); + self.builder.buf.push(PROCESSOR_OP); + + let mut name_buf = Vec::new(); + encode_name_string(name, &mut name_buf); + write_pkg_length(&mut self.builder.buf, name_buf.len() + 6); + + self.builder.buf.extend_from_slice(&name_buf); + self.builder.buf.push(proc_id); + self.builder.buf.extend_from_slice(&[0u8; 4]); + self.builder.buf.push(0); + } + + pub fn raw(&mut self, bytes: &[u8]) { + self.builder.raw(bytes); + } +} + +impl Drop for ScopeGuard<'_> { + fn drop(&mut self) { + finalize_pkg_length( + &mut self.builder.buf, + self.start_pos, + self.content_start, + ); + } +} + +/// ```compile_fail +/// use propolis::firmware::acpi::AmlBuilder; +/// let mut builder = AmlBuilder::new(); +/// { +/// let mut sb = builder.scope("\\_SB_"); +/// { +/// let mut dev = sb.device("DEV0"); +/// { +/// let m = dev.method("_STA", 0, false); +/// } +/// dev.name("_UID", &0u32); +/// sb.device("DEV1"); // error: `sb` still borrowed by `dev` +/// } +/// } +/// ``` +pub struct DeviceGuard<'a> { + builder: &'a mut AmlBuilder, + start_pos: usize, + content_start: usize, +} + +impl<'a> DeviceGuard<'a> { + fn new(builder: &'a mut AmlBuilder, name: &str) -> Self { + builder.buf.push(EXT_OP_PREFIX); + builder.buf.push(DEVICE_OP); + let start_pos = builder.buf.len(); + builder.buf.extend_from_slice(&[0; MAX_PKG_LENGTH_BYTES]); + encode_name_string(name, &mut builder.buf); + let content_start = builder.buf.len(); + Self { builder, start_pos, content_start } + } + + pub fn device(&mut self, name: &str) -> DeviceGuard<'_> { + DeviceGuard::new(self.builder, name) + } + + pub fn method( + &mut self, + name: &str, + arg_count: u8, + serialized: bool, + ) -> MethodGuard<'_> { + MethodGuard::new(self.builder, name, arg_count, serialized) + } + + pub fn name(&mut self, name: &str, value: &T) { + self.builder.name(name, value); + } + + pub fn name_package(&mut self, name: &str, elements: &[T]) { + self.builder.name_package(name, elements); + } + + pub fn raw(&mut self, bytes: &[u8]) { + self.builder.raw(bytes); + } +} + +impl Drop for DeviceGuard<'_> { + fn drop(&mut self) { + finalize_pkg_length( + &mut self.builder.buf, + self.start_pos, + self.content_start, + ); + } +} + +/// ```compile_fail +/// use propolis::firmware::acpi::AmlBuilder; +/// let mut builder = AmlBuilder::new(); +/// { +/// let mut sb = builder.scope("\\_SB_"); +/// { +/// let mut dev = sb.device("DEV0"); +/// { +/// let m = dev.method("_STA", 0, false); +/// dev.method("_ON_", 0, false); // error: `dev` still borrowed by `m` +/// } +/// } +/// } +/// ``` +pub struct MethodGuard<'a> { + builder: &'a mut AmlBuilder, + start_pos: usize, + content_start: usize, +} + +impl<'a> MethodGuard<'a> { + fn new( + builder: &'a mut AmlBuilder, + name: &str, + arg_count: u8, + serialized: bool, + ) -> Self { + assert!(arg_count <= 7, "method can have at most 7 arguments"); + builder.buf.push(METHOD_OP); + let start_pos = builder.buf.len(); + builder.buf.extend_from_slice(&[0; MAX_PKG_LENGTH_BYTES]); + encode_name_string(name, &mut builder.buf); + let flags = arg_count | if serialized { 0x08 } else { 0 }; + builder.buf.push(flags); + let content_start = builder.buf.len(); + Self { builder, start_pos, content_start } + } + + pub fn return_value(&mut self, value: &T) { + self.builder.return_value(value); + } + + pub fn raw(&mut self, bytes: &[u8]) { + self.builder.raw(bytes); + } +} + +impl Drop for MethodGuard<'_> { + fn drop(&mut self) { + finalize_pkg_length( + &mut self.builder.buf, + self.start_pos, + self.content_start, + ); + } +} + +fn finalize_pkg_length( + buf: &mut Vec, + start_pos: usize, + content_start: usize, +) { + let name_len = content_start - start_pos - MAX_PKG_LENGTH_BYTES; + let body_len = buf.len() - content_start; + let content_len = name_len + body_len; + let pkg_size = pkg_length_size(content_len); + let (pkg_bytes, size) = encode_pkg_length(pkg_size + content_len); + + buf.splice( + start_pos..start_pos + MAX_PKG_LENGTH_BYTES, + pkg_bytes[..size].iter().copied(), + ); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn integer_encoding() { + let mut buf = Vec::new(); + 0u8.write_aml(&mut buf); + assert_eq!(buf, vec![ZERO_OP]); + + buf.clear(); + 1u8.write_aml(&mut buf); + assert_eq!(buf, vec![ONE_OP]); + + buf.clear(); + 42u8.write_aml(&mut buf); + assert_eq!(buf, vec![BYTE_PREFIX, 42]); + + buf.clear(); + 0x1234u16.write_aml(&mut buf); + assert_eq!(buf, vec![WORD_PREFIX, 0x34, 0x12]); + + buf.clear(); + 0xDEADBEEFu32.write_aml(&mut buf); + assert_eq!(buf, vec![DWORD_PREFIX, 0xEF, 0xBE, 0xAD, 0xDE]); + + buf.clear(); + 0x123456789ABCDEF0u64.write_aml(&mut buf); + assert_eq!(buf[0], QWORD_PREFIX); + } + + #[test] + fn string_encoding() { + let mut buf = Vec::new(); + "Hello".write_aml(&mut buf); + assert_eq!(buf, vec![STRING_PREFIX, b'H', b'e', b'l', b'l', b'o', 0]); + } + + #[test] + fn scope_with_named_object() { + let mut builder = AmlBuilder::new(); + { + let mut scope = builder.scope("_SB_"); + scope.name("TEST", &42u8); + } + let aml = builder.finish(); + + assert_eq!(aml[0], SCOPE_OP); + assert!(aml.windows(4).any(|w| w == b"TEST")); + } + + #[test] + fn device_in_scope() { + use super::super::names::EisaId; + + let mut builder = AmlBuilder::new(); + { + let mut sb = builder.scope("\\_SB_"); + { + let mut dev = sb.device("PCI0"); + dev.name("_HID", &EisaId::from_str("PNP0A08")); + } + } + let aml = builder.finish(); + + assert_eq!(aml[0], SCOPE_OP); + assert!(aml.windows(2).any(|w| w == [EXT_OP_PREFIX, DEVICE_OP])); + } + + #[test] + fn method_with_return() { + let mut builder = AmlBuilder::new(); + { + let mut scope = builder.scope("_SB_"); + { + let mut method = scope.method("_STA", 0, false); + method.return_value(&0x0Fu8); + } + } + let aml = builder.finish(); + + assert!(aml.windows(1).any(|w| w == [METHOD_OP])); + assert!(aml.windows(4).any(|w| w == b"_STA")); + } + + #[test] + fn nested_scopes() { + let mut builder = AmlBuilder::new(); + { + let mut sb = builder.scope("\\_SB_"); + { + let mut pci = sb.scope("PCI0"); + pci.name("_ADR", &0u32); + } + } + let aml = builder.finish(); + + let scope_count = aml.iter().filter(|&&b| b == SCOPE_OP).count(); + assert_eq!(scope_count, 2); + } + + #[test] + fn pkg_length_single_byte() { + let mut builder = AmlBuilder::new(); + { + let scope = builder.scope("TEST"); + drop(scope); + } + let aml = builder.finish(); + + assert_eq!(aml.len(), 6); + assert_eq!(aml[0], SCOPE_OP); + assert_eq!(aml[1], 5); + } + + #[test] + fn pkg_length_zero() { + let (bytes, size) = encode_pkg_length(0); + assert_eq!(size, 1); + assert_eq!(bytes[0], 0); + } +} From 4aa74fd7ad7f2e7014422f44ea5f20374edada02 Mon Sep 17 00:00:00 2001 From: Amey Narkhede Date: Mon, 29 Dec 2025 06:08:09 +0000 Subject: [PATCH 10/47] Support resource construction for ACPI methods Implement ResourceTemplateBuilder for constructing resource descriptors used in methods like _CRS. Supports QWord/DWord memory and I/O ranges, Word bus numbers and IRQ descriptors per ACPI Specification Section 6.4 [1]. [1]: https://uefi.org/specs/ACPI/6.4_A/06_Device_Configuration.html#resource-data-types-for-acpi Signed-off-by: Amey Narkhede Signed-off-by: glitzflitz --- lib/propolis/src/firmware/acpi/resources.rs | 316 ++++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 lib/propolis/src/firmware/acpi/resources.rs diff --git a/lib/propolis/src/firmware/acpi/resources.rs b/lib/propolis/src/firmware/acpi/resources.rs new file mode 100644 index 000000000..180161fbd --- /dev/null +++ b/lib/propolis/src/firmware/acpi/resources.rs @@ -0,0 +1,316 @@ +// 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 resource template encoding. +//! +//! See ACPI Specification 6.4, Section 6.4 "Resource Data Types for ACPI": +//! + +use super::aml::AmlWriter; + +// Table 6.27 "Small Resource Items" +// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#small-resource-data-type +const SMALL_IRQ_TAG: u8 = 0x04; +const SMALL_IO_TAG: u8 = 0x08; +const SMALL_END_TAG: u8 = 0x0F; + +// Table 6.40 "Large Resource Items" +// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#large-resource-data-type +const LARGE_DWORD_ADDR_SPACE: u8 = 0x07; +const LARGE_WORD_ADDR_SPACE: u8 = 0x08; +const LARGE_EXT_IRQ: u8 = 0x09; +const LARGE_QWORD_ADDR_SPACE: u8 = 0x0A; + +// Table 6.48 "QWord Address Space Descriptor" +// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#qword-address-space-descriptor +const ADDR_SPACE_TYPE_MEMORY: u8 = 0x00; +const ADDR_SPACE_TYPE_IO: u8 = 0x01; +const ADDR_SPACE_TYPE_BUS: u8 = 0x02; + +// Table 6.49 "Memory Resource Flag (Resource Type = 0) Definitions" +// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#qword-address-space-descriptor +const MEM_FLAG_READ_WRITE: u8 = 1 << 0; +const MEM_FLAG_CACHEABLE: u8 = 1 << 1; +const MEM_FLAG_WRITE_COMBINING: u8 = 1 << 2; + +// Table 6.56 "Extended Interrupt Descriptor Definition" +// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#extended-interrupt-descriptor +const EXT_IRQ_FLAG_CONSUMER: u8 = 1 << 0; +const EXT_IRQ_FLAG_EDGE: u8 = 1 << 1; +const EXT_IRQ_FLAG_ACTIVE_LOW: u8 = 1 << 2; +const EXT_IRQ_FLAG_SHARED: u8 = 1 << 3; + +// Table 6.33 "I/O Port Descriptor Definition" +// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#i-o-port-descriptor +const IO_DECODE_16BIT: u8 = 1 << 0; + +fn mem_type_flags(cacheable: bool, read_write: bool) -> u8 { + let mut f = 0u8; + if cacheable { + f |= MEM_FLAG_CACHEABLE | MEM_FLAG_WRITE_COMBINING; + } + if read_write { + f |= MEM_FLAG_READ_WRITE; + } + f +} + +/// Builder for ACPI resource templates used in _CRS, _PRS and _SRS methods. +#[must_use = "call .finish() to get the resource template bytes"] +pub struct ResourceTemplateBuilder { + buf: Vec, +} + +impl ResourceTemplateBuilder { + pub fn new() -> Self { + Self { buf: Vec::new() } + } + + pub fn qword_memory( + &mut self, + cacheable: bool, + read_write: bool, + min: u64, + max: u64, + translation: u64, + len: u64, + ) -> &mut Self { + self.qword_address_space( + ADDR_SPACE_TYPE_MEMORY, + cacheable, + read_write, + min, + max, + translation, + len, + ) + } + + #[allow(clippy::too_many_arguments)] + fn qword_address_space( + &mut self, + resource_type: u8, + cacheable: bool, + read_write: bool, + min: u64, + max: u64, + translation: u64, + len: u64, + ) -> &mut Self { + // 3 bytes of header + 5 u64 fields + let data_len = (3 + 5 * std::mem::size_of::()) as u16; + + self.buf.push(0x80 | LARGE_QWORD_ADDR_SPACE); + self.buf.extend_from_slice(&data_len.to_le_bytes()); + + self.buf.push(resource_type); + self.buf.push(0x00); // General flags + + let type_flags = if resource_type == ADDR_SPACE_TYPE_MEMORY { + mem_type_flags(cacheable, read_write) + } else { + 0x00 + }; + self.buf.push(type_flags); + + self.buf.extend_from_slice(&0u64.to_le_bytes()); // Granularity + self.buf.extend_from_slice(&min.to_le_bytes()); + self.buf.extend_from_slice(&max.to_le_bytes()); + self.buf.extend_from_slice(&translation.to_le_bytes()); + self.buf.extend_from_slice(&len.to_le_bytes()); + + self + } + + pub fn word_bus_number( + &mut self, + min: u16, + max: u16, + translation: u16, + len: u16, + ) -> &mut Self { + // 3 bytes of header + 5 u16 fields + let data_len = (3 + 5 * std::mem::size_of::()) as u16; + + self.buf.push(0x80 | LARGE_WORD_ADDR_SPACE); + self.buf.extend_from_slice(&data_len.to_le_bytes()); + + self.buf.push(ADDR_SPACE_TYPE_BUS); + self.buf.push(0x00); // General flags + self.buf.push(0x00); // Type-specific flags + + self.buf.extend_from_slice(&0u16.to_le_bytes()); // Granularity + self.buf.extend_from_slice(&min.to_le_bytes()); + self.buf.extend_from_slice(&max.to_le_bytes()); + self.buf.extend_from_slice(&translation.to_le_bytes()); + self.buf.extend_from_slice(&len.to_le_bytes()); + + self + } + + pub fn dword_memory( + &mut self, + cacheable: bool, + read_write: bool, + min: u32, + max: u32, + translation: u32, + len: u32, + ) -> &mut Self { + // 3 bytes of header + 5 u32 fields + let data_len = (3 + 5 * std::mem::size_of::()) as u16; + + self.buf.push(0x80 | LARGE_DWORD_ADDR_SPACE); + self.buf.extend_from_slice(&data_len.to_le_bytes()); + + self.buf.push(ADDR_SPACE_TYPE_MEMORY); + self.buf.push(0x00); // General flags + self.buf.push(mem_type_flags(cacheable, read_write)); + + self.buf.extend_from_slice(&0u32.to_le_bytes()); // Granularity + self.buf.extend_from_slice(&min.to_le_bytes()); + self.buf.extend_from_slice(&max.to_le_bytes()); + self.buf.extend_from_slice(&translation.to_le_bytes()); + self.buf.extend_from_slice(&len.to_le_bytes()); + + self + } + + pub fn io(&mut self, min: u16, max: u16, align: u8, len: u8) -> &mut Self { + // info(1) + min(2) + max(2) + align(1) + len(1) + let data_len = 1 + 2 * std::mem::size_of::() + 2; + + self.buf.push((SMALL_IO_TAG << 3) | data_len as u8); + self.buf.push(IO_DECODE_16BIT); + self.buf.extend_from_slice(&min.to_le_bytes()); + self.buf.extend_from_slice(&max.to_le_bytes()); + self.buf.push(align); + self.buf.push(len); + self + } + + pub fn irq(&mut self, irq_mask: u16) -> &mut Self { + let data_len = std::mem::size_of::(); + + self.buf.push((SMALL_IRQ_TAG << 3) | data_len as u8); + self.buf.extend_from_slice(&irq_mask.to_le_bytes()); + self + } + + pub fn extended_irq( + &mut self, + consumer: bool, + edge_triggered: bool, + active_low: bool, + shared: bool, + irqs: &[u32], + ) -> &mut Self { + let data_len = 2 + (irqs.len() * 4); + + self.buf.push(0x80 | LARGE_EXT_IRQ); + self.buf.extend_from_slice(&(data_len as u16).to_le_bytes()); + + let mut flags = 0u8; + if consumer { + flags |= EXT_IRQ_FLAG_CONSUMER; + } + if edge_triggered { + flags |= EXT_IRQ_FLAG_EDGE; + } + if active_low { + flags |= EXT_IRQ_FLAG_ACTIVE_LOW; + } + if shared { + flags |= EXT_IRQ_FLAG_SHARED; + } + self.buf.push(flags); + self.buf.push(irqs.len() as u8); + + for &irq in irqs { + self.buf.extend_from_slice(&irq.to_le_bytes()); + } + + self + } + + pub fn finish(mut self) -> Vec { + self.buf.push((SMALL_END_TAG << 3) | 1); // 1 byte checksum + self.buf.push(0x00); + self.buf + } +} + +impl AmlWriter for ResourceTemplateBuilder { + fn write_aml(&self, buf: &mut Vec) { + let mut data = Vec::with_capacity(self.buf.len() + 2); + data.extend_from_slice(&self.buf); + data.push((SMALL_END_TAG << 3) | 1); + data.push(0x00); + data.as_slice().write_aml(buf); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn small_descriptors() { + let io_data_len = 1 + 2 * std::mem::size_of::() + 2; + let irq_data_len = std::mem::size_of::(); + + let mut builder = ResourceTemplateBuilder::new(); + builder.io(0x3F8, 0x3F8, 1, 8); + let data = builder.finish(); + assert_eq!(data[0], (SMALL_IO_TAG << 3) | io_data_len as u8); + assert_eq!(data[1], IO_DECODE_16BIT); + + let mut builder = ResourceTemplateBuilder::new(); + builder.irq(0x0010); + let data = builder.finish(); + assert_eq!(data[0], (SMALL_IRQ_TAG << 3) | irq_data_len as u8); + } + + #[test] + fn large_descriptors() { + let mut builder = ResourceTemplateBuilder::new(); + builder.word_bus_number(0, 255, 0, 256); + let data = builder.finish(); + assert_eq!(data[0], 0x80 | LARGE_WORD_ADDR_SPACE); + assert_eq!(data[3], ADDR_SPACE_TYPE_BUS); + + let mut builder = ResourceTemplateBuilder::new(); + builder.qword_memory( + false, + true, + 0xE000_0000, + 0xEFFF_FFFF, + 0, + 0x1000_0000, + ); + let data = builder.finish(); + assert_eq!(data[0], 0x80 | LARGE_QWORD_ADDR_SPACE); + assert_eq!(data[3], ADDR_SPACE_TYPE_MEMORY); + } + + #[test] + fn chained_resources() { + let mut builder = ResourceTemplateBuilder::new(); + builder + .word_bus_number(0, 255, 0, 256) + .io(0xCF8, 0xCFF, 1, 8) + .qword_memory( + false, + true, + 0xE000_0000, + 0xEFFF_FFFF, + 0, + 0x1000_0000, + ); + let data = builder.finish(); + + let len = data.len(); + assert_eq!(data[len - 2], (SMALL_END_TAG << 3) | 1); + } +} From 317d357623343c2757e2bab4a32c79baf27d8b16 Mon Sep 17 00:00:00 2001 From: Amey Narkhede Date: Mon, 29 Dec 2025 06:12:25 +0000 Subject: [PATCH 11/47] Wire up firmware/acpi module exports Export public API for AML generation AmlBuilder, AmlWriter trait, guard types (ScopeGuard, DeviceGuard, MethodGuard), EisaId and ResourceTemplateBuilder. This would enable generating the dynamic bytecode used in tables like DSDT. Signed-off-by: Amey Narkhede --- lib/propolis/src/firmware/acpi/mod.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/propolis/src/firmware/acpi/mod.rs b/lib/propolis/src/firmware/acpi/mod.rs index e26745dc0..69ee8e1a4 100644 --- a/lib/propolis/src/firmware/acpi/mod.rs +++ b/lib/propolis/src/firmware/acpi/mod.rs @@ -4,6 +4,13 @@ //! ACPI table and AML bytecode generation. +pub mod aml; +pub mod names; +pub mod opcodes; +pub mod resources; pub mod tables; +pub use aml::{AmlBuilder, AmlWriter, DeviceGuard, MethodGuard, ScopeGuard}; +pub use names::EisaId; +pub use resources::ResourceTemplateBuilder; pub use tables::{Dsdt, Facs, Fadt, Hpet, Madt, Mcfg, Rsdt, Rsdp, Xsdt}; From 0a0b3f24d963939aacd3a2922d47ad656c01b649 Mon Sep 17 00:00:00 2001 From: Amey Narkhede Date: Tue, 30 Dec 2025 08:50:00 +0000 Subject: [PATCH 12/47] Generate DSDT with PCIe host bridge Add DSDT generation that provides the guest OS with device information via AML. The DSDT contains _SB.PCI0 describing the PCIe host bridge with bus number and MMIO resources. The ECAM is reserved via a separate PNP0C02 motherboard resources device (_SB.MRES) rather than in the PCI host bridge's _CRS. This is required by PCI Firmware Spec 3.2, sec 4.1.2. Also add the DsdtGenerator trait that will be implemented by each device in DSDT to expose its ACPI description. Signed-off-by: Amey Narkhede --- lib/propolis/src/firmware/acpi/aml.rs | 19 ++ lib/propolis/src/firmware/acpi/dsdt.rs | 253 ++++++++++++++++++++ lib/propolis/src/firmware/acpi/mod.rs | 2 + lib/propolis/src/firmware/acpi/resources.rs | 57 ++++- lib/propolis/src/lifecycle.rs | 10 + 5 files changed, 334 insertions(+), 7 deletions(-) create mode 100644 lib/propolis/src/firmware/acpi/dsdt.rs diff --git a/lib/propolis/src/firmware/acpi/aml.rs b/lib/propolis/src/firmware/acpi/aml.rs index 73981fab5..a00cdba07 100644 --- a/lib/propolis/src/firmware/acpi/aml.rs +++ b/lib/propolis/src/firmware/acpi/aml.rs @@ -228,6 +228,14 @@ fn write_package(buf: &mut Vec, elements: &[T]) { buf.extend_from_slice(&content); } +pub fn write_package_raw(buf: &mut Vec, num_elements: u8, content: &[u8]) { + buf.push(PACKAGE_OP); + let len = 1 + content.len(); + write_pkg_length(buf, len); + buf.push(num_elements); + buf.extend_from_slice(content); +} + /// ```compile_fail /// use propolis::firmware::acpi::AmlBuilder; /// let mut builder = AmlBuilder::new(); @@ -418,10 +426,21 @@ impl<'a> MethodGuard<'a> { Self { builder, start_pos, content_start } } + pub fn store_arg_to_name(&mut self, arg: u8, name: &str) { + self.builder.buf.push(STORE_OP); + self.builder.buf.push(ARG0_OP + arg); + encode_name_string(name, &mut self.builder.buf); + } + pub fn return_value(&mut self, value: &T) { self.builder.return_value(value); } + pub fn return_name(&mut self, name: &str) { + self.builder.buf.push(RETURN_OP); + encode_name_string(name, &mut self.builder.buf); + } + pub fn raw(&mut self, bytes: &[u8]) { self.builder.raw(bytes); } diff --git a/lib/propolis/src/firmware/acpi/dsdt.rs b/lib/propolis/src/firmware/acpi/dsdt.rs new file mode 100644 index 000000000..8c55ac81a --- /dev/null +++ b/lib/propolis/src/firmware/acpi/dsdt.rs @@ -0,0 +1,253 @@ +// 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 super::aml::{AmlBuilder, AmlWriter, ScopeGuard}; +use super::names::EisaId; +use super::resources::ResourceTemplateBuilder; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DsdtScope { + SystemBus, + PciRoot, +} + +pub trait DsdtGenerator { + fn dsdt_scope(&self) -> DsdtScope; + fn generate_dsdt(&self, scope: &mut ScopeGuard<'_>); +} + +const PCI_CONFIG_IO_BASE: u16 = 0x0CF8; +const PCI_CONFIG_IO_SIZE: u8 = 8; + +const PCI_IO_BASE: u16 = 0x1000; +const PCI_IO_LIMIT: u16 = 0xFFFF; +const PCI_IO_SIZE: u16 = PCI_IO_LIMIT - PCI_IO_BASE + 1; + +const PCI_INT_PINS: u8 = 4; +const PCI_GSI_BASE: u32 = 16; +const PCI_SLOTS: u8 = 32; +const PRT_ENTRY_SIZE: u8 = 4; +const PCI_ADR_ALL_FUNC: u32 = 0xFFFF; + +const IO_ALIGN_BYTE: u8 = 1; + +const SLP_TYP_S0: u8 = 5; +const SLP_TYP_S3: u8 = 1; +const SLP_TYP_S4: u8 = 6; +const SLP_TYP_S5: u8 = 7; + +#[derive(Clone, Copy)] +pub struct PcieConfig { + pub ecam_base: u64, + pub ecam_size: u64, + pub bus_start: u8, + pub bus_end: u8, + pub mmio32_base: u64, + pub mmio32_limit: u64, + pub mmio64_base: u64, + pub mmio64_limit: u64, +} + +pub struct DsdtConfig { + pub pcie: Option, +} + +struct PrtEntry { + slot: u8, + pin: u8, + gsi: u32, +} + +impl AmlWriter for PrtEntry { + fn write_aml(&self, buf: &mut Vec) { + let addr: u32 = ((self.slot as u32) << 16) | PCI_ADR_ALL_FUNC; + + let mut content = Vec::new(); + addr.write_aml(&mut content); + self.pin.write_aml(&mut content); + 0u8.write_aml(&mut content); + self.gsi.write_aml(&mut content); + + super::aml::write_package_raw(buf, PRT_ENTRY_SIZE, &content); + } +} + +pub fn build_dsdt_aml( + config: &DsdtConfig, + generators: &[&dyn DsdtGenerator], +) -> Vec { + let mut builder = AmlBuilder::new(); + + builder.name("PICM", &0u8); + + { + let mut pic = builder.method("_PIC", 1, false); + pic.store_arg_to_name(0, "PICM"); + } + + builder.name_package("\\_S0_", &[SLP_TYP_S0, SLP_TYP_S0, 0, 0]); + builder.name_package("\\_S3_", &[SLP_TYP_S3, SLP_TYP_S3, 0, 0]); + builder.name_package("\\_S4_", &[SLP_TYP_S4, SLP_TYP_S4, 0, 0]); + builder.name_package("\\_S5_", &[SLP_TYP_S5, SLP_TYP_S5, 0, 0]); + + { + let mut sb = builder.scope("\\_SB_"); + + if let Some(pcie) = &config.pcie { + build_pcie_host_bridge(&mut sb, pcie); + build_motherboard_resources(&mut sb, pcie); + } + + for generator in generators { + if generator.dsdt_scope() == DsdtScope::SystemBus { + generator.generate_dsdt(&mut sb); + } + } + } + + builder.finish() +} + +fn build_pcie_host_bridge( + sb: &mut super::aml::ScopeGuard<'_>, + pcie: &PcieConfig, +) { + let mut pci0 = sb.device("PCI0"); + + pci0.name("_HID", &EisaId::from_str("PNP0A08")); + pci0.name("_CID", &EisaId::from_str("PNP0A03")); + pci0.name("_SEG", &0u32); + pci0.name("_UID", &0u32); + pci0.name("_ADR", &0u32); + + let mut crs = ResourceTemplateBuilder::new(); + + let bus_count = (pcie.bus_end as u16) - (pcie.bus_start as u16) + 1; + crs.word_bus_number( + pcie.bus_start as u16, + pcie.bus_end as u16, + 0, + bus_count, + ); + + crs.io( + PCI_CONFIG_IO_BASE, + PCI_CONFIG_IO_BASE, + IO_ALIGN_BYTE, + PCI_CONFIG_IO_SIZE, + ); + + crs.io_range(PCI_IO_BASE, PCI_IO_LIMIT, PCI_IO_SIZE); + + let ecam_end = pcie.ecam_base + pcie.ecam_size; + + if pcie.ecam_base > pcie.mmio32_base { + let len = pcie.ecam_base - pcie.mmio32_base; + crs.dword_memory( + false, + true, + pcie.mmio32_base as u32, + (pcie.ecam_base - 1) as u32, + 0, + len as u32, + ); + } + + if pcie.mmio32_limit >= ecam_end { + let len = pcie.mmio32_limit - ecam_end + 1; + crs.dword_memory( + false, + true, + ecam_end as u32, + pcie.mmio32_limit as u32, + 0, + len as u32, + ); + } + + if pcie.mmio64_limit > pcie.mmio64_base { + let len = pcie.mmio64_limit - pcie.mmio64_base + 1; + crs.qword_memory( + false, + true, + pcie.mmio64_base, + pcie.mmio64_limit, + 0, + len, + ); + } + + pci0.name("_CRS", &crs); + + let mut prt_entries: Vec = Vec::new(); + for slot in 1..PCI_SLOTS { + for pin in 0..PCI_INT_PINS { + let gsi = PCI_GSI_BASE + + (((slot as u32) + (pin as u32)) % (PCI_INT_PINS as u32)); + prt_entries.push(PrtEntry { slot, pin, gsi }); + } + } + pci0.name_package("_PRT", &prt_entries); +} + +/// Build a PNP0C02 motherboard resources device to reserve ECAM space. +/// +/// Per PCI Firmware Spec 3.2, sec 4.1.2, the ECAM region must be reserved +/// by declaring a motherboard resource with _HID PNP0C02. The ECAM must +/// not be declared in the PCI host bridge's _CRS. +fn build_motherboard_resources( + sb: &mut super::aml::ScopeGuard<'_>, + pcie: &PcieConfig, +) { + let mut mres = sb.device("MRES"); + + mres.name("_HID", &EisaId::from_str("PNP0C02")); + + let mut crs = ResourceTemplateBuilder::new(); + if pcie.ecam_base + pcie.ecam_size - 1 >= (1u64 << 32) { + crs.qword_memory( + false, + false, + pcie.ecam_base, + pcie.ecam_base + pcie.ecam_size - 1, + 0, + pcie.ecam_size, + ); + } else { + crs.dword_memory( + false, + false, + pcie.ecam_base as u32, + (pcie.ecam_base + pcie.ecam_size - 1) as u32, + 0, + pcie.ecam_size as u32, + ); + } + mres.name("_CRS", &crs); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn basic() { + let config = DsdtConfig { + pcie: Some(PcieConfig { + ecam_base: 0xe000_0000, + ecam_size: 0x1000_0000, + bus_start: 0, + bus_end: 255, + mmio32_base: 0xc000_0000, + mmio32_limit: 0xfbff_ffff, + mmio64_base: 0x1_0000_0000, + mmio64_limit: 0xf_ffff_ffff, + }), + }; + let aml = build_dsdt_aml(&config, &[]); + 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"MRES")); + } +} diff --git a/lib/propolis/src/firmware/acpi/mod.rs b/lib/propolis/src/firmware/acpi/mod.rs index 69ee8e1a4..3fd6f871e 100644 --- a/lib/propolis/src/firmware/acpi/mod.rs +++ b/lib/propolis/src/firmware/acpi/mod.rs @@ -5,12 +5,14 @@ //! ACPI table and AML bytecode generation. pub mod aml; +pub mod dsdt; pub mod names; pub mod opcodes; pub mod resources; pub mod tables; pub use aml::{AmlBuilder, AmlWriter, DeviceGuard, MethodGuard, ScopeGuard}; +pub use dsdt::{build_dsdt_aml, DsdtConfig, DsdtGenerator, DsdtScope, PcieConfig}; pub use names::EisaId; pub use resources::ResourceTemplateBuilder; pub use tables::{Dsdt, Facs, Fadt, Hpet, Madt, Mcfg, Rsdt, Rsdp, Xsdt}; diff --git a/lib/propolis/src/firmware/acpi/resources.rs b/lib/propolis/src/firmware/acpi/resources.rs index 180161fbd..86004663b 100644 --- a/lib/propolis/src/firmware/acpi/resources.rs +++ b/lib/propolis/src/firmware/acpi/resources.rs @@ -17,6 +17,8 @@ const SMALL_END_TAG: u8 = 0x0F; // Table 6.40 "Large Resource Items" // https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#large-resource-data-type +const LARGE_RESOURCE_BIT: u8 = 0x80; +const LARGE_MEMORY32_FIXED: u8 = 0x06; const LARGE_DWORD_ADDR_SPACE: u8 = 0x07; const LARGE_WORD_ADDR_SPACE: u8 = 0x08; const LARGE_EXT_IRQ: u8 = 0x09; @@ -56,6 +58,15 @@ fn mem_type_flags(cacheable: bool, read_write: bool) -> u8 { f } +// Table 6.47 "General Flags" +// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#qword-address-space-descriptor +const ADDR_SPACE_FLAG_MIF: u8 = 1 << 2; +const ADDR_SPACE_FLAG_MAF: u8 = 1 << 3; + +// Table 6.50 "I/O Resource Flag (Resource Type = 1) Definitions" +// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#qword-address-space-descriptor +const IO_RANGE_ENTIRE: u8 = 0x03; + /// Builder for ACPI resource templates used in _CRS, _PRS and _SRS methods. #[must_use = "call .finish() to get the resource template bytes"] pub struct ResourceTemplateBuilder { @@ -101,7 +112,7 @@ impl ResourceTemplateBuilder { // 3 bytes of header + 5 u64 fields let data_len = (3 + 5 * std::mem::size_of::()) as u16; - self.buf.push(0x80 | LARGE_QWORD_ADDR_SPACE); + self.buf.push(LARGE_RESOURCE_BIT | LARGE_QWORD_ADDR_SPACE); self.buf.extend_from_slice(&data_len.to_le_bytes()); self.buf.push(resource_type); @@ -133,12 +144,12 @@ impl ResourceTemplateBuilder { // 3 bytes of header + 5 u16 fields let data_len = (3 + 5 * std::mem::size_of::()) as u16; - self.buf.push(0x80 | LARGE_WORD_ADDR_SPACE); + self.buf.push(LARGE_RESOURCE_BIT | LARGE_WORD_ADDR_SPACE); self.buf.extend_from_slice(&data_len.to_le_bytes()); self.buf.push(ADDR_SPACE_TYPE_BUS); self.buf.push(0x00); // General flags - self.buf.push(0x00); // Type-specific flags + self.buf.push(0x00); // Type specific flags self.buf.extend_from_slice(&0u16.to_le_bytes()); // Granularity self.buf.extend_from_slice(&min.to_le_bytes()); @@ -161,7 +172,7 @@ impl ResourceTemplateBuilder { // 3 bytes of header + 5 u32 fields let data_len = (3 + 5 * std::mem::size_of::()) as u16; - self.buf.push(0x80 | LARGE_DWORD_ADDR_SPACE); + self.buf.push(LARGE_RESOURCE_BIT | LARGE_DWORD_ADDR_SPACE); self.buf.extend_from_slice(&data_len.to_le_bytes()); self.buf.push(ADDR_SPACE_TYPE_MEMORY); @@ -190,6 +201,38 @@ impl ResourceTemplateBuilder { self } + pub fn io_range(&mut self, min: u16, max: u16, len: u16) -> &mut Self { + // 3 bytes of header + 5 u16 fields + let data_len = (3 + 5 * std::mem::size_of::()) as u16; + + self.buf.push(LARGE_RESOURCE_BIT | LARGE_WORD_ADDR_SPACE); + self.buf.extend_from_slice(&data_len.to_le_bytes()); + + self.buf.push(ADDR_SPACE_TYPE_IO); + self.buf.push(ADDR_SPACE_FLAG_MIF | ADDR_SPACE_FLAG_MAF); + self.buf.push(IO_RANGE_ENTIRE); + + self.buf.extend_from_slice(&0u16.to_le_bytes()); + self.buf.extend_from_slice(&min.to_le_bytes()); + self.buf.extend_from_slice(&max.to_le_bytes()); + self.buf.extend_from_slice(&0u16.to_le_bytes()); + self.buf.extend_from_slice(&len.to_le_bytes()); + + self + } + + pub fn fixed_memory(&mut self, base: u32, len: u32) -> &mut Self { + // info(1) + base(4) + len(4) + let data_len = (1 + 2 * std::mem::size_of::()) as u16; + + self.buf.push(LARGE_RESOURCE_BIT | LARGE_MEMORY32_FIXED); + self.buf.extend_from_slice(&data_len.to_le_bytes()); + self.buf.push(MEM_FLAG_READ_WRITE); + self.buf.extend_from_slice(&base.to_le_bytes()); + self.buf.extend_from_slice(&len.to_le_bytes()); + self + } + pub fn irq(&mut self, irq_mask: u16) -> &mut Self { let data_len = std::mem::size_of::(); @@ -208,7 +251,7 @@ impl ResourceTemplateBuilder { ) -> &mut Self { let data_len = 2 + (irqs.len() * 4); - self.buf.push(0x80 | LARGE_EXT_IRQ); + self.buf.push(LARGE_RESOURCE_BIT | LARGE_EXT_IRQ); self.buf.extend_from_slice(&(data_len as u16).to_le_bytes()); let mut flags = 0u8; @@ -277,7 +320,7 @@ mod tests { let mut builder = ResourceTemplateBuilder::new(); builder.word_bus_number(0, 255, 0, 256); let data = builder.finish(); - assert_eq!(data[0], 0x80 | LARGE_WORD_ADDR_SPACE); + assert_eq!(data[0], LARGE_RESOURCE_BIT | LARGE_WORD_ADDR_SPACE); assert_eq!(data[3], ADDR_SPACE_TYPE_BUS); let mut builder = ResourceTemplateBuilder::new(); @@ -290,7 +333,7 @@ mod tests { 0x1000_0000, ); let data = builder.finish(); - assert_eq!(data[0], 0x80 | LARGE_QWORD_ADDR_SPACE); + assert_eq!(data[0], LARGE_RESOURCE_BIT | LARGE_QWORD_ADDR_SPACE); assert_eq!(data[3], ADDR_SPACE_TYPE_MEMORY); } diff --git a/lib/propolis/src/lifecycle.rs b/lib/propolis/src/lifecycle.rs index 1d8e194c5..e4356b8f7 100644 --- a/lib/propolis/src/lifecycle.rs +++ b/lib/propolis/src/lifecycle.rs @@ -96,6 +96,16 @@ pub trait Lifecycle: Send + Sync + 'static { fn migrate(&'_ self) -> Migrator<'_> { Migrator::Empty } + + /// Returns this device as a [`DsdtGenerator`] if it contributes to DSDT. + /// + /// 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 crate::firmware::acpi::DsdtGenerator> { + None + } } /// Indicator for tracking [Lifecycle] states. From b54f0e17a065e6b2cab3c42a66036832ffd2c0b0 Mon Sep 17 00:00:00 2001 From: Amey Narkhede Date: Sun, 4 Jan 2026 18:06:23 +0000 Subject: [PATCH 13/47] Implement DsdtGenerator for LpcUart Since we can generation our own ACPI tables, implement DsdtGenerator trait for serial console device to expose it in generated DSDT. Signed-off-by: Amey Narkhede --- bin/propolis-server/src/lib/initializer.rs | 32 ++++++++++---- bin/propolis-standalone/src/main.rs | 36 +++++++++++---- lib/propolis/src/hw/uart/lpc.rs | 51 ++++++++++++++++++++-- 3 files changed, 99 insertions(+), 20 deletions(-) diff --git a/bin/propolis-server/src/lib/initializer.rs b/bin/propolis-server/src/lib/initializer.rs index ee7ebdac6..575509677 100644 --- a/bin/propolis-server/src/lib/initializer.rs +++ b/bin/propolis-server/src/lib/initializer.rs @@ -393,16 +393,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 (irq, port, uart_name) = match desc.num { + SerialPortNumber::Com1 => { + (ibmpc::IRQ_COM1, ibmpc::PORT_COM1, "COM1") + } + SerialPortNumber::Com2 => { + (ibmpc::IRQ_COM2, ibmpc::PORT_COM2, "COM2") + } + SerialPortNumber::Com3 => { + (ibmpc::IRQ_COM3, ibmpc::PORT_COM3, "COM3") + } + SerialPortNumber::Com4 => { + (ibmpc::IRQ_COM4, ibmpc::PORT_COM4, "COM4") + } }; - let dev = LpcUart::new(chipset.irq_pin(irq).unwrap()); + let dev = + LpcUart::new(chipset.irq_pin(irq).unwrap(), port, irq, uart_name); dev.set_autodiscard(true); - LpcUart::attach(&dev, &self.machine.bus_pio, port); + dev.attach(&self.machine.bus_pio); self.devices.insert(name.to_owned(), dev.clone()); if desc.num == SerialPortNumber::Com1 { assert!(com1.is_none()); @@ -899,9 +908,14 @@ 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( + chipset.irq_pin(ibmpc::IRQ_COM4).unwrap(), + ibmpc::PORT_COM4, + ibmpc::IRQ_COM4, + "COM4", + ); uart.set_autodiscard(true); - LpcUart::attach(&uart, &self.machine.bus_pio, ibmpc::PORT_COM4); + uart.attach(&self.machine.bus_pio); self.devices .insert(SpecKey::Name("softnpu-uart".to_string()), uart.clone()); diff --git a/bin/propolis-standalone/src/main.rs b/bin/propolis-standalone/src/main.rs index 507284faf..17d2903ba 100644 --- a/bin/propolis-standalone/src/main.rs +++ b/bin/propolis-standalone/src/main.rs @@ -1139,10 +1139,30 @@ 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( + chipset_lpc.irq_pin(ibmpc::IRQ_COM1).unwrap(), + ibmpc::PORT_COM1, + ibmpc::IRQ_COM1, + "COM1", + ); + let com2 = LpcUart::new( + chipset_lpc.irq_pin(ibmpc::IRQ_COM2).unwrap(), + ibmpc::PORT_COM2, + ibmpc::IRQ_COM2, + "COM2", + ); + let com3 = LpcUart::new( + chipset_lpc.irq_pin(ibmpc::IRQ_COM3).unwrap(), + ibmpc::PORT_COM3, + ibmpc::IRQ_COM3, + "COM3", + ); + let com4 = LpcUart::new( + chipset_lpc.irq_pin(ibmpc::IRQ_COM4).unwrap(), + ibmpc::PORT_COM4, + ibmpc::IRQ_COM4, + "COM4", + ); com1_sock.spawn( Arc::clone(&com1) as Arc, @@ -1156,10 +1176,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); + com2.attach(pio); + com3.attach(pio); + com4.attach(pio); guard.inventory.register_instance(&com1, "com1"); guard.inventory.register_instance(&com2, "com2"); guard.inventory.register_instance(&com3, "com3"); diff --git a/lib/propolis/src/hw/uart/lpc.rs b/lib/propolis/src/hw/uart/lpc.rs index 5c1714e1c..9bb17f27e 100644 --- a/lib/propolis/src/hw/uart/lpc.rs +++ b/lib/propolis/src/hw/uart/lpc.rs @@ -39,10 +39,18 @@ pub struct LpcUart { state: Mutex, notify_readable: NotifierCell, notify_writable: NotifierCell, + io_base: u16, + irq: u8, + name: &'static str, } impl LpcUart { - pub fn new(irq_pin: Box) -> Arc { + pub fn new( + irq_pin: Box, + io_base: u16, + irq: u8, + name: &'static str, + ) -> Arc { Arc::new(Self { state: Mutex::new(UartState { uart: Uart::new(), @@ -52,13 +60,17 @@ impl LpcUart { }), notify_readable: NotifierCell::new(), notify_writable: NotifierCell::new(), + io_base, + irq, + name, }) } - pub fn attach(self: &Arc, bus: &PioBus, port: u16) { + + pub fn attach(self: &Arc, bus: &PioBus) { let this = self.clone(); let piofn = Arc::new(move |_port: u16, rwo: RWOp| this.pio_rw(rwo)) as Arc; - bus.register(port, REGISTER_LEN as u16, piofn).unwrap(); + bus.register(self.io_base, REGISTER_LEN as u16, piofn).unwrap(); } fn pio_rw(&self, rwo: RWOp) { assert!(rwo.offset() < REGISTER_LEN); @@ -172,6 +184,12 @@ impl Lifecycle for LpcUart { let mut state = self.state.lock().unwrap(); state.paused = false; } + + fn as_dsdt_generator( + &self, + ) -> Option<&dyn crate::firmware::acpi::DsdtGenerator> { + Some(self) + } } impl MigrateSingle for LpcUart { fn export( @@ -194,3 +212,30 @@ impl MigrateSingle for LpcUart { Ok(()) } } + +impl crate::firmware::acpi::DsdtGenerator for LpcUart { + fn dsdt_scope(&self) -> crate::firmware::acpi::DsdtScope { + crate::firmware::acpi::DsdtScope::SystemBus + } + + fn generate_dsdt(&self, scope: &mut crate::firmware::acpi::ScopeGuard<'_>) { + use crate::firmware::acpi::{EisaId, ResourceTemplateBuilder}; + + let uid: u32 = match self.name { + "COM1" => 0, + "COM2" => 1, + "COM3" => 2, + "COM4" => 3, + _ => 0, + }; + + let mut dev = scope.device(self.name); + dev.name("_HID", &EisaId::from_str("PNP0501")); + dev.name("_UID", &uid); + + let mut crs = ResourceTemplateBuilder::new(); + crs.io(self.io_base, self.io_base, 1, REGISTER_LEN as u8); + crs.irq(1u16 << self.irq); + dev.name("_CRS", &crs); + } +} From 37e7b0f3574b959442cf31314b9ba2a530ffb9e2 Mon Sep 17 00:00:00 2001 From: Amey Narkhede Date: Tue, 30 Dec 2025 21:42:08 +0000 Subject: [PATCH 14/47] Add PS/2 controller in DSDT Add AT keyboard controller resources to allow guest to enumerate the i8042 controller. Only keyboard is added to match the OVMF's existing behaviour for now. Signed-off-by: Amey Narkhede --- lib/propolis/src/hw/ps2/ctrl.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/propolis/src/hw/ps2/ctrl.rs b/lib/propolis/src/hw/ps2/ctrl.rs index a201a685d..8023ceb37 100644 --- a/lib/propolis/src/hw/ps2/ctrl.rs +++ b/lib/propolis/src/hw/ps2/ctrl.rs @@ -605,6 +605,11 @@ impl Lifecycle for PS2Ctrl { fn migrate(&self) -> Migrator<'_> { Migrator::Single(self) } + fn as_dsdt_generator( + &self, + ) -> Option<&dyn crate::firmware::acpi::DsdtGenerator> { + Some(self) + } } impl MigrateSingle for PS2Ctrl { fn export( @@ -1090,6 +1095,28 @@ impl Default for PS2Mouse { } } +impl crate::firmware::acpi::DsdtGenerator for PS2Ctrl { + fn dsdt_scope(&self) -> crate::firmware::acpi::DsdtScope { + crate::firmware::acpi::DsdtScope::SystemBus + } + + fn generate_dsdt(&self, scope: &mut crate::firmware::acpi::ScopeGuard<'_>) { + use crate::firmware::acpi::{EisaId, ResourceTemplateBuilder}; + use crate::hw::ibmpc; + + const PS2_KBD_IRQ: u8 = 1; + + let mut kbd = scope.device("KBD_"); + kbd.name("_HID", &EisaId::from_str("PNP0303")); + + let mut crs = ResourceTemplateBuilder::new(); + crs.io(ibmpc::PORT_PS2_DATA, ibmpc::PORT_PS2_DATA, 1, 1); + crs.io(ibmpc::PORT_PS2_CMD_STATUS, ibmpc::PORT_PS2_CMD_STATUS, 1, 1); + crs.irq(1u16 << PS2_KBD_IRQ); + kbd.name("_CRS", &crs); + } +} + pub mod migrate { use crate::migrate::*; From d68d8c23843bcf3a9ababacd957368c140b97278 Mon Sep 17 00:00:00 2001 From: Amey Narkhede Date: Sun, 4 Jan 2026 18:29:34 +0000 Subject: [PATCH 15/47] Add Qemu pvpanic device to DSDT Implement DsdtGenerator for QemuPvPanic to export it via new DSDT. Signed-off-by: Amey Narkhede --- lib/propolis/src/hw/qemu/pvpanic.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/propolis/src/hw/qemu/pvpanic.rs b/lib/propolis/src/hw/qemu/pvpanic.rs index 093d658e4..3bd61359e 100644 --- a/lib/propolis/src/hw/qemu/pvpanic.rs +++ b/lib/propolis/src/hw/qemu/pvpanic.rs @@ -108,4 +108,29 @@ impl Lifecycle for QemuPvpanic { fn type_name(&self) -> &'static str { DEVICE_NAME } + + fn as_dsdt_generator( + &self, + ) -> Option<&dyn crate::firmware::acpi::DsdtGenerator> { + Some(self) + } +} + +impl crate::firmware::acpi::DsdtGenerator for QemuPvpanic { + fn dsdt_scope(&self) -> crate::firmware::acpi::DsdtScope { + crate::firmware::acpi::DsdtScope::SystemBus + } + + fn generate_dsdt(&self, scope: &mut crate::firmware::acpi::ScopeGuard<'_>) { + use crate::firmware::acpi::ResourceTemplateBuilder; + + let mut dev = scope.device("PEVT"); + dev.name("_HID", &"QEMU0001"); + + let mut crs = ResourceTemplateBuilder::new(); + crs.io(Self::IOPORT, Self::IOPORT, 1, 1); + dev.name("_CRS", &crs); + + dev.name("_STA", &0x0Fu32); + } } From 4eedc0e5e68b7cda95a7a69de2a0b3534e6c0000 Mon Sep 17 00:00:00 2001 From: Amey Narkhede Date: Tue, 30 Dec 2025 01:18:32 +0000 Subject: [PATCH 16/47] Add PCIe _OSC method for OS capability negotiation The OS calls _OSC on the PCIe host bridge to negotiate control of native PCIe features like hotplug, AER and PME. Without _OSC, Linux logs warning about missing capability negotiation(_OSC: platform retains control of PCIe features (AE_NOT_FOUND). Since as of now we don't have support for any PCIe handling, no capabilities are exposed. In future when PCIe handling is implemented the supported bits can be simply unmasked to expose them to the guest. Also to simplify the aml generation of _OSC itself introduce some high level wrappers around aml generation. [1]: https://learn.microsoft.com/en-us/windows-hardware/drivers/pci/enabling-pci-express-native-control Signed-off-by: Amey Narkhede --- lib/propolis/src/firmware/acpi/aml.rs | 79 ++++++++++++++++++++++- lib/propolis/src/firmware/acpi/dsdt.rs | 40 ++++++++++++ lib/propolis/src/firmware/acpi/mod.rs | 2 +- lib/propolis/src/firmware/acpi/names.rs | 63 ++++++++++++++++++ lib/propolis/src/firmware/acpi/opcodes.rs | 1 + 5 files changed, 183 insertions(+), 2 deletions(-) diff --git a/lib/propolis/src/firmware/acpi/aml.rs b/lib/propolis/src/firmware/acpi/aml.rs index a00cdba07..e021c4078 100644 --- a/lib/propolis/src/firmware/acpi/aml.rs +++ b/lib/propolis/src/firmware/acpi/aml.rs @@ -2,7 +2,7 @@ // 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 super::names::{encode_name_string, EisaId}; +use super::names::{encode_name_string, EisaId, UUID_SIZE}; use super::opcodes::*; pub trait AmlWriter { @@ -441,9 +441,86 @@ impl<'a> MethodGuard<'a> { encode_name_string(name, &mut self.builder.buf); } + pub fn return_arg(&mut self, n: u8) { + assert!(n <= 6); + self.builder.buf.push(RETURN_OP); + self.builder.buf.push(ARG0_OP + n); + } + pub fn raw(&mut self, bytes: &[u8]) { self.builder.raw(bytes); } + + pub fn create_dword_field(&mut self, source: u8, offset: u8, name: &str) { + self.builder.buf.push(CREATE_DWORD_FIELD_OP); + self.builder.buf.push(source); + if offset == 0 { + self.builder.buf.push(ZERO_OP); + } else { + self.builder.buf.push(BYTE_PREFIX); + self.builder.buf.push(offset); + } + encode_name_string(name, &mut self.builder.buf); + } + + pub fn if_uuid_equal( + &mut self, + uuid: &[u8; 16], + body: impl FnOnce(&mut Self), + ) { + self.builder.buf.push(IF_OP); + let start = self.builder.buf.len(); + self.builder.buf.extend_from_slice(&[0; MAX_PKG_LENGTH_BYTES]); + + self.builder.buf.push(LEQUAL_OP); + self.builder.buf.push(ARG0_OP); + + self.builder.buf.push(BUFFER_OP); + let buffer_start = self.builder.buf.len(); + self.builder.buf.extend_from_slice(&[0; MAX_PKG_LENGTH_BYTES]); + self.builder.buf.push(BYTE_PREFIX); + self.builder.buf.push(UUID_SIZE as u8); + let buffer_content_start = self.builder.buf.len(); + self.builder.buf.extend_from_slice(uuid); + finalize_pkg_length( + &mut self.builder.buf, + buffer_start, + buffer_content_start, + ); + + let content_start = self.builder.buf.len(); + body(self); + finalize_pkg_length(&mut self.builder.buf, start, content_start); + } + + pub fn else_block(&mut self, body: impl FnOnce(&mut Self)) { + self.builder.buf.push(ELSE_OP); + let start = self.builder.buf.len(); + self.builder.buf.extend_from_slice(&[0; MAX_PKG_LENGTH_BYTES]); + let content_start = self.builder.buf.len(); + body(self); + finalize_pkg_length(&mut self.builder.buf, start, content_start); + } + + pub fn and_to(&mut self, name: &str, mask: u32) { + self.builder.buf.push(AND_OP); + encode_name_string(name, &mut self.builder.buf); + mask.write_aml(&mut self.builder.buf); + encode_name_string(name, &mut self.builder.buf); + } + + pub fn or_to(&mut self, name: &str, value: u32) { + self.builder.buf.push(OR_OP); + encode_name_string(name, &mut self.builder.buf); + value.write_aml(&mut self.builder.buf); + encode_name_string(name, &mut self.builder.buf); + } + + pub fn store(&mut self, source: &str, dest: &str) { + self.builder.buf.push(STORE_OP); + encode_name_string(source, &mut self.builder.buf); + encode_name_string(dest, &mut self.builder.buf); + } } impl Drop for MethodGuard<'_> { diff --git a/lib/propolis/src/firmware/acpi/dsdt.rs b/lib/propolis/src/firmware/acpi/dsdt.rs index 8c55ac81a..d54dba9d0 100644 --- a/lib/propolis/src/firmware/acpi/dsdt.rs +++ b/lib/propolis/src/firmware/acpi/dsdt.rs @@ -189,6 +189,46 @@ fn build_pcie_host_bridge( } } pci0.name_package("_PRT", &prt_entries); + pci0.name("SUPP", &0u32); + + build_pcie_osc_method(&mut pci0); +} + +fn build_pcie_osc_method(dev: &mut super::aml::DeviceGuard<'_>) { + use super::names::{encode_uuid, UUID_SIZE}; + use super::opcodes::*; + + const PCIE_UUID: [u8; UUID_SIZE] = + encode_uuid("33DB4D5B-1FF7-401C-9657-7441C03DD766"); + const OSC_STATUS_UNSUPPORT_UUID: u32 = 1 << 2; + const OSC_CTRL_PCIE_HP: u32 = 1 << 0; + const OSC_CTRL_SHPC_HP: u32 = 1 << 1; + const OSC_CTRL_PCIE_PME: u32 = 1 << 2; + const OSC_CTRL_PCIE_AER: u32 = 1 << 3; + const OSC_CTRL_PCIE_CAP: u32 = 1 << 4; + + let unsupported_mask = !(OSC_CTRL_PCIE_HP + | OSC_CTRL_SHPC_HP + | OSC_CTRL_PCIE_PME + | OSC_CTRL_PCIE_AER + | OSC_CTRL_PCIE_CAP); + + let mut osc = dev.method("_OSC", 4, false); + + osc.create_dword_field(ARG3_OP, 0, "CDW1"); + osc.create_dword_field(ARG3_OP, 4, "CDW2"); + osc.create_dword_field(ARG3_OP, 8, "CDW3"); + + osc.if_uuid_equal(&PCIE_UUID, |osc| { + osc.store("CDW2", "SUPP"); + osc.and_to("CDW3", unsupported_mask); + }); + + osc.else_block(|osc| { + osc.or_to("CDW1", OSC_STATUS_UNSUPPORT_UUID); + }); + + osc.return_arg(3); } /// Build a PNP0C02 motherboard resources device to reserve ECAM space. diff --git a/lib/propolis/src/firmware/acpi/mod.rs b/lib/propolis/src/firmware/acpi/mod.rs index 3fd6f871e..0117a7149 100644 --- a/lib/propolis/src/firmware/acpi/mod.rs +++ b/lib/propolis/src/firmware/acpi/mod.rs @@ -13,6 +13,6 @@ pub mod tables; pub use aml::{AmlBuilder, AmlWriter, DeviceGuard, MethodGuard, ScopeGuard}; pub use dsdt::{build_dsdt_aml, DsdtConfig, DsdtGenerator, DsdtScope, PcieConfig}; -pub use names::EisaId; +pub use names::{encode_uuid, EisaId}; pub use resources::ResourceTemplateBuilder; pub use tables::{Dsdt, Facs, Fadt, Hpet, Madt, Mcfg, Rsdt, Rsdp, Xsdt}; diff --git a/lib/propolis/src/firmware/acpi/names.rs b/lib/propolis/src/firmware/acpi/names.rs index 94c6fffde..b1abd3805 100644 --- a/lib/propolis/src/firmware/acpi/names.rs +++ b/lib/propolis/src/firmware/acpi/names.rs @@ -133,6 +133,57 @@ impl EisaId { } } +/// UUID byte size. +pub const UUID_SIZE: usize = 16; + +/// Encode a UUID string into ACPI ToUUID format at compile time. +/// +/// UUID format: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" +/// +/// ACPI ToUUID uses mixed-endian encoding: +/// - First 3 groups (8-4-4 hex digits): little-endian +/// - Last 2 groups (4-12 hex digits): big-endian +pub const fn encode_uuid(uuid: &str) -> [u8; UUID_SIZE] { + let b = uuid.as_bytes(); + assert!(b.len() == 36, "UUID must be 36 characters"); + assert!( + b[8] == b'-' && b[13] == b'-' && b[18] == b'-' && b[23] == b'-', + "UUID must have dashes at positions 8, 13, 18, 23" + ); + + const fn hex(c: u8) -> u8 { + match c { + b'0'..=b'9' => c - b'0', + b'A'..=b'F' => c - b'A' + 10, + b'a'..=b'f' => c - b'a' + 10, + _ => panic!("invalid hex"), + } + } + + const fn byte(b: &[u8], i: usize) -> u8 { + (hex(b[i]) << 4) | hex(b[i + 1]) + } + + [ + byte(b, 6), + byte(b, 4), + byte(b, 2), + byte(b, 0), + byte(b, 11), + byte(b, 9), + byte(b, 16), + byte(b, 14), + byte(b, 19), + byte(b, 21), + byte(b, 24), + byte(b, 26), + byte(b, 28), + byte(b, 30), + byte(b, 32), + byte(b, 34), + ] +} + #[cfg(test)] mod tests { use super::*; @@ -190,4 +241,16 @@ mod tests { fn eisaid_rejects_lowercase() { encode_eisaid("pnp0A08"); } + + #[test] + fn uuid_encoding() { + let uuid = encode_uuid("33DB4D5B-1FF7-401C-9657-7441C03DD766"); + assert_eq!( + uuid, + [ + 0x5B, 0x4D, 0xDB, 0x33, 0xF7, 0x1F, 0x1C, 0x40, 0x96, 0x57, + 0x74, 0x41, 0xC0, 0x3D, 0xD7, 0x66, + ] + ); + } } diff --git a/lib/propolis/src/firmware/acpi/opcodes.rs b/lib/propolis/src/firmware/acpi/opcodes.rs index 2aa56ab27..2c791c2c4 100644 --- a/lib/propolis/src/firmware/acpi/opcodes.rs +++ b/lib/propolis/src/firmware/acpi/opcodes.rs @@ -94,6 +94,7 @@ pub const SIZEOF_OP: u8 = 0x87; pub const INDEX_OP: u8 = 0x88; pub const DEREF_OF_OP: u8 = 0x83; pub const REF_OF_OP: u8 = 0x71; +pub const CREATE_DWORD_FIELD_OP: u8 = 0x8A; // Resource template end tag pub const END_TAG: u8 = 0x79; From 7036a3e08731afb15630243f0537e06ccdda5a28 Mon Sep 17 00:00:00 2001 From: Amey Narkhede Date: Sun, 4 Jan 2026 17:22:12 +0000 Subject: [PATCH 17/47] Prepare the ACPI tables for generation Combine all ACPI tables into the format expected by firmware(OVMF) by using fw_cfg's table-loader commands for address patching and checksum computation. Signed-off-by: Amey Narkhede --- lib/propolis/src/firmware/acpi/tables.rs | 1 + lib/propolis/src/hw/qemu/fwcfg.rs | 235 +++++++++++++++++++++++ 2 files changed, 236 insertions(+) diff --git a/lib/propolis/src/firmware/acpi/tables.rs b/lib/propolis/src/firmware/acpi/tables.rs index 174d2ba58..d845967cd 100644 --- a/lib/propolis/src/firmware/acpi/tables.rs +++ b/lib/propolis/src/firmware/acpi/tables.rs @@ -13,6 +13,7 @@ use zerocopy::{Immutable, IntoBytes}; pub const ACPI_TABLE_HEADER_SIZE: usize = 36; const ACPI_TABLE_LENGTH_OFFSET: usize = 4; +pub const ACPI_TABLE_CHECKSUM_OFF: usize = 9; #[derive(Copy, Clone, IntoBytes, Immutable)] #[repr(C, packed)] diff --git a/lib/propolis/src/hw/qemu/fwcfg.rs b/lib/propolis/src/hw/qemu/fwcfg.rs index 05d6b6c17..ac3c1fc52 100644 --- a/lib/propolis/src/hw/qemu/fwcfg.rs +++ b/lib/propolis/src/hw/qemu/fwcfg.rs @@ -1477,6 +1477,241 @@ pub mod formats { pub const ACPI_RSDP_FWCFG_NAME: &str = "etc/acpi/rsdp"; pub use crate::firmware::acpi::{Dsdt, Facs, Fadt, Hpet, Madt, Mcfg, Rsdt, Rsdp, Xsdt}; + use crate::firmware::acpi::tables::{ + ACPI_PROCESSOR_ALL, ACPI_TABLE_CHECKSUM_OFF, FADT_OFF_DSDT32, + FADT_OFF_DSDT64, FADT_OFF_FACS32, GSI_SCI, GSI_TIMER, ISA_BUS, + ISA_IRQ_SCI, ISA_IRQ_TIMER, LINT1, MADT_INT_FLAGS_DEFAULT, + MADT_INT_LEVEL_ACTIVE_HIGH, MADT_LAPIC_ENABLED, RSDP_CHECKSUM_OFFSET, + RSDP_EXT_CHECKSUM_OFFSET, RSDP_SIZE, RSDP_V1_SIZE, RSDP_XSDT_ADDR_OFFSET, + }; + + use std::mem::size_of; + + pub struct AcpiConfig { + pub num_cpus: u8, + pub local_apic_addr: u32, + pub io_apic_id: u8, + pub io_apic_addr: u32, + pub pcie_ecam_base: u64, + pub pcie_ecam_size: u64, + pub pcie_bus_start: u8, + pub pcie_bus_end: u8, + pub pcie_mmio32_base: u64, + pub pcie_mmio32_limit: u64, + pub pcie_mmio64_base: u64, + pub pcie_mmio64_limit: u64, + } + + impl Default for AcpiConfig { + fn default() -> Self { + Self { + num_cpus: 1, + local_apic_addr: 0xfee0_0000, + io_apic_id: 0, + io_apic_addr: 0xfec0_0000, + pcie_ecam_base: 0xe000_0000, + pcie_ecam_size: 0x1000_0000, + pcie_bus_start: 0, + pcie_bus_end: 255, + pcie_mmio32_base: 0xc000_0000, + pcie_mmio32_limit: 0xfbff_ffff, + pcie_mmio64_base: 0x1_0000_0000, + pcie_mmio64_limit: 0xf_ffff_ffff, + } + } + } + + pub struct AcpiTables { + pub tables: Vec, + pub rsdp: Vec, + pub loader: Vec, + } + + pub fn build_acpi_tables( + config: &AcpiConfig, + generators: &[&dyn crate::firmware::acpi::DsdtGenerator], + ) -> AcpiTables { + use crate::firmware::acpi::{build_dsdt_aml, DsdtConfig, PcieConfig}; + + let mut tables = Vec::new(); + let mut loader = TableLoader::new(); + + let dsdt_config = DsdtConfig { + pcie: Some(PcieConfig { + ecam_base: config.pcie_ecam_base, + ecam_size: config.pcie_ecam_size, + bus_start: config.pcie_bus_start, + bus_end: config.pcie_bus_end, + mmio32_base: config.pcie_mmio32_base, + mmio32_limit: config.pcie_mmio32_limit, + mmio64_base: config.pcie_mmio64_base, + mmio64_limit: config.pcie_mmio64_limit, + }), + }; + let aml = build_dsdt_aml(&dsdt_config, generators); + let mut dsdt = Dsdt::new(); + dsdt.append_aml(&aml); + let dsdt_offset = tables.len(); + tables.extend_from_slice(&dsdt.finish()); + + let fadt = Fadt::new(); + let fadt_offset = tables.len(); + tables.extend_from_slice(&fadt.finish()); + + let mut madt = Madt::new(config.local_apic_addr); + for i in 0..config.num_cpus { + madt.add_local_apic(i, i, MADT_LAPIC_ENABLED); + } + madt.add_io_apic(config.io_apic_id, config.io_apic_addr, 0); + madt.add_int_src_override(ISA_BUS, ISA_IRQ_TIMER, GSI_TIMER, 0); + madt.add_int_src_override( + ISA_BUS, + ISA_IRQ_SCI, + GSI_SCI, + MADT_INT_LEVEL_ACTIVE_HIGH, + ); + madt.add_lapic_nmi(ACPI_PROCESSOR_ALL, MADT_INT_FLAGS_DEFAULT, LINT1); + let madt_offset = tables.len(); + tables.extend_from_slice(&madt.finish()); + + let mut mcfg = Mcfg::new(); + mcfg.add_allocation( + config.pcie_ecam_base, + 0, + config.pcie_bus_start, + config.pcie_bus_end, + ); + let mcfg_offset = tables.len(); + tables.extend_from_slice(&mcfg.finish()); + + let hpet = Hpet::new(); + let hpet_offset = tables.len(); + tables.extend_from_slice(&hpet.finish()); + + let mut xsdt = Xsdt::new(); + let xsdt_fadt_off = xsdt.add_entry(); + let xsdt_madt_off = xsdt.add_entry(); + let xsdt_mcfg_off = xsdt.add_entry(); + let xsdt_hpet_off = xsdt.add_entry(); + let xsdt_offset = tables.len(); + tables.extend_from_slice(&xsdt.finish()); + + let facs = Facs::new(); + let facs_offset = tables.len(); + tables.extend_from_slice(&facs.finish()); + + let rsdp_data = Rsdp::new().finish(); + + tables[fadt_offset + FADT_OFF_FACS32 + ..fadt_offset + FADT_OFF_FACS32 + size_of::()] + .copy_from_slice(&(facs_offset as u32).to_le_bytes()); + + tables[fadt_offset + FADT_OFF_DSDT32 + ..fadt_offset + FADT_OFF_DSDT32 + size_of::()] + .copy_from_slice(&(dsdt_offset as u32).to_le_bytes()); + tables[fadt_offset + FADT_OFF_DSDT64 + ..fadt_offset + FADT_OFF_DSDT64 + size_of::()] + .copy_from_slice(&(dsdt_offset as u64).to_le_bytes()); + + let xsdt_entries = [ + (xsdt_fadt_off, fadt_offset), + (xsdt_madt_off, madt_offset), + (xsdt_mcfg_off, mcfg_offset), + (xsdt_hpet_off, hpet_offset), + ]; + for (entry_off, table_offset) in xsdt_entries { + let off = xsdt_offset + entry_off as usize; + tables[off..off + size_of::()] + .copy_from_slice(&(table_offset as u64).to_le_bytes()); + } + + loader + .add_allocate(ACPI_TABLES_FWCFG_NAME, 64, AllocZone::High) + .unwrap(); + loader.add_allocate(ACPI_RSDP_FWCFG_NAME, 16, AllocZone::FSeg).unwrap(); + + let table_pointers: &[(u32, u8)] = &[ + ( + fadt_offset as u32 + FADT_OFF_FACS32 as u32, + size_of::() as u8, + ), + ( + fadt_offset as u32 + FADT_OFF_DSDT32 as u32, + size_of::() as u8, + ), + ( + fadt_offset as u32 + FADT_OFF_DSDT64 as u32, + size_of::() as u8, + ), + (xsdt_offset as u32 + xsdt_fadt_off, size_of::() as u8), + (xsdt_offset as u32 + xsdt_madt_off, size_of::() as u8), + (xsdt_offset as u32 + xsdt_mcfg_off, size_of::() as u8), + (xsdt_offset as u32 + xsdt_hpet_off, size_of::() as u8), + ]; + for &(offset, size) in table_pointers { + loader + .add_pointer( + ACPI_TABLES_FWCFG_NAME, + ACPI_TABLES_FWCFG_NAME, + offset, + size, + ) + .unwrap(); + } + + loader + .add_pointer( + ACPI_RSDP_FWCFG_NAME, + ACPI_TABLES_FWCFG_NAME, + RSDP_XSDT_ADDR_OFFSET as u32, + size_of::() as u8, + ) + .unwrap(); + + let table_offsets = [ + dsdt_offset, + fadt_offset, + madt_offset, + mcfg_offset, + hpet_offset, + xsdt_offset, + facs_offset, + ]; + for pair in table_offsets.windows(2) { + let (start, end) = (pair[0] as u32, pair[1] as u32); + loader + .add_checksum( + ACPI_TABLES_FWCFG_NAME, + start + ACPI_TABLE_CHECKSUM_OFF as u32, + start, + end - start, + ) + .unwrap(); + } + + let rsdp_checksums = [ + (RSDP_CHECKSUM_OFFSET, RSDP_V1_SIZE), + (RSDP_EXT_CHECKSUM_OFFSET, RSDP_SIZE), + ]; + for (checksum_off, length) in rsdp_checksums { + loader + .add_checksum( + ACPI_RSDP_FWCFG_NAME, + checksum_off as u32, + 0, + length as u32, + ) + .unwrap(); + } + + let loader_entry = loader.finish(); + let loader_bytes = match loader_entry { + super::Entry::Bytes(b) => b, + _ => unreachable!(), + }; + + AcpiTables { tables, rsdp: rsdp_data, loader: loader_bytes } + } #[cfg(test)] mod test_table_loader { From 61f0ed4c8de28dfd4c1e5bd72062b1937e08aaf8 Mon Sep 17 00:00:00 2001 From: Amey Narkhede Date: Wed, 31 Dec 2025 04:59:56 +0000 Subject: [PATCH 18/47] Wire up ACPI table generation via fw_cfg Integrate the new ACPI table generation into propolis-standalone and propolis-server. Also replace hardcoded memory region addresses with constants that align with ACPI table definitions. The PCIe ECAM base is kept same as before at 0xe000_0000 (3.5GB) to match existing i440fx chipset ECAM placement. ECAM is no longer added to the E820 map as reserved memory since it is MMIO space properly described in the MCFG ACPI table. Guest physical memory map: 0x0000_0000 - 0xbfff_ffff Low RAM (up to 3 GiB) 0xc000_0000 - 0xffff_ffff PCI hole (1 GiB MMIO region) 0xc000_0000 - 0xdfff_ffff 32-bit PCI MMIO 0xe000_0000 - 0xefff_ffff PCIe ECAM (256 MiB, 256 buses) 0xfec0_0000 IOAPIC 0xfed0_0000 HPET 0xffe0_0000 - 0xffff_ffff Bootrom (2 MiB) 0x1_0000_0000+ High RAM + 64-bit PCI MMIO e820 map as seen by guest: 0x0000_0000 - 0x0009_ffff Usable (640 KiB low memory) 0x0010_0000 - 0xbeaf_ffff Usable (~3 GiB main RAM) 0xbeb0_0000 - 0xbfb6_cfff Reserved (UEFI runtime/data) 0xbfb6_d000 - 0xbfbf_efff ACPI Tables + NVS 0xbfbf_f000 - 0xbffd_ffff Usable (top of low memory) 0xbffe_0000 - 0xffff_ffff Reserved (PCI hole) 0x1_0000_0000 - highmem Usable (high RAM above 4 GiB) To stay on safe side only enable using new ACPI tables for newly launched VMs. Old VMs using OVMF tables would keep using the same OVMF tables throughout multiple migrations. To verify this add the phd test as well for new VM launched with native tables, native tables preserved through migration and VM launched from old propolis without native tables stays with OVMF through multiple future migrations. Signed-off-by: Amey Narkhede Signed-off-by: glitzflitz --- bin/propolis-cli/src/main.rs | 1 + bin/propolis-server/src/lib/initializer.rs | 89 ++++++++++++- bin/propolis-server/src/lib/server.rs | 18 ++- .../src/lib/spec/api_spec_v0.rs | 1 + bin/propolis-server/src/lib/spec/builder.rs | 2 + bin/propolis-server/src/lib/spec/mod.rs | 7 ++ bin/propolis-standalone/src/main.rs | 73 ++++++++++- .../src/instance_spec/components/board.rs | 9 ++ phd-tests/framework/src/test_vm/config.rs | 9 ++ phd-tests/tests/src/acpi.rs | 118 ++++++++++++++++++ phd-tests/tests/src/lib.rs | 1 + phd-tests/tests/src/migrate.rs | 3 + 12 files changed, 317 insertions(+), 14 deletions(-) create mode 100644 phd-tests/tests/src/acpi.rs diff --git a/bin/propolis-cli/src/main.rs b/bin/propolis-cli/src/main.rs index 86f7dcd4e..b86b6849c 100644 --- a/bin/propolis-cli/src/main.rs +++ b/bin/propolis-cli/src/main.rs @@ -340,6 +340,7 @@ impl VmConfig { } else { Default::default() }, + native_acpi_tables: Some(true), }, components: Default::default(), smbios: None, diff --git a/bin/propolis-server/src/lib/initializer.rs b/bin/propolis-server/src/lib/initializer.rs index 575509677..3aff80e11 100644 --- a/bin/propolis-server/src/lib/initializer.rs +++ b/bin/propolis-server/src/lib/initializer.rs @@ -111,6 +111,12 @@ pub enum MachineInitError { /// Arbitrary ROM limit for now const MAX_ROM_SIZE: usize = 0x20_0000; +const PCIE_ECAM_BASE: usize = 0xe000_0000; +const PCIE_ECAM_SIZE: usize = 0x1000_0000; +const MEM_32BIT_DEVICES_START: usize = 0xc000_0000; +const MEM_32BIT_DEVICES_END: usize = 0xfc00_0000; +const HIGHMEM_START: usize = 0x1_0000_0000; + 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); @@ -141,19 +147,22 @@ pub fn build_instance( .context("failed to add low memory region")? .add_rom_region(0x1_0000_0000 - MAX_ROM_SIZE, MAX_ROM_SIZE, "bootrom") .context("failed to add bootrom region")? - .add_mmio_region(0xc000_0000_usize, 0x2000_0000_usize, "dev32") + .add_mmio_region(lowmem, PCIE_ECAM_BASE - lowmem, "dev32") .context("failed to add low device MMIO region")? - .add_mmio_region(0xe000_0000_usize, 0x1000_0000_usize, "pcicfg") + .add_mmio_region( + PCIE_ECAM_BASE, + MEM_32BIT_DEVICES_END - PCIE_ECAM_BASE, + "pcicfg", + ) .context("failed to add PCI config region")?; - let highmem_start = 0x1_0000_0000; if highmem > 0 { builder = builder - .add_mem_region(highmem_start, highmem, "highmem") + .add_mem_region(HIGHMEM_START, highmem, "highmem") .context("failed to add high memory region")?; } - let dev64_start = highmem_start + highmem; + let dev64_start = HIGHMEM_START + highmem; builder = builder .add_mmio_region(dev64_start, vmm::MAX_PHYSMEM - dev64_start, "dev64") .context("failed to add high device MMIO region")?; @@ -1170,9 +1179,17 @@ impl MachineInitializer<'_> { propolis::vmm::MapType::Dram => { e820_table.add_mem(addr, len); } - _ => { + propolis::vmm::MapType::Rom => { e820_table.add_reserved(addr, len); } + propolis::vmm::MapType::Mmio => { + // With native ACPI tables, MMIO is described in the DSDT + // _CRS and should not appear in E820. Without native + // tables, preserve original E820 layout. + if self.spec.board.native_acpi_tables != Some(true) { + e820_table.add_reserved(addr, len); + } + } } } @@ -1284,6 +1301,66 @@ impl MachineInitializer<'_> { .insert_named("etc/e820", e820_entry) .map_err(|e| MachineInitError::FwcfgInsertFailed("e820", e))?; + if self.spec.board.native_acpi_tables == Some(true) { + let (_, highmem) = get_spec_guest_ram_limits(self.spec); + let dev64_start = HIGHMEM_START + highmem; + + // Collect DSDT generators from devices that implement the trait + let generators: Vec<_> = self + .devices + .values() + .filter_map(|dev| dev.as_dsdt_generator()) + .collect(); + + // Get the physical address width from CPUID leaf 0x8000_0008. + // EAX[7:0] contains the physical address bits supported by the CPU. + // The 64-bit MMIO limit must not exceed what the CPU can address. + let phys_addr_bits = self + .spec + .cpuid + .get(CpuidIdent::leaf(0x8000_0008)) + .map(|v| v.eax & 0xff) + .unwrap_or(48) as u64; + let max_phys_addr = (1u64 << phys_addr_bits) - 1; + let mmio64_limit = + max_phys_addr.min(vmm::MAX_PHYSMEM as u64 - 1); + + let acpi_tables = fwcfg::formats::build_acpi_tables( + &fwcfg::formats::AcpiConfig { + num_cpus: cpus, + pcie_ecam_base: PCIE_ECAM_BASE as u64, + pcie_ecam_size: PCIE_ECAM_SIZE as u64, + pcie_mmio32_base: MEM_32BIT_DEVICES_START as u64, + pcie_mmio32_limit: (MEM_32BIT_DEVICES_END - 1) as u64, + pcie_mmio64_base: dev64_start as u64, + pcie_mmio64_limit: mmio64_limit, + ..Default::default() + }, + &generators, + ); + fwcfg + .insert_named( + "etc/acpi/tables", + Entry::Bytes(acpi_tables.tables), + ) + .map_err(|e| { + MachineInitError::FwcfgInsertFailed("acpi/tables", e) + })?; + fwcfg + .insert_named("etc/acpi/rsdp", Entry::Bytes(acpi_tables.rsdp)) + .map_err(|e| { + MachineInitError::FwcfgInsertFailed("acpi/rsdp", e) + })?; + fwcfg + .insert_named( + "etc/table-loader", + Entry::Bytes(acpi_tables.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-server/src/lib/server.rs b/bin/propolis-server/src/lib/server.rs index 9664f3abc..b8f82154d 100644 --- a/bin/propolis-server/src/lib/server.rs +++ b/bin/propolis-server/src/lib/server.rs @@ -248,7 +248,14 @@ impl PropolisServerApi for PropolisServerImpl { let vm_init = match init { InstanceInitializationMethod::Spec { spec } => spec .try_into() - .map(|s| VmInitializationMethod::Spec(Box::new(s))) + .map(|mut s: crate::spec::Spec| { + // Default to native ACPI tables for new VMs but respect + // explicit client preference if provided. + if s.board.native_acpi_tables.is_none() { + s.board.native_acpi_tables = Some(true); + } + VmInitializationMethod::Spec(Box::new(s)) + }) .map_err(|e| { if let Some(s) = e.source() { format!("{e}: {s}") @@ -337,7 +344,14 @@ impl PropolisServerApi for PropolisServerImpl { let vm_init = match init { InstanceInitializationMethodV0::Spec { spec } => spec .try_into() - .map(|s| VmInitializationMethod::Spec(Box::new(s))) + .map(|mut s: crate::spec::Spec| { + // Default to native ACPI tables for new VMs, but respect + // explicit client preference if provided. + if s.board.native_acpi_tables.is_none() { + s.board.native_acpi_tables = Some(true); + } + VmInitializationMethod::Spec(Box::new(s)) + }) .map_err(|e| { if let Some(s) = e.source() { format!("{e}: {s}") diff --git a/bin/propolis-server/src/lib/spec/api_spec_v0.rs b/bin/propolis-server/src/lib/spec/api_spec_v0.rs index 2b52c16c7..47d6f4ca7 100644 --- a/bin/propolis-server/src/lib/spec/api_spec_v0.rs +++ b/bin/propolis-server/src/lib/spec/api_spec_v0.rs @@ -101,6 +101,7 @@ impl From for InstanceSpecV0 { chipset: board.chipset, guest_hv_interface: board.guest_hv_interface, cpuid: Some(cpuid.into_instance_spec_cpuid()), + native_acpi_tables: board.native_acpi_tables, }; let mut spec = InstanceSpecV0 { board, components: Default::default() }; diff --git a/bin/propolis-server/src/lib/spec/builder.rs b/bin/propolis-server/src/lib/spec/builder.rs index ddd5c5e5b..41c48ba15 100644 --- a/bin/propolis-server/src/lib/spec/builder.rs +++ b/bin/propolis-server/src/lib/spec/builder.rs @@ -96,6 +96,7 @@ impl SpecBuilder { memory_mb: board.memory_mb, chipset: board.chipset, guest_hv_interface: board.guest_hv_interface, + native_acpi_tables: board.native_acpi_tables, }, cpuid, ..Default::default() @@ -380,6 +381,7 @@ mod test { memory_mb: 512, chipset: Chipset::I440Fx(I440Fx { enable_pcie: false }), guest_hv_interface: GuestHypervisorInterface::Bhyve, + native_acpi_tables: Some(false), }; SpecBuilder { diff --git a/bin/propolis-server/src/lib/spec/mod.rs b/bin/propolis-server/src/lib/spec/mod.rs index 236730c87..c31c0576c 100644 --- a/bin/propolis-server/src/lib/spec/mod.rs +++ b/bin/propolis-server/src/lib/spec/mod.rs @@ -129,6 +129,12 @@ pub(crate) struct Board { pub memory_mb: u64, pub chipset: Chipset, pub guest_hv_interface: GuestHypervisorInterface, + /// Use native ACPI tables instead of OVMF-provided tables. + /// + /// `None` indicates a VM created before native ACPI table support existed. + /// For migration compatibility, `None` is preserved through round-trips + /// and treated the same as `Some(false)` at runtime. + pub native_acpi_tables: Option, } impl Default for Board { @@ -138,6 +144,7 @@ impl Default for Board { memory_mb: 0, chipset: Chipset::I440Fx(I440Fx { enable_pcie: false }), guest_hv_interface: GuestHypervisorInterface::Bhyve, + native_acpi_tables: None, } } } diff --git a/bin/propolis-standalone/src/main.rs b/bin/propolis-standalone/src/main.rs index 17d2903ba..63faec47a 100644 --- a/bin/propolis-standalone/src/main.rs +++ b/bin/propolis-standalone/src/main.rs @@ -43,6 +43,12 @@ const PAGE_OFFSET: u64 = 0xfff; // Arbitrary ROM limit for now const MAX_ROM_SIZE: usize = 0x20_0000; +const PCIE_ECAM_BASE: usize = 0xe000_0000; +const PCIE_ECAM_SIZE: usize = 0x1000_0000; +const MEM_32BIT_DEVICES_START: usize = 0xc000_0000; +const MEM_32BIT_DEVICES_END: usize = 0xfc00_0000; +const HIGHMEM_START: usize = 0x1_0000_0000; + const MIN_RT_THREADS: usize = 8; const BASE_RT_THREADS: usize = 4; @@ -738,15 +744,18 @@ fn build_machine( .max_cpus(max_cpu)? .add_mem_region(0, lowmem, "lowmem")? .add_rom_region(0x1_0000_0000 - MAX_ROM_SIZE, MAX_ROM_SIZE, "bootrom")? - .add_mmio_region(0xc000_0000, 0x2000_0000, "dev32")? - .add_mmio_region(0xe000_0000, 0x1000_0000, "pcicfg")?; + .add_mmio_region(lowmem, PCIE_ECAM_BASE - lowmem, "dev32")? + .add_mmio_region( + PCIE_ECAM_BASE, + MEM_32BIT_DEVICES_END - PCIE_ECAM_BASE, + "pcicfg", + )?; - let highmem_start = 0x1_0000_0000; if highmem > 0 { - builder = builder.add_mem_region(highmem_start, highmem, "highmem")?; + builder = builder.add_mem_region(HIGHMEM_START, highmem, "highmem")?; } - let dev64_start = highmem_start + highmem; + let dev64_start = HIGHMEM_START + highmem; builder = builder.add_mmio_region( dev64_start, vmm::MAX_PHYSMEM - dev64_start, @@ -974,9 +983,13 @@ fn generate_e820( MapType::Dram => { e820_table.add_mem(addr, len); } - _ => { + MapType::Rom => { e820_table.add_reserved(addr, len); } + MapType::Mmio => { + // MMIO is described in the DSDT _CRS and should not + // appear in E820. + } } } @@ -1395,6 +1408,54 @@ fn setup_instance( let e820_entry = generate_e820(machine, log).expect("can build E820 table"); fwcfg.insert_named("etc/e820", e820_entry).unwrap(); + // Collect DSDT generators from devices that implement the trait + let generators: Vec<_> = guard + .inventory + .devs + .values() + .filter_map(|dev| dev.as_dsdt_generator()) + .collect(); + + // Get the physical address width from CPUID leaf 0x8000_0008. + // EAX[7:0] contains the physical address bits supported by the CPU. + // The 64-bit MMIO limit must not exceed what the CPU can address. + let phys_addr_bits = cpuid_profile + .as_ref() + .and_then(|p| p.get(CpuidIdent::leaf(0x8000_0008))) + .map(|v| v.eax & 0xff) + .unwrap_or(48) as u64; + let max_phys_addr = (1u64 << phys_addr_bits) - 1; + let mmio64_limit = max_phys_addr.min(vmm::MAX_PHYSMEM as u64 - 1); + + let acpi_tables = fwcfg::formats::build_acpi_tables( + &fwcfg::formats::AcpiConfig { + num_cpus: cpus, + pcie_ecam_base: PCIE_ECAM_BASE as u64, + pcie_ecam_size: PCIE_ECAM_SIZE as u64, + pcie_mmio32_base: MEM_32BIT_DEVICES_START as u64, + pcie_mmio32_limit: (MEM_32BIT_DEVICES_END - 1) as u64, + pcie_mmio64_base: (HIGHMEM_START + highmem) as u64, + pcie_mmio64_limit: mmio64_limit, + ..Default::default() + }, + &generators, + ); + fwcfg + .insert_named( + "etc/acpi/tables", + fwcfg::Entry::Bytes(acpi_tables.tables), + ) + .context("failed to insert ACPI tables")?; + fwcfg + .insert_named("etc/acpi/rsdp", fwcfg::Entry::Bytes(acpi_tables.rsdp)) + .context("failed to insert ACPI RSDP")?; + fwcfg + .insert_named( + "etc/table-loader", + fwcfg::Entry::Bytes(acpi_tables.loader), + ) + .context("failed to insert ACPI table-loader")?; + fwcfg.attach(pio, &machine.acc_mem); guard.inventory.register(&fwcfg); diff --git a/crates/propolis-api-types/src/instance_spec/components/board.rs b/crates/propolis-api-types/src/instance_spec/components/board.rs index 9295296e6..80c3def6a 100644 --- a/crates/propolis-api-types/src/instance_spec/components/board.rs +++ b/crates/propolis-api-types/src/instance_spec/components/board.rs @@ -178,5 +178,14 @@ pub struct Board { /// default values from the host's CPUID values. #[serde(default, skip_serializing_if = "Option::is_none")] pub cpuid: Option, + + /// Use native ACPI tables (via fw_cfg) instead of OVMF provided tables. + /// + /// VMs created before propolis supported ACPI table generation will not + /// have this field. For backwards compatibility with live migration, + /// `None` is treated as `false` (use OVMF tables). New VMs should set + /// this to `true`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub native_acpi_tables: Option, // TODO: Processor and NUMA topology. } diff --git a/phd-tests/framework/src/test_vm/config.rs b/phd-tests/framework/src/test_vm/config.rs index 3be06080c..e855bd8d7 100644 --- a/phd-tests/framework/src/test_vm/config.rs +++ b/phd-tests/framework/src/test_vm/config.rs @@ -56,6 +56,7 @@ pub struct VmConfig<'dr> { disks: Vec>, migration_failure: Option, guest_hv_interface: Option, + native_acpi_tables: Option, } impl<'dr> VmConfig<'dr> { @@ -76,6 +77,7 @@ impl<'dr> VmConfig<'dr> { disks: Vec::new(), migration_failure: None, guest_hv_interface: None, + native_acpi_tables: Some(true), }; config.boot_disk( @@ -151,6 +153,11 @@ impl<'dr> VmConfig<'dr> { self } + pub fn native_acpi_tables(&mut self, enabled: Option) -> &mut Self { + self.native_acpi_tables = enabled; + self + } + /// Add a new disk to the VM config, and add it to the front of the VM's /// boot order. /// @@ -221,6 +228,7 @@ impl<'dr> VmConfig<'dr> { disks, migration_failure, guest_hv_interface, + native_acpi_tables, } = self; let bootrom_path = framework @@ -302,6 +310,7 @@ impl<'dr> VmConfig<'dr> { .as_ref() .cloned() .unwrap_or_default(), + native_acpi_tables: *native_acpi_tables, }, components: Default::default(), smbios: None, diff --git a/phd-tests/tests/src/acpi.rs b/phd-tests/tests/src/acpi.rs new file mode 100644 index 000000000..e0894da76 --- /dev/null +++ b/phd-tests/tests/src/acpi.rs @@ -0,0 +1,118 @@ +// 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_framework::{artifacts, lifecycle::Action}; +use phd_testcase::*; +use propolis_client::instance_spec::InstanceSpecStatus; + +#[phd_testcase] +async fn native_acpi_tables_in_spec(ctx: &Framework) { + let mut vm = ctx.spawn_default_vm("native_acpi_tables_in_spec").await?; + vm.launch().await?; + vm.wait_to_boot().await?; + + let InstanceSpecStatus::Present(spec) = vm.get_spec().await?.spec else { + panic!("instance should have a spec"); + }; + + assert_eq!(spec.board.native_acpi_tables, Some(true)); +} + +#[phd_testcase] +async fn native_tables_preserved_on_migration(ctx: &Framework) { + let mut source = + ctx.spawn_default_vm("native_tables_migration_source").await?; + + source.launch().await?; + source.wait_to_boot().await?; + + ctx.lifecycle_test( + source, + &[ + Action::MigrateToPropolis(artifacts::DEFAULT_PROPOLIS_ARTIFACT), + Action::MigrateToPropolis(artifacts::DEFAULT_PROPOLIS_ARTIFACT), + ], + |vm| { + Box::pin(async { + let InstanceSpecStatus::Present(spec) = + vm.get_spec().await.unwrap().spec + else { + panic!("should have a spec"); + }; + assert_eq!(spec.board.native_acpi_tables, Some(true)); + }) + }, + ) + .await?; +} + +#[phd_testcase] +async fn ovmf_tables_preserved_on_migration(ctx: &Framework) { + let mut cfg = ctx.vm_config_builder("ovmf_tables_migration_source"); + cfg.native_acpi_tables(Some(false)); + + let mut source = ctx.spawn_vm(&cfg, None).await?; + + source.launch().await?; + source.wait_to_boot().await?; + + ctx.lifecycle_test( + source, + &[ + Action::MigrateToPropolis(artifacts::DEFAULT_PROPOLIS_ARTIFACT), + Action::MigrateToPropolis(artifacts::DEFAULT_PROPOLIS_ARTIFACT), + ], + |vm| { + Box::pin(async { + let InstanceSpecStatus::Present(spec) = + vm.get_spec().await.unwrap().spec + else { + panic!("should have a spec"); + }; + assert_eq!(spec.board.native_acpi_tables, Some(false)); + }) + }, + ) + .await?; +} + +mod from_base { + use super::*; + + #[phd_testcase] + async fn ovmf_tables_preserved_through_migrations(ctx: &Framework) { + if !ctx.migration_base_enabled() { + phd_skip!("No 'migration base' Propolis revision available"); + } + + let mut env = ctx.environment_builder(); + env.propolis(artifacts::BASE_PROPOLIS_ARTIFACT); + let mut cfg = ctx.vm_config_builder("ovmf_tables_from_base"); + cfg.clear_boot_order(); + cfg.native_acpi_tables(None); + + let mut source = ctx.spawn_vm(&cfg, Some(&env)).await?; + source.launch().await?; + source.wait_to_boot().await?; + + ctx.lifecycle_test( + source, + &[ + Action::MigrateToPropolis(artifacts::DEFAULT_PROPOLIS_ARTIFACT), + Action::MigrateToPropolis(artifacts::DEFAULT_PROPOLIS_ARTIFACT), + ], + |vm| { + Box::pin(async { + let InstanceSpecStatus::Present(spec) = + vm.get_spec().await.unwrap().spec + else { + panic!("should have a spec"); + }; + assert!(spec.board.native_acpi_tables != Some(true)); + }) + }, + ) + .await?; + } +} diff --git a/phd-tests/tests/src/lib.rs b/phd-tests/tests/src/lib.rs index da2437a87..8e2a559a9 100644 --- a/phd-tests/tests/src/lib.rs +++ b/phd-tests/tests/src/lib.rs @@ -4,6 +4,7 @@ pub use phd_testcase; +mod acpi; mod boot_order; mod cpuid; mod crucible; diff --git a/phd-tests/tests/src/migrate.rs b/phd-tests/tests/src/migrate.rs index e75978887..8893fc85e 100644 --- a/phd-tests/tests/src/migrate.rs +++ b/phd-tests/tests/src/migrate.rs @@ -100,6 +100,9 @@ mod from_base { // because a newer base Propolis will understand `boot_settings` just // fine. cfg.clear_boot_order(); + // Base Propolis predates native ACPI table support. None ensures the + // field isn't serialized and is preserved through migration round trips. + cfg.native_acpi_tables(None); ctx.spawn_vm(&cfg, Some(&env)).await } } From ac472cfb089f6467cb1e602efa74e8b2d528ae40 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Mon, 16 Mar 2026 23:17:16 +0000 Subject: [PATCH 19/47] acpi: generate ACPI tables using acpi_tables crate Use the acpi_tables crate to generate the ACPI tables. The crate was missing some elements we needed, so it was forked for now. If changes are accepted and merged upstream we can stop using the fork. The ACPI tables were also kept identical to the current static EDK2 tables to allow us to move forward with table generation while minimizing risk. --- Cargo.lock | 17 +- Cargo.toml | 3 + bin/propolis-cli/src/main.rs | 1 - bin/propolis-server/src/lib/initializer.rs | 140 +- bin/propolis-server/src/lib/server.rs | 18 +- .../src/lib/spec/api_spec_v0.rs | 1 - bin/propolis-server/src/lib/spec/builder.rs | 2 - bin/propolis-server/src/lib/spec/mod.rs | 7 - bin/propolis-standalone/src/main.rs | 133 +- .../src/instance_spec/components/board.rs | 9 - lib/propolis/Cargo.toml | 1 + lib/propolis/src/firmware/acpi/aml.rs | 674 --------- lib/propolis/src/firmware/acpi/dsdt.rs | 1319 ++++++++++++++--- lib/propolis/src/firmware/acpi/facs.rs | 31 + lib/propolis/src/firmware/acpi/fadt.rs | 151 ++ lib/propolis/src/firmware/acpi/madt.rs | 101 ++ lib/propolis/src/firmware/acpi/mod.rs | 52 +- lib/propolis/src/firmware/acpi/names.rs | 256 ---- lib/propolis/src/firmware/acpi/opcodes.rs | 117 -- lib/propolis/src/firmware/acpi/resources.rs | 359 ----- lib/propolis/src/firmware/acpi/rsdp.rs | 49 + lib/propolis/src/firmware/acpi/tables.rs | 563 ------- lib/propolis/src/firmware/acpi/xsdt.rs | 36 + lib/propolis/src/hw/ps2/ctrl.rs | 57 +- lib/propolis/src/hw/qemu/fwcfg.rs | 748 +++++----- lib/propolis/src/hw/qemu/pvpanic.rs | 25 - lib/propolis/src/hw/uart/lpc.rs | 82 +- lib/propolis/src/lifecycle.rs | 6 +- phd-tests/framework/src/test_vm/config.rs | 9 - phd-tests/tests/src/acpi.rs | 118 -- phd-tests/tests/src/lib.rs | 1 - phd-tests/tests/src/migrate.rs | 3 - 32 files changed, 2111 insertions(+), 2978 deletions(-) delete mode 100644 lib/propolis/src/firmware/acpi/aml.rs create mode 100644 lib/propolis/src/firmware/acpi/facs.rs create mode 100644 lib/propolis/src/firmware/acpi/fadt.rs create mode 100644 lib/propolis/src/firmware/acpi/madt.rs delete mode 100644 lib/propolis/src/firmware/acpi/names.rs delete mode 100644 lib/propolis/src/firmware/acpi/opcodes.rs delete mode 100644 lib/propolis/src/firmware/acpi/resources.rs create mode 100644 lib/propolis/src/firmware/acpi/rsdp.rs delete mode 100644 lib/propolis/src/firmware/acpi/tables.rs create mode 100644 lib/propolis/src/firmware/acpi/xsdt.rs delete mode 100644 phd-tests/tests/src/acpi.rs diff --git a/Cargo.lock b/Cargo.lock index 9efce8a55..185008917 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#a4e63474d63ec81dbaacebebf41aefac743dc045" +dependencies = [ + "zerocopy 0.8.27", +] + [[package]] name = "addr2line" version = "0.21.0" @@ -1827,7 +1835,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5349,6 +5357,7 @@ dependencies = [ name = "propolis" version = "0.1.0" dependencies = [ + "acpi_tables", "anyhow", "async-trait", "bhyve_api 0.0.0", @@ -6152,7 +6161,7 @@ dependencies = [ "errno 0.3.14", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7320,7 +7329,7 @@ dependencies = [ "getrandom 0.3.2", "once_cell", "rustix 1.1.2", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7329,7 +7338,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2111ef44dae28680ae9752bb89409e7310ca33a8c621ebe7b106cf5c928b3ac0" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 55659ade2..cbb3f2b20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,6 +92,7 @@ crucible = { git = "https://github.com/oxidecomputer/crucible", rev = "7103cd3a3 crucible-client-types = { git = "https://github.com/oxidecomputer/crucible", rev = "7103cd3a3d7b0112d2949dd135db06fef0c156bb" } # External dependencies +acpi_tables = "0.2.0" anyhow = "1.0" async-trait = "0.1.88" atty = "0.2.14" @@ -180,6 +181,8 @@ usdt = { version = "0.5", default-features = false } uuid = "1.3.2" zerocopy = "0.8.25" +[patch.crates-io] +acpi_tables = { git = 'https://github.com/oxidecomputer/acpi_tables.git' } # # It's common during development to use a local copy of various complex diff --git a/bin/propolis-cli/src/main.rs b/bin/propolis-cli/src/main.rs index b86b6849c..86f7dcd4e 100644 --- a/bin/propolis-cli/src/main.rs +++ b/bin/propolis-cli/src/main.rs @@ -340,7 +340,6 @@ impl VmConfig { } else { Default::default() }, - native_acpi_tables: Some(true), }, components: Default::default(), smbios: None, diff --git a/bin/propolis-server/src/lib/initializer.rs b/bin/propolis-server/src/lib/initializer.rs index 3aff80e11..9a3d31c8f 100644 --- a/bin/propolis-server/src/lib/initializer.rs +++ b/bin/propolis-server/src/lib/initializer.rs @@ -111,11 +111,11 @@ pub enum MachineInitError { /// Arbitrary ROM limit for now const MAX_ROM_SIZE: usize = 0x20_0000; -const PCIE_ECAM_BASE: usize = 0xe000_0000; -const PCIE_ECAM_SIZE: usize = 0x1000_0000; -const MEM_32BIT_DEVICES_START: usize = 0xc000_0000; -const MEM_32BIT_DEVICES_END: usize = 0xfc00_0000; -const HIGHMEM_START: usize = 0x1_0000_0000; +/// End address of the 32-bit PCI MMIO window. +// XXX(acpi): Value inherited from the original EDK2 static tables. It should +// match the actual memory regions registered in the instance. +// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/PlatformPei/Platform.c#L180-L192 +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; @@ -147,22 +147,19 @@ pub fn build_instance( .context("failed to add low memory region")? .add_rom_region(0x1_0000_0000 - MAX_ROM_SIZE, MAX_ROM_SIZE, "bootrom") .context("failed to add bootrom region")? - .add_mmio_region(lowmem, PCIE_ECAM_BASE - lowmem, "dev32") + .add_mmio_region(0xc000_0000_usize, 0x2000_0000_usize, "dev32") .context("failed to add low device MMIO region")? - .add_mmio_region( - PCIE_ECAM_BASE, - MEM_32BIT_DEVICES_END - PCIE_ECAM_BASE, - "pcicfg", - ) + .add_mmio_region(0xe000_0000_usize, 0x1000_0000_usize, "pcicfg") .context("failed to add PCI config region")?; + let highmem_start = 0x1_0000_0000; if highmem > 0 { builder = builder - .add_mem_region(HIGHMEM_START, highmem, "highmem") + .add_mem_region(highmem_start, highmem, "highmem") .context("failed to add high memory region")?; } - let dev64_start = HIGHMEM_START + highmem; + let dev64_start = highmem_start + highmem; builder = builder .add_mmio_region(dev64_start, vmm::MAX_PHYSMEM - dev64_start, "dev64") .context("failed to add high device MMIO region")?; @@ -418,9 +415,9 @@ impl MachineInitializer<'_> { }; let dev = - LpcUart::new(chipset.irq_pin(irq).unwrap(), port, irq, uart_name); + LpcUart::new(uart_name, irq, chipset.irq_pin(irq).unwrap()); dev.set_autodiscard(true); - dev.attach(&self.machine.bus_pio); + dev.attach(&self.machine.bus_pio, port); self.devices.insert(name.to_owned(), dev.clone()); if desc.num == SerialPortNumber::Com1 { assert!(com1.is_none()); @@ -1179,17 +1176,9 @@ impl MachineInitializer<'_> { propolis::vmm::MapType::Dram => { e820_table.add_mem(addr, len); } - propolis::vmm::MapType::Rom => { + _ => { e820_table.add_reserved(addr, len); } - propolis::vmm::MapType::Mmio => { - // With native ACPI tables, MMIO is described in the DSDT - // _CRS and should not appear in E820. Without native - // tables, preserve original E820 layout. - if self.spec.board.native_acpi_tables != Some(true) { - e820_table.add_reserved(addr, len); - } - } } } @@ -1255,8 +1244,38 @@ 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(); + + let config = &fwcfg::formats::AcpiConfig { + num_cpus: cpus, + pci_window_32: fwcfg::formats::PciWindow { + base: lowmem as u64, + end: PCI_MMIO32_END as u64, + }, + // XXX(acpi): Value inherited from the original EDK2 static tables, + // where the 64-bit PCI MMIO region was never set. It + // should match the actual memory regions registered in + // the instance. + // https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiPlatformDxe/Qemu.c#L284-L286 + pci_window_64: fwcfg::formats::PciWindow { base: 0, end: 0 }, + dsdt_generators: &generators, + }; + let acpi_tables = fwcfg::formats::AcpiTablesBuilder::new(config); + + Ok(acpi_tables.finish()) + } + /// 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( @@ -1301,65 +1320,18 @@ impl MachineInitializer<'_> { .insert_named("etc/e820", e820_entry) .map_err(|e| MachineInitError::FwcfgInsertFailed("e820", e))?; - if self.spec.board.native_acpi_tables == Some(true) { - let (_, highmem) = get_spec_guest_ram_limits(self.spec); - let dev64_start = HIGHMEM_START + highmem; - - // Collect DSDT generators from devices that implement the trait - let generators: Vec<_> = self - .devices - .values() - .filter_map(|dev| dev.as_dsdt_generator()) - .collect(); - - // Get the physical address width from CPUID leaf 0x8000_0008. - // EAX[7:0] contains the physical address bits supported by the CPU. - // The 64-bit MMIO limit must not exceed what the CPU can address. - let phys_addr_bits = self - .spec - .cpuid - .get(CpuidIdent::leaf(0x8000_0008)) - .map(|v| v.eax & 0xff) - .unwrap_or(48) as u64; - let max_phys_addr = (1u64 << phys_addr_bits) - 1; - let mmio64_limit = - max_phys_addr.min(vmm::MAX_PHYSMEM as u64 - 1); - - let acpi_tables = fwcfg::formats::build_acpi_tables( - &fwcfg::formats::AcpiConfig { - num_cpus: cpus, - pcie_ecam_base: PCIE_ECAM_BASE as u64, - pcie_ecam_size: PCIE_ECAM_SIZE as u64, - pcie_mmio32_base: MEM_32BIT_DEVICES_START as u64, - pcie_mmio32_limit: (MEM_32BIT_DEVICES_END - 1) as u64, - pcie_mmio64_base: dev64_start as u64, - pcie_mmio64_limit: mmio64_limit, - ..Default::default() - }, - &generators, - ); - fwcfg - .insert_named( - "etc/acpi/tables", - Entry::Bytes(acpi_tables.tables), - ) - .map_err(|e| { - MachineInitError::FwcfgInsertFailed("acpi/tables", e) - })?; - fwcfg - .insert_named("etc/acpi/rsdp", Entry::Bytes(acpi_tables.rsdp)) - .map_err(|e| { - MachineInitError::FwcfgInsertFailed("acpi/rsdp", e) - })?; - fwcfg - .insert_named( - "etc/table-loader", - Entry::Bytes(acpi_tables.loader), - ) - .map_err(|e| { - MachineInitError::FwcfgInsertFailed("table-loader", 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-server/src/lib/server.rs b/bin/propolis-server/src/lib/server.rs index b8f82154d..9664f3abc 100644 --- a/bin/propolis-server/src/lib/server.rs +++ b/bin/propolis-server/src/lib/server.rs @@ -248,14 +248,7 @@ impl PropolisServerApi for PropolisServerImpl { let vm_init = match init { InstanceInitializationMethod::Spec { spec } => spec .try_into() - .map(|mut s: crate::spec::Spec| { - // Default to native ACPI tables for new VMs but respect - // explicit client preference if provided. - if s.board.native_acpi_tables.is_none() { - s.board.native_acpi_tables = Some(true); - } - VmInitializationMethod::Spec(Box::new(s)) - }) + .map(|s| VmInitializationMethod::Spec(Box::new(s))) .map_err(|e| { if let Some(s) = e.source() { format!("{e}: {s}") @@ -344,14 +337,7 @@ impl PropolisServerApi for PropolisServerImpl { let vm_init = match init { InstanceInitializationMethodV0::Spec { spec } => spec .try_into() - .map(|mut s: crate::spec::Spec| { - // Default to native ACPI tables for new VMs, but respect - // explicit client preference if provided. - if s.board.native_acpi_tables.is_none() { - s.board.native_acpi_tables = Some(true); - } - VmInitializationMethod::Spec(Box::new(s)) - }) + .map(|s| VmInitializationMethod::Spec(Box::new(s))) .map_err(|e| { if let Some(s) = e.source() { format!("{e}: {s}") diff --git a/bin/propolis-server/src/lib/spec/api_spec_v0.rs b/bin/propolis-server/src/lib/spec/api_spec_v0.rs index 47d6f4ca7..2b52c16c7 100644 --- a/bin/propolis-server/src/lib/spec/api_spec_v0.rs +++ b/bin/propolis-server/src/lib/spec/api_spec_v0.rs @@ -101,7 +101,6 @@ impl From for InstanceSpecV0 { chipset: board.chipset, guest_hv_interface: board.guest_hv_interface, cpuid: Some(cpuid.into_instance_spec_cpuid()), - native_acpi_tables: board.native_acpi_tables, }; let mut spec = InstanceSpecV0 { board, components: Default::default() }; diff --git a/bin/propolis-server/src/lib/spec/builder.rs b/bin/propolis-server/src/lib/spec/builder.rs index 41c48ba15..ddd5c5e5b 100644 --- a/bin/propolis-server/src/lib/spec/builder.rs +++ b/bin/propolis-server/src/lib/spec/builder.rs @@ -96,7 +96,6 @@ impl SpecBuilder { memory_mb: board.memory_mb, chipset: board.chipset, guest_hv_interface: board.guest_hv_interface, - native_acpi_tables: board.native_acpi_tables, }, cpuid, ..Default::default() @@ -381,7 +380,6 @@ mod test { memory_mb: 512, chipset: Chipset::I440Fx(I440Fx { enable_pcie: false }), guest_hv_interface: GuestHypervisorInterface::Bhyve, - native_acpi_tables: Some(false), }; SpecBuilder { diff --git a/bin/propolis-server/src/lib/spec/mod.rs b/bin/propolis-server/src/lib/spec/mod.rs index c31c0576c..236730c87 100644 --- a/bin/propolis-server/src/lib/spec/mod.rs +++ b/bin/propolis-server/src/lib/spec/mod.rs @@ -129,12 +129,6 @@ pub(crate) struct Board { pub memory_mb: u64, pub chipset: Chipset, pub guest_hv_interface: GuestHypervisorInterface, - /// Use native ACPI tables instead of OVMF-provided tables. - /// - /// `None` indicates a VM created before native ACPI table support existed. - /// For migration compatibility, `None` is preserved through round-trips - /// and treated the same as `Some(false)` at runtime. - pub native_acpi_tables: Option, } impl Default for Board { @@ -144,7 +138,6 @@ impl Default for Board { memory_mb: 0, chipset: Chipset::I440Fx(I440Fx { enable_pcie: false }), guest_hv_interface: GuestHypervisorInterface::Bhyve, - native_acpi_tables: None, } } } diff --git a/bin/propolis-standalone/src/main.rs b/bin/propolis-standalone/src/main.rs index 63faec47a..c0e7466f5 100644 --- a/bin/propolis-standalone/src/main.rs +++ b/bin/propolis-standalone/src/main.rs @@ -43,11 +43,11 @@ const PAGE_OFFSET: u64 = 0xfff; // Arbitrary ROM limit for now const MAX_ROM_SIZE: usize = 0x20_0000; -const PCIE_ECAM_BASE: usize = 0xe000_0000; -const PCIE_ECAM_SIZE: usize = 0x1000_0000; -const MEM_32BIT_DEVICES_START: usize = 0xc000_0000; -const MEM_32BIT_DEVICES_END: usize = 0xfc00_0000; -const HIGHMEM_START: usize = 0x1_0000_0000; +/// End address of the 32-bit PCI MMIO window. +// XXX(acpi): Value inherited from the original EDK2 static tables. It should +// match the actual memory regions registered in the instance. +// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/PlatformPei/Platform.c#L180-L192 +const PCI_MMIO32_END: usize = 0xfeef_ffff; const MIN_RT_THREADS: usize = 8; const BASE_RT_THREADS: usize = 4; @@ -744,18 +744,15 @@ fn build_machine( .max_cpus(max_cpu)? .add_mem_region(0, lowmem, "lowmem")? .add_rom_region(0x1_0000_0000 - MAX_ROM_SIZE, MAX_ROM_SIZE, "bootrom")? - .add_mmio_region(lowmem, PCIE_ECAM_BASE - lowmem, "dev32")? - .add_mmio_region( - PCIE_ECAM_BASE, - MEM_32BIT_DEVICES_END - PCIE_ECAM_BASE, - "pcicfg", - )?; + .add_mmio_region(0xc000_0000, 0x2000_0000, "dev32")? + .add_mmio_region(0xe000_0000, 0x1000_0000, "pcicfg")?; + let highmem_start = 0x1_0000_0000; if highmem > 0 { - builder = builder.add_mem_region(HIGHMEM_START, highmem, "highmem")?; + builder = builder.add_mem_region(highmem_start, highmem, "highmem")?; } - let dev64_start = HIGHMEM_START + highmem; + let dev64_start = highmem_start + highmem; builder = builder.add_mmio_region( dev64_start, vmm::MAX_PHYSMEM - dev64_start, @@ -983,13 +980,9 @@ fn generate_e820( MapType::Dram => { e820_table.add_mem(addr, len); } - MapType::Rom => { + _ => { e820_table.add_reserved(addr, len); } - MapType::Mmio => { - // MMIO is described in the DSDT _CRS and should not - // appear in E820. - } } } @@ -1045,6 +1038,36 @@ 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(); + + let config = &fwcfg::formats::AcpiConfig { + num_cpus: cpus, + pci_window_32: fwcfg::formats::PciWindow { + base: lowmem as u64, + end: PCI_MMIO32_END as u64, + }, + // XXX(acpi): Value inherited from the original EDK2 static tables, + // where the 64-bit PCI MMIO region was never set. It + // should match the actual memory regions registered in + // the instance. + // https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiPlatformDxe/Qemu.c#L284-L286 + pci_window_64: fwcfg::formats::PciWindow { base: 0, end: 0 }, + dsdt_generators: &generators, + }; + let acpi_tables = fwcfg::formats::AcpiTablesBuilder::new(config); + + Ok(acpi_tables.finish()) +} + fn setup_instance( config: config::Config, from_restore: bool, @@ -1153,28 +1176,24 @@ fn setup_instance( // UARTs let com1 = LpcUart::new( - chipset_lpc.irq_pin(ibmpc::IRQ_COM1).unwrap(), - ibmpc::PORT_COM1, - ibmpc::IRQ_COM1, "COM1", + ibmpc::IRQ_COM1, + chipset_lpc.irq_pin(ibmpc::IRQ_COM1).unwrap(), ); let com2 = LpcUart::new( - chipset_lpc.irq_pin(ibmpc::IRQ_COM2).unwrap(), - ibmpc::PORT_COM2, - ibmpc::IRQ_COM2, "COM2", + ibmpc::IRQ_COM2, + chipset_lpc.irq_pin(ibmpc::IRQ_COM2).unwrap(), ); let com3 = LpcUart::new( - chipset_lpc.irq_pin(ibmpc::IRQ_COM3).unwrap(), - ibmpc::PORT_COM3, - ibmpc::IRQ_COM3, "COM3", + ibmpc::IRQ_COM3, + chipset_lpc.irq_pin(ibmpc::IRQ_COM3).unwrap(), ); let com4 = LpcUart::new( - chipset_lpc.irq_pin(ibmpc::IRQ_COM4).unwrap(), - ibmpc::PORT_COM4, - ibmpc::IRQ_COM4, "COM4", + ibmpc::IRQ_COM4, + chipset_lpc.irq_pin(ibmpc::IRQ_COM4).unwrap(), ); com1_sock.spawn( @@ -1189,10 +1208,10 @@ fn setup_instance( com4.set_autodiscard(true); let pio = &machine.bus_pio; - com1.attach(pio); - com2.attach(pio); - com3.attach(pio); - com4.attach(pio); + 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"); @@ -1408,52 +1427,16 @@ fn setup_instance( let e820_entry = generate_e820(machine, log).expect("can build E820 table"); fwcfg.insert_named("etc/e820", e820_entry).unwrap(); - // Collect DSDT generators from devices that implement the trait - let generators: Vec<_> = guard - .inventory - .devs - .values() - .filter_map(|dev| dev.as_dsdt_generator()) - .collect(); - - // Get the physical address width from CPUID leaf 0x8000_0008. - // EAX[7:0] contains the physical address bits supported by the CPU. - // The 64-bit MMIO limit must not exceed what the CPU can address. - let phys_addr_bits = cpuid_profile - .as_ref() - .and_then(|p| p.get(CpuidIdent::leaf(0x8000_0008))) - .map(|v| v.eax & 0xff) - .unwrap_or(48) as u64; - let max_phys_addr = (1u64 << phys_addr_bits) - 1; - let mmio64_limit = max_phys_addr.min(vmm::MAX_PHYSMEM as u64 - 1); - - let acpi_tables = fwcfg::formats::build_acpi_tables( - &fwcfg::formats::AcpiConfig { - num_cpus: cpus, - pcie_ecam_base: PCIE_ECAM_BASE as u64, - pcie_ecam_size: PCIE_ECAM_SIZE as u64, - pcie_mmio32_base: MEM_32BIT_DEVICES_START as u64, - pcie_mmio32_limit: (MEM_32BIT_DEVICES_END - 1) as u64, - pcie_mmio64_base: (HIGHMEM_START + highmem) as u64, - pcie_mmio64_limit: mmio64_limit, - ..Default::default() - }, - &generators, - ); + let acpi_entries = generate_acpi_tables(cpus, lowmem, &guard.inventory) + .expect("failed to build ACPI tables"); fwcfg - .insert_named( - "etc/acpi/tables", - fwcfg::Entry::Bytes(acpi_tables.tables), - ) + .insert_named("etc/acpi/tables", acpi_entries.tables) .context("failed to insert ACPI tables")?; fwcfg - .insert_named("etc/acpi/rsdp", fwcfg::Entry::Bytes(acpi_tables.rsdp)) + .insert_named("etc/acpi/rsdp", acpi_entries.rsdp) .context("failed to insert ACPI RSDP")?; fwcfg - .insert_named( - "etc/table-loader", - fwcfg::Entry::Bytes(acpi_tables.loader), - ) + .insert_named("etc/table-loader", acpi_entries.table_loader) .context("failed to insert ACPI table-loader")?; fwcfg.attach(pio, &machine.acc_mem); diff --git a/crates/propolis-api-types/src/instance_spec/components/board.rs b/crates/propolis-api-types/src/instance_spec/components/board.rs index 80c3def6a..9295296e6 100644 --- a/crates/propolis-api-types/src/instance_spec/components/board.rs +++ b/crates/propolis-api-types/src/instance_spec/components/board.rs @@ -178,14 +178,5 @@ pub struct Board { /// default values from the host's CPUID values. #[serde(default, skip_serializing_if = "Option::is_none")] pub cpuid: Option, - - /// Use native ACPI tables (via fw_cfg) instead of OVMF provided tables. - /// - /// VMs created before propolis supported ACPI table generation will not - /// have this field. For backwards compatibility with live migration, - /// `None` is treated as `false` (use OVMF tables). New VMs should set - /// this to `true`. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub native_acpi_tables: Option, // TODO: Processor and NUMA topology. } diff --git a/lib/propolis/Cargo.toml b/lib/propolis/Cargo.toml index a3e243155..a8fe57e32 100644 --- a/lib/propolis/Cargo.toml +++ b/lib/propolis/Cargo.toml @@ -22,6 +22,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 diff --git a/lib/propolis/src/firmware/acpi/aml.rs b/lib/propolis/src/firmware/acpi/aml.rs deleted file mode 100644 index e021c4078..000000000 --- a/lib/propolis/src/firmware/acpi/aml.rs +++ /dev/null @@ -1,674 +0,0 @@ -// 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 super::names::{encode_name_string, EisaId, UUID_SIZE}; -use super::opcodes::*; - -pub trait AmlWriter { - fn write_aml(&self, buf: &mut Vec); -} - -impl AmlWriter for u8 { - fn write_aml(&self, buf: &mut Vec) { - match *self { - 0 => buf.push(ZERO_OP), - 1 => buf.push(ONE_OP), - v => { - buf.push(BYTE_PREFIX); - buf.push(v); - } - } - } -} - -impl AmlWriter for u16 { - fn write_aml(&self, buf: &mut Vec) { - if *self <= u8::MAX as u16 { - (*self as u8).write_aml(buf); - } else { - buf.push(WORD_PREFIX); - buf.extend_from_slice(&self.to_le_bytes()); - } - } -} - -impl AmlWriter for u32 { - fn write_aml(&self, buf: &mut Vec) { - if *self <= u16::MAX as u32 { - (*self as u16).write_aml(buf); - } else { - buf.push(DWORD_PREFIX); - buf.extend_from_slice(&self.to_le_bytes()); - } - } -} - -impl AmlWriter for u64 { - fn write_aml(&self, buf: &mut Vec) { - if *self <= u32::MAX as u64 { - (*self as u32).write_aml(buf); - } else { - buf.push(QWORD_PREFIX); - buf.extend_from_slice(&self.to_le_bytes()); - } - } -} - -impl AmlWriter for &str { - fn write_aml(&self, buf: &mut Vec) { - buf.push(STRING_PREFIX); - buf.extend_from_slice(self.as_bytes()); - buf.push(0); - } -} - -impl AmlWriter for String { - fn write_aml(&self, buf: &mut Vec) { - self.as_str().write_aml(buf); - } -} - -impl AmlWriter for EisaId { - fn write_aml(&self, buf: &mut Vec) { - buf.push(DWORD_PREFIX); - buf.extend_from_slice(&self.0.to_le_bytes()); - } -} - -impl AmlWriter for Vec { - fn write_aml(&self, buf: &mut Vec) { - write_buffer(buf, self); - } -} - -impl AmlWriter for &[u8] { - fn write_aml(&self, buf: &mut Vec) { - write_buffer(buf, self); - } -} - -fn write_buffer(buf: &mut Vec, data: &[u8]) { - buf.push(BUFFER_OP); - - let mut size_buf = Vec::new(); - (data.len() as u64).write_aml(&mut size_buf); - - write_pkg_length(buf, size_buf.len() + data.len()); - buf.extend_from_slice(&size_buf); - buf.extend_from_slice(data); -} - -fn encode_pkg_length(total_len: usize) -> ([u8; MAX_PKG_LENGTH_BYTES], usize) { - let mut bytes = [0u8; MAX_PKG_LENGTH_BYTES]; - let size = pkg_length_size(total_len.saturating_sub(1)); - match size { - 1 => bytes[0] = total_len as u8, - 2 => { - bytes[0] = 0x40 | ((total_len & 0x0F) as u8); - bytes[1] = ((total_len >> 4) & 0xFF) as u8; - } - 3 => { - bytes[0] = 0x80 | ((total_len & 0x0F) as u8); - bytes[1] = ((total_len >> 4) & 0xFF) as u8; - bytes[2] = ((total_len >> 12) & 0xFF) as u8; - } - 4 => { - bytes[0] = 0xC0 | ((total_len & 0x0F) as u8); - bytes[1] = ((total_len >> 4) & 0xFF) as u8; - bytes[2] = ((total_len >> 12) & 0xFF) as u8; - bytes[3] = ((total_len >> 20) & 0xFF) as u8; - } - _ => unreachable!(), - } - (bytes, size) -} - -fn write_pkg_length(buf: &mut Vec, content_len: usize) { - let pkg_size = pkg_length_size(content_len); - let (bytes, size) = encode_pkg_length(pkg_size + content_len); - buf.extend_from_slice(&bytes[..size]); -} - -fn pkg_length_size(content_len: usize) -> usize { - if content_len < 0x3F - 1 { - 1 - } else if content_len < 0xFFF - 2 { - 2 - } else if content_len < 0xFFFFF - 3 { - 3 - } else { - 4 - } -} - -const MAX_PKG_LENGTH_BYTES: usize = 4; - -#[must_use = "call .finish() to get the AML bytes"] -pub struct AmlBuilder { - buf: Vec, -} - -impl AmlBuilder { - pub fn new() -> Self { - Self { buf: Vec::new() } - } - - pub fn scope(&mut self, name: &str) -> ScopeGuard<'_> { - ScopeGuard::new(self, name) - } - - pub fn device(&mut self, name: &str) -> DeviceGuard<'_> { - DeviceGuard::new(self, name) - } - - pub fn method( - &mut self, - name: &str, - arg_count: u8, - serialized: bool, - ) -> MethodGuard<'_> { - MethodGuard::new(self, name, arg_count, serialized) - } - - pub fn name(&mut self, name: &str, value: &T) { - self.buf.push(NAME_OP); - encode_name_string(name, &mut self.buf); - value.write_aml(&mut self.buf); - } - - pub fn name_package(&mut self, name: &str, elements: &[T]) { - self.buf.push(NAME_OP); - encode_name_string(name, &mut self.buf); - write_package(&mut self.buf, elements); - } - - pub fn raw(&mut self, bytes: &[u8]) { - self.buf.extend_from_slice(bytes); - } - - pub fn return_value(&mut self, value: &T) { - self.buf.push(RETURN_OP); - value.write_aml(&mut self.buf); - } - - pub fn finish(self) -> Vec { - self.buf - } - - pub fn len(&self) -> usize { - self.buf.len() - } - - pub fn is_empty(&self) -> bool { - self.buf.is_empty() - } - - pub fn as_bytes(&self) -> &[u8] { - &self.buf - } -} - -impl Default for AmlBuilder { - fn default() -> Self { - Self::new() - } -} - -fn write_package(buf: &mut Vec, elements: &[T]) { - buf.push(PACKAGE_OP); - - let mut content = Vec::new(); - content.push(elements.len() as u8); - for elem in elements { - elem.write_aml(&mut content); - } - - write_pkg_length(buf, content.len()); - buf.extend_from_slice(&content); -} - -pub fn write_package_raw(buf: &mut Vec, num_elements: u8, content: &[u8]) { - buf.push(PACKAGE_OP); - let len = 1 + content.len(); - write_pkg_length(buf, len); - buf.push(num_elements); - buf.extend_from_slice(content); -} - -/// ```compile_fail -/// use propolis::firmware::acpi::AmlBuilder; -/// let mut builder = AmlBuilder::new(); -/// { -/// let mut sb = builder.scope("\\_SB_"); -/// { -/// let mut pci = sb.device("PCI0"); -/// { -/// let dev = pci.device("DEV0"); -/// } -/// sb.device("DEV1"); // error: `sb` still borrowed by `pci` -/// } -/// } -/// ``` -pub struct ScopeGuard<'a> { - builder: &'a mut AmlBuilder, - start_pos: usize, - content_start: usize, -} - -impl<'a> ScopeGuard<'a> { - fn new(builder: &'a mut AmlBuilder, name: &str) -> Self { - builder.buf.push(SCOPE_OP); - let start_pos = builder.buf.len(); - builder.buf.extend_from_slice(&[0; MAX_PKG_LENGTH_BYTES]); - encode_name_string(name, &mut builder.buf); - let content_start = builder.buf.len(); - Self { builder, start_pos, content_start } - } - - pub fn scope(&mut self, name: &str) -> ScopeGuard<'_> { - ScopeGuard::new(self.builder, name) - } - - pub fn device(&mut self, name: &str) -> DeviceGuard<'_> { - DeviceGuard::new(self.builder, name) - } - - pub fn method( - &mut self, - name: &str, - arg_count: u8, - serialized: bool, - ) -> MethodGuard<'_> { - MethodGuard::new(self.builder, name, arg_count, serialized) - } - - pub fn name(&mut self, name: &str, value: &T) { - self.builder.name(name, value); - } - - pub fn name_package(&mut self, name: &str, elements: &[T]) { - self.builder.name_package(name, elements); - } - - pub fn processor(&mut self, name: &str, proc_id: u8) { - self.builder.buf.push(EXT_OP_PREFIX); - self.builder.buf.push(PROCESSOR_OP); - - let mut name_buf = Vec::new(); - encode_name_string(name, &mut name_buf); - write_pkg_length(&mut self.builder.buf, name_buf.len() + 6); - - self.builder.buf.extend_from_slice(&name_buf); - self.builder.buf.push(proc_id); - self.builder.buf.extend_from_slice(&[0u8; 4]); - self.builder.buf.push(0); - } - - pub fn raw(&mut self, bytes: &[u8]) { - self.builder.raw(bytes); - } -} - -impl Drop for ScopeGuard<'_> { - fn drop(&mut self) { - finalize_pkg_length( - &mut self.builder.buf, - self.start_pos, - self.content_start, - ); - } -} - -/// ```compile_fail -/// use propolis::firmware::acpi::AmlBuilder; -/// let mut builder = AmlBuilder::new(); -/// { -/// let mut sb = builder.scope("\\_SB_"); -/// { -/// let mut dev = sb.device("DEV0"); -/// { -/// let m = dev.method("_STA", 0, false); -/// } -/// dev.name("_UID", &0u32); -/// sb.device("DEV1"); // error: `sb` still borrowed by `dev` -/// } -/// } -/// ``` -pub struct DeviceGuard<'a> { - builder: &'a mut AmlBuilder, - start_pos: usize, - content_start: usize, -} - -impl<'a> DeviceGuard<'a> { - fn new(builder: &'a mut AmlBuilder, name: &str) -> Self { - builder.buf.push(EXT_OP_PREFIX); - builder.buf.push(DEVICE_OP); - let start_pos = builder.buf.len(); - builder.buf.extend_from_slice(&[0; MAX_PKG_LENGTH_BYTES]); - encode_name_string(name, &mut builder.buf); - let content_start = builder.buf.len(); - Self { builder, start_pos, content_start } - } - - pub fn device(&mut self, name: &str) -> DeviceGuard<'_> { - DeviceGuard::new(self.builder, name) - } - - pub fn method( - &mut self, - name: &str, - arg_count: u8, - serialized: bool, - ) -> MethodGuard<'_> { - MethodGuard::new(self.builder, name, arg_count, serialized) - } - - pub fn name(&mut self, name: &str, value: &T) { - self.builder.name(name, value); - } - - pub fn name_package(&mut self, name: &str, elements: &[T]) { - self.builder.name_package(name, elements); - } - - pub fn raw(&mut self, bytes: &[u8]) { - self.builder.raw(bytes); - } -} - -impl Drop for DeviceGuard<'_> { - fn drop(&mut self) { - finalize_pkg_length( - &mut self.builder.buf, - self.start_pos, - self.content_start, - ); - } -} - -/// ```compile_fail -/// use propolis::firmware::acpi::AmlBuilder; -/// let mut builder = AmlBuilder::new(); -/// { -/// let mut sb = builder.scope("\\_SB_"); -/// { -/// let mut dev = sb.device("DEV0"); -/// { -/// let m = dev.method("_STA", 0, false); -/// dev.method("_ON_", 0, false); // error: `dev` still borrowed by `m` -/// } -/// } -/// } -/// ``` -pub struct MethodGuard<'a> { - builder: &'a mut AmlBuilder, - start_pos: usize, - content_start: usize, -} - -impl<'a> MethodGuard<'a> { - fn new( - builder: &'a mut AmlBuilder, - name: &str, - arg_count: u8, - serialized: bool, - ) -> Self { - assert!(arg_count <= 7, "method can have at most 7 arguments"); - builder.buf.push(METHOD_OP); - let start_pos = builder.buf.len(); - builder.buf.extend_from_slice(&[0; MAX_PKG_LENGTH_BYTES]); - encode_name_string(name, &mut builder.buf); - let flags = arg_count | if serialized { 0x08 } else { 0 }; - builder.buf.push(flags); - let content_start = builder.buf.len(); - Self { builder, start_pos, content_start } - } - - pub fn store_arg_to_name(&mut self, arg: u8, name: &str) { - self.builder.buf.push(STORE_OP); - self.builder.buf.push(ARG0_OP + arg); - encode_name_string(name, &mut self.builder.buf); - } - - pub fn return_value(&mut self, value: &T) { - self.builder.return_value(value); - } - - pub fn return_name(&mut self, name: &str) { - self.builder.buf.push(RETURN_OP); - encode_name_string(name, &mut self.builder.buf); - } - - pub fn return_arg(&mut self, n: u8) { - assert!(n <= 6); - self.builder.buf.push(RETURN_OP); - self.builder.buf.push(ARG0_OP + n); - } - - pub fn raw(&mut self, bytes: &[u8]) { - self.builder.raw(bytes); - } - - pub fn create_dword_field(&mut self, source: u8, offset: u8, name: &str) { - self.builder.buf.push(CREATE_DWORD_FIELD_OP); - self.builder.buf.push(source); - if offset == 0 { - self.builder.buf.push(ZERO_OP); - } else { - self.builder.buf.push(BYTE_PREFIX); - self.builder.buf.push(offset); - } - encode_name_string(name, &mut self.builder.buf); - } - - pub fn if_uuid_equal( - &mut self, - uuid: &[u8; 16], - body: impl FnOnce(&mut Self), - ) { - self.builder.buf.push(IF_OP); - let start = self.builder.buf.len(); - self.builder.buf.extend_from_slice(&[0; MAX_PKG_LENGTH_BYTES]); - - self.builder.buf.push(LEQUAL_OP); - self.builder.buf.push(ARG0_OP); - - self.builder.buf.push(BUFFER_OP); - let buffer_start = self.builder.buf.len(); - self.builder.buf.extend_from_slice(&[0; MAX_PKG_LENGTH_BYTES]); - self.builder.buf.push(BYTE_PREFIX); - self.builder.buf.push(UUID_SIZE as u8); - let buffer_content_start = self.builder.buf.len(); - self.builder.buf.extend_from_slice(uuid); - finalize_pkg_length( - &mut self.builder.buf, - buffer_start, - buffer_content_start, - ); - - let content_start = self.builder.buf.len(); - body(self); - finalize_pkg_length(&mut self.builder.buf, start, content_start); - } - - pub fn else_block(&mut self, body: impl FnOnce(&mut Self)) { - self.builder.buf.push(ELSE_OP); - let start = self.builder.buf.len(); - self.builder.buf.extend_from_slice(&[0; MAX_PKG_LENGTH_BYTES]); - let content_start = self.builder.buf.len(); - body(self); - finalize_pkg_length(&mut self.builder.buf, start, content_start); - } - - pub fn and_to(&mut self, name: &str, mask: u32) { - self.builder.buf.push(AND_OP); - encode_name_string(name, &mut self.builder.buf); - mask.write_aml(&mut self.builder.buf); - encode_name_string(name, &mut self.builder.buf); - } - - pub fn or_to(&mut self, name: &str, value: u32) { - self.builder.buf.push(OR_OP); - encode_name_string(name, &mut self.builder.buf); - value.write_aml(&mut self.builder.buf); - encode_name_string(name, &mut self.builder.buf); - } - - pub fn store(&mut self, source: &str, dest: &str) { - self.builder.buf.push(STORE_OP); - encode_name_string(source, &mut self.builder.buf); - encode_name_string(dest, &mut self.builder.buf); - } -} - -impl Drop for MethodGuard<'_> { - fn drop(&mut self) { - finalize_pkg_length( - &mut self.builder.buf, - self.start_pos, - self.content_start, - ); - } -} - -fn finalize_pkg_length( - buf: &mut Vec, - start_pos: usize, - content_start: usize, -) { - let name_len = content_start - start_pos - MAX_PKG_LENGTH_BYTES; - let body_len = buf.len() - content_start; - let content_len = name_len + body_len; - let pkg_size = pkg_length_size(content_len); - let (pkg_bytes, size) = encode_pkg_length(pkg_size + content_len); - - buf.splice( - start_pos..start_pos + MAX_PKG_LENGTH_BYTES, - pkg_bytes[..size].iter().copied(), - ); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn integer_encoding() { - let mut buf = Vec::new(); - 0u8.write_aml(&mut buf); - assert_eq!(buf, vec![ZERO_OP]); - - buf.clear(); - 1u8.write_aml(&mut buf); - assert_eq!(buf, vec![ONE_OP]); - - buf.clear(); - 42u8.write_aml(&mut buf); - assert_eq!(buf, vec![BYTE_PREFIX, 42]); - - buf.clear(); - 0x1234u16.write_aml(&mut buf); - assert_eq!(buf, vec![WORD_PREFIX, 0x34, 0x12]); - - buf.clear(); - 0xDEADBEEFu32.write_aml(&mut buf); - assert_eq!(buf, vec![DWORD_PREFIX, 0xEF, 0xBE, 0xAD, 0xDE]); - - buf.clear(); - 0x123456789ABCDEF0u64.write_aml(&mut buf); - assert_eq!(buf[0], QWORD_PREFIX); - } - - #[test] - fn string_encoding() { - let mut buf = Vec::new(); - "Hello".write_aml(&mut buf); - assert_eq!(buf, vec![STRING_PREFIX, b'H', b'e', b'l', b'l', b'o', 0]); - } - - #[test] - fn scope_with_named_object() { - let mut builder = AmlBuilder::new(); - { - let mut scope = builder.scope("_SB_"); - scope.name("TEST", &42u8); - } - let aml = builder.finish(); - - assert_eq!(aml[0], SCOPE_OP); - assert!(aml.windows(4).any(|w| w == b"TEST")); - } - - #[test] - fn device_in_scope() { - use super::super::names::EisaId; - - let mut builder = AmlBuilder::new(); - { - let mut sb = builder.scope("\\_SB_"); - { - let mut dev = sb.device("PCI0"); - dev.name("_HID", &EisaId::from_str("PNP0A08")); - } - } - let aml = builder.finish(); - - assert_eq!(aml[0], SCOPE_OP); - assert!(aml.windows(2).any(|w| w == [EXT_OP_PREFIX, DEVICE_OP])); - } - - #[test] - fn method_with_return() { - let mut builder = AmlBuilder::new(); - { - let mut scope = builder.scope("_SB_"); - { - let mut method = scope.method("_STA", 0, false); - method.return_value(&0x0Fu8); - } - } - let aml = builder.finish(); - - assert!(aml.windows(1).any(|w| w == [METHOD_OP])); - assert!(aml.windows(4).any(|w| w == b"_STA")); - } - - #[test] - fn nested_scopes() { - let mut builder = AmlBuilder::new(); - { - let mut sb = builder.scope("\\_SB_"); - { - let mut pci = sb.scope("PCI0"); - pci.name("_ADR", &0u32); - } - } - let aml = builder.finish(); - - let scope_count = aml.iter().filter(|&&b| b == SCOPE_OP).count(); - assert_eq!(scope_count, 2); - } - - #[test] - fn pkg_length_single_byte() { - let mut builder = AmlBuilder::new(); - { - let scope = builder.scope("TEST"); - drop(scope); - } - let aml = builder.finish(); - - assert_eq!(aml.len(), 6); - assert_eq!(aml[0], SCOPE_OP); - assert_eq!(aml[1], 5); - } - - #[test] - fn pkg_length_zero() { - let (bytes, size) = encode_pkg_length(0); - assert_eq!(size, 1); - assert_eq!(bytes[0], 0); - } -} diff --git a/lib/propolis/src/firmware/acpi/dsdt.rs b/lib/propolis/src/firmware/acpi/dsdt.rs index d54dba9d0..945184609 100644 --- a/lib/propolis/src/firmware/acpi/dsdt.rs +++ b/lib/propolis/src/firmware/acpi/dsdt.rs @@ -2,292 +2,1189 @@ // 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 super::aml::{AmlBuilder, AmlWriter, ScopeGuard}; -use super::names::EisaId; -use super::resources::ResourceTemplateBuilder; +//! 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. +// XXX(acpi): Most of the DSDT and SSDT tables are generated here to keep them +// consistent with the original EDK2 static tables. In the future +// they could be created by the devices they represent. For example, +// the _SB scope can be created by the I440FxHostBridge struct, the +// LPC by Piix3Lpc etc., but they currently lack all the information +// necessary to generate the tables. This pattern is already used +// for some devices when possible. +// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiTables/Dsdt.asl + +use super::{IO_APIC_ADDR, LOCAL_APIC_ADDR, PCI_LINK_IRQS, SCI_IRQ}; +use acpi_tables::{aml, sdt::Sdt, Aml, AmlSink}; + +/// The ACPI scope in which DsdtGenerators are placed. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DsdtScope { - SystemBus, - PciRoot, + SystemBus, // \_SB scope. + PciRoot, // \_SB.PCI0 scope. + Lpc, // \_SB.PCI0.LPC scope. } -pub trait DsdtGenerator { +/// 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; - fn generate_dsdt(&self, scope: &mut ScopeGuard<'_>); } -const PCI_CONFIG_IO_BASE: u16 = 0x0CF8; -const PCI_CONFIG_IO_SIZE: u8 = 8; - -const PCI_IO_BASE: u16 = 0x1000; -const PCI_IO_LIMIT: u16 = 0xFFFF; -const PCI_IO_SIZE: u16 = PCI_IO_LIMIT - PCI_IO_BASE + 1; +/// 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, +} -const PCI_INT_PINS: u8 = 4; -const PCI_GSI_BASE: u32 = 16; -const PCI_SLOTS: u8 = 32; -const PRT_ENTRY_SIZE: u8 = 4; -const PCI_ADR_ALL_FUNC: u32 = 0xFFFF; +impl<'a> DsdtGeneratorAml<'a> { + fn new(generators: &'a [&'a dyn DsdtGenerator], scope: DsdtScope) -> Self { + Self { generators, scope } + } +} -const IO_ALIGN_BYTE: u8 = 1; +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)); + } +} -const SLP_TYP_S0: u8 = 5; -const SLP_TYP_S3: u8 = 1; -const SLP_TYP_S4: u8 = 6; -const SLP_TYP_S5: u8 = 7; +/// Values for the PM1a_CNT.SLP_TYP register to enter different sleep states. +/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/07_Power_and_Performance_Mgmt/oem-supplied-system-level-control-methods.html?highlight=_sx#sx-system-states +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; -#[derive(Clone, Copy)] -pub struct PcieConfig { - pub ecam_base: u64, - pub ecam_size: u64, - pub bus_start: u8, - pub bus_end: u8, - pub mmio32_base: u64, - pub mmio32_limit: u64, - pub mmio64_base: u64, - pub mmio64_limit: u64, +pub struct DsdtConfig<'a> { + pub generators: &'a [&'a dyn DsdtGenerator], } -pub struct DsdtConfig { - pub pcie: Option, +/// The DSDT table is part of the fixed ACPI tables and is used to describe +/// system resources. +/// +/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#differentiated-system-description-table-dsdt +pub struct Dsdt<'a> { + config: DsdtConfig<'a>, } -struct PrtEntry { - slot: u8, - pin: u8, - gsi: u32, +impl<'a> Dsdt<'a> { + pub fn new(config: DsdtConfig<'a>) -> Self { + Self { config } + } } -impl AmlWriter for PrtEntry { - fn write_aml(&self, buf: &mut Vec) { - let addr: u32 = ((self.slot as u32) << 16) | PCI_ADR_ALL_FUNC; +impl<'a> Aml for Dsdt<'a> { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + let mut dsdt = Vec::new(); - let mut content = Vec::new(); - addr.write_aml(&mut content); - self.pin.write_aml(&mut content); - 0u8.write_aml(&mut content); - self.gsi.write_aml(&mut content); + // XXX(acpi): 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. + aml::If::new( + &aml::ZERO, + vec![&aml::External::new( + "\\_SB_.PCI0._CRS.FWDT".into(), + aml::ExternalObjectType::OperationRegion, + None, + )], + ) + .to_aml_bytes(&mut dsdt); - super::aml::write_package_raw(buf, PRT_ENTRY_SIZE, &content); + // 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. + // XXX(acpi): OEM ID, table ID, and revision are kept the same as the + // original static EDK2 tables for consistency. They could + // be set to Propolis-specific values in the future. + let mut sdt = Sdt::new(*b"DSDT", 36, 1, *b"INTEL ", *b"OVMF ", 0x4); + sdt.append_slice(dsdt.as_slice()); + sdt.to_aml_bytes(sink); } } -pub fn build_dsdt_aml( - config: &DsdtConfig, - generators: &[&dyn DsdtGenerator], -) -> Vec { - let mut builder = AmlBuilder::new(); - - builder.name("PICM", &0u8); +/// Describe a sleep state. +struct SleepState<'a> { + state: &'a str, + pm1a_cnt: u8, +} - { - let mut pic = builder.method("_PIC", 1, false); - pic.store_arg_to_name(0, "PICM"); +impl<'a> SleepState<'a> { + fn new(state: &'a str, pm1a_cnt: u8) -> Self { + Self { state, pm1a_cnt } } +} - builder.name_package("\\_S0_", &[SLP_TYP_S0, SLP_TYP_S0, 0, 0]); - builder.name_package("\\_S3_", &[SLP_TYP_S3, SLP_TYP_S3, 0, 0]); - builder.name_package("\\_S4_", &[SLP_TYP_S4, SLP_TYP_S4, 0, 0]); - builder.name_package("\\_S5_", &[SLP_TYP_S5, SLP_TYP_S5, 0, 0]); - - { - let mut sb = builder.scope("\\_SB_"); +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); + } +} - if let Some(pcie) = &config.pcie { - build_pcie_host_bridge(&mut sb, pcie); - build_motherboard_resources(&mut sb, pcie); - } +/// PCI root bridge namespace (\_SB.PCI0). +struct PciRootBridge<'a> { + generators: &'a [&'a dyn DsdtGenerator], +} - for generator in generators { - if generator.dsdt_scope() == DsdtScope::SystemBus { - generator.generate_dsdt(&mut sb); - } - } +impl<'a> Aml for PciRootBridge<'a> { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + aml::Device::new( + "PCI0".into(), + vec![ + &aml::Name::new("_HID".into(), &aml::EISAName::new("PNP0A03")), + &aml::Name::new("_ADR".into(), &aml::ZERO), + &aml::Name::new("_BBN".into(), &aml::ZERO), + &aml::Name::new("_UID".into(), &aml::ZERO), + &PciRootBridgeCrs {}, + &PciRootBridgePrt {}, + &PciRootBridgeLpc { generators: self.generators }, + &DsdtGeneratorAml::new(self.generators, DsdtScope::PciRoot), + ], + ) + .to_aml_bytes(sink); } - - builder.finish() } -fn build_pcie_host_bridge( - sb: &mut super::aml::ScopeGuard<'_>, - pcie: &PcieConfig, -) { - let mut pci0 = sb.device("PCI0"); +/// I/O port range for PCI configuration. +/// https://wiki.osdev.org/PCI#Configuration_Space_Access_Mechanism_#1 +const PCI_CONFIG_IO_BASE: u16 = 0x0cf8; +const PCI_CONFIG_IO_SIZE: u8 = 8; + +/// 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. +/// https://en.wikipedia.org/wiki/Video_Graphics_Array#Use +const LEGACY_VGA_BASE: u32 = 0x000a_0000; +const LEGACY_VGA_LIMIT: u32 = 0x000b_ffff; - pci0.name("_HID", &EisaId::from_str("PNP0A08")); - pci0.name("_CID", &EisaId::from_str("PNP0A03")); - pci0.name("_SEG", &0u32); - pci0.name("_UID", &0u32); - pci0.name("_ADR", &0u32); +/// _CRS method for the PCI0 device (\_SB.PCI0._CRS). +/// +/// Refer to section 4.3 of the PCI Firmware Specification for more +/// information. +struct PciRootBridgeCrs {} - let mut crs = ResourceTemplateBuilder::new(); +// XXX(acpi): This implementation currently follows the original static EDK2 +// tables. It can be simplified to return a single ResourceTemplate +// with all final values already populated instead of dynamically +// updating them based on the values 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(); - let bus_count = (pcie.bus_end as u16) - (pcie.bus_start as u16) + 1; - crs.word_bus_number( - pcie.bus_start as u16, - pcie.bus_end as u16, - 0, - bus_count, - ); + // 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); - crs.io( - PCI_CONFIG_IO_BASE, - PCI_CONFIG_IO_BASE, - IO_ALIGN_BYTE, - PCI_CONFIG_IO_SIZE, - ); + // Legacy PCI configuration I/O ports. + let pci_config_io_ports = aml::IO::new( + PCI_CONFIG_IO_BASE, + PCI_CONFIG_IO_BASE, + 1, + PCI_CONFIG_IO_SIZE, + ); + cres.push(&pci_config_io_ports); - crs.io_range(PCI_IO_BASE, PCI_IO_LIMIT, PCI_IO_SIZE); + // I/O ports below the PCI config ports (0x0000-0x0cf7). + let pci_io_ports_low = + aml::AddressSpace::new_io(0x0000, PCI_CONFIG_IO_BASE - 1, None); + cres.push(&pci_io_ports_low); - let ecam_end = pcie.ecam_base + pcie.ecam_size; + // IO ports above the PCI config ports (0x0d00-0xffff). + let pci_io_ports_high = aml::AddressSpace::new_io( + PCI_CONFIG_IO_BASE + PCI_CONFIG_IO_SIZE as u16, + 0xffff, + None, + ); + cres.push(&pci_io_ports_high); - if pcie.ecam_base > pcie.mmio32_base { - let len = pcie.ecam_base - pcie.mmio32_base; - crs.dword_memory( - false, + // Legacy VGA MMIO region. + let legacy_vga = aml::AddressSpace::new_memory( + aml::AddressSpaceCacheable::Cacheable, true, - pcie.mmio32_base as u32, - (pcie.ecam_base - 1) as u32, - 0, - len as u32, + LEGACY_VGA_BASE, + LEGACY_VGA_LIMIT, + None, ); - } + cres.push(&legacy_vga); - if pcie.mmio32_limit >= ecam_end { - let len = pcie.mmio32_limit - ecam_end + 1; - crs.dword_memory( - false, + // 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, - ecam_end as u32, - pcie.mmio32_limit as u32, - 0, - len as u32, + 0xf800_0000_u32, + 0xfffb_ffff_u32, + None, ); - } + cres.push(&mmio32); - if pcie.mmio64_limit > pcie.mmio64_base { - let len = pcie.mmio64_limit - pcie.mmio64_base + 1; - crs.qword_memory( - false, + 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, - pcie.mmio64_base, - pcie.mmio64_limit, - 0, - len, + 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 read 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. + aml::Method::new( + "_CRS".into(), + 0, + true, + vec![ + &aml::Field::new( + // Create references to values in the FWDT OperationRegion. + "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 fields that reference values from the mmio32 + // AddressSpace from CRES. + &aml::CreateDWordField::new( + &aml::Path::new("PS32"), + &aml::Path::new("CRES"), + &(mmio32_offset + 0x0a), // Byte offset for min. + ), + &aml::CreateDWordField::new( + &aml::Path::new("PE32"), + &aml::Path::new("CRES"), + &(mmio32_offset + 0x0e), // Byte offset for max. + ), + &aml::CreateDWordField::new( + &aml::Path::new("PL32"), + &aml::Path::new("CRES"), + &(mmio32_offset + 0x16), // Byte offset for len. + ), + // Update the values of mmio32 based on the FWDT. + &aml::Store::new( + &aml::Path::new("PS32"), + &aml::Path::new("P0SL"), + ), + &aml::Store::new( + &aml::Path::new("PE32"), + &aml::Path::new("P0EL"), + ), + &aml::Store::new( + &aml::Path::new("PL32"), + &aml::Path::new("P0LL"), + ), + // Check if a 64-bit MMIO region is needed. + &aml::If::new( + &aml::LogicalAnd::new( + &aml::Equal::new(&aml::Path::new("P1SL"), &aml::ZERO), + &aml::Equal::new(&aml::Path::new("P1SH"), &aml::ZERO), + ), + vec![&aml::Return::new(&aml::Path::new("CRES"))], + ), + &aml::Else::new(vec![ + // Create fields that reference values from the mmio64 + // AddressSpace from CR64. + &aml::CreateQWordField::new( + &aml::Path::new("PS64"), + &aml::Path::new("CR64"), + &(mmio64_offset + 0x0e), + ), + &aml::CreateQWordField::new( + &aml::Path::new("PE64"), + &aml::Path::new("CR64"), + &(mmio64_offset + 0x16), + ), + &aml::CreateQWordField::new( + &aml::Path::new("PL64"), + &aml::Path::new("CR64"), + &(mmio64_offset + 0x26), + ), + // Update the values of mmio64 based on the FWDT. + &aml::Store::new( + &aml::Path::new("PS64"), + &aml::Path::new("P1S_"), + ), + &aml::Store::new( + &aml::Path::new("PE64"), + &aml::Path::new("P1E_"), + ), + &aml::Store::new( + &aml::Path::new("PL64"), + &aml::Path::new("P1L_"), + ), + // 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(); + vec.iter().fold(0, |_acc, aml| { + aml.to_aml_bytes(&mut sink); + sink.len() + }) +} - pci0.name("_CRS", &crs); +/// Number of devices in the PCI0 root bridge. +// XXX(acpi): Value inherited from the original EDK2 static tables. It should +// be updated to match Propolis's expectations. +const PCI_DEVICES: u8 = 16; +const PCI_INT_PINS: u8 = 4; - let mut prt_entries: Vec = Vec::new(); - for slot in 1..PCI_SLOTS { - for pin in 0..PCI_INT_PINS { - let gsi = PCI_GSI_BASE - + (((slot as u32) + (pin as u32)) % (PCI_INT_PINS as u32)); - prt_entries.push(PrtEntry { slot, pin, gsi }); +/// _PRT method for the PCI0 device (\_SB.PCI0._PRT) +/// +/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html?highlight=_prt#prt-pci-routing-table +struct PciRootBridgePrt {} + +impl Aml for PciRootBridgePrt { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + let sources = + vec!["^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(); + + aml::Method::new( + "_PRT".into(), + 0, + false, + vec![&aml::Return::new(&aml::Package::new(ptr))], + ) + .to_aml_bytes(sink); } - pci0.name_package("_PRT", &prt_entries); - pci0.name("SUPP", &0u32); +} + +/// Low-word of an _ADR value that refers to all PCI functions. +/// +/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#adr-object-address-encodings +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, +} - build_pcie_osc_method(&mut pci0); +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); + } } -fn build_pcie_osc_method(dev: &mut super::aml::DeviceGuard<'_>) { - use super::names::{encode_uuid, UUID_SIZE}; - use super::opcodes::*; +/// 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. +/// +/// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiTables/Dsdt.asl#L299-L303 +struct PciRootBridgeLpc<'a> { + generators: &'a [&'a dyn DsdtGenerator], +} - const PCIE_UUID: [u8; UUID_SIZE] = - encode_uuid("33DB4D5B-1FF7-401C-9657-7441C03DD766"); - const OSC_STATUS_UNSUPPORT_UUID: u32 = 1 << 2; - const OSC_CTRL_PCIE_HP: u32 = 1 << 0; - const OSC_CTRL_SHPC_HP: u32 = 1 << 1; - const OSC_CTRL_PCIE_PME: u32 = 1 << 2; - const OSC_CTRL_PCIE_AER: u32 = 1 << 3; - const OSC_CTRL_PCIE_CAP: u32 = 1 << 4; +// XXX(acpi): 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 in a virtual machine environment. +impl<'a> Aml for PciRootBridgeLpc<'a> { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + aml::Device::new( + "LPC_".into(), + vec![ + &aml::Name::new("_ADR".into(), &0x0001_0000_u64), + &Lnk::new("S", 0), + // PCI Interrupt Routing Configuration Registers, PIRQRC[A:D]. + &aml::OpRegion::new( + "PRR0".into(), + aml::OpRegionSpace::PCIConfig, + &0x60_u64, + &0x04_u64, + ), + &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, + false, + vec![ + &aml::If::new( + &aml::And::new(&aml::ZERO, &aml::Arg(0), &0x80_u64), + 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, + true, + 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"), + &0x05_u64, // TODO: document. + ), + &aml::If::new( + &aml::LogicalNot::new(&aml::And::new( + &aml::ZERO, + &aml::Arg(0), + &0x80_u64, + )), + 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 != SCI_IRQ) // The SCI has special handling LNKS. + .map(|i| *i as u32) + .collect(), + )]), + ), + &Lnk::new("A", 1), + &Lnk::new("B", 2), + &Lnk::new("C", 3), + &Lnk::new("D", 4), + // Programmable Interrupt Controller (PIC). + &aml::Device::new( + "PIC_".into(), + vec![ + &aml::Name::new( + "_HID".into(), + &aml::EISAName::new("PNP0000"), + ), + &aml::Name::new( + "_CRS".into(), + &aml::ResourceTemplate::new(vec![ + &aml::IO::new(0x0020, 0x0020, 0x00, 0x02), + &aml::IO::new(0x00A0, 0x00A0, 0x00, 0x02), + &aml::IO::new(0x04d0, 0x04d0, 0x00, 0x02), + &aml::IrqNoFlags::new(2), + ]), + ), + ], + ), + // ISA DMA. + &aml::Device::new( + "DMAC".into(), + vec![ + &aml::Name::new( + "_HID".into(), + &aml::EISAName::new("PNP0200"), + ), + &aml::Name::new( + "_CRS".into(), + &aml::ResourceTemplate::new(vec![ + &aml::IO::new(0x0000, 0x0000, 0x00, 0x10), + &aml::IO::new(0x0081, 0x0081, 0x00, 0x03), + &aml::IO::new(0x0087, 0x0087, 0x00, 0x01), + &aml::IO::new(0x0089, 0x0089, 0x00, 0x03), + &aml::IO::new(0x008f, 0x008f, 0x00, 0x01), + &aml::IO::new(0x00c0, 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![ + &aml::Name::new( + "_HID".into(), + &aml::EISAName::new("PNP0100"), + ), + &aml::Name::new( + "_CRS".into(), + &aml::ResourceTemplate::new(vec![ + &aml::IO::new(0x0040, 0x0040, 0x00, 0x04), + &aml::IrqNoFlags::new(0), + ]), + ), + ], + ), + // Real Time Clock. + &aml::Device::new( + "RTC_".into(), + vec![ + &aml::Name::new( + "_HID".into(), + &aml::EISAName::new("PNP0B00"), + ), + &aml::Name::new( + "_CRS".into(), + &aml::ResourceTemplate::new(vec![ + &aml::IO::new(0x0070, 0x0070, 0x00, 0x02), + &aml::IrqNoFlags::new(8), + ]), + ), + ], + ), + // PCAT Speaker. + &aml::Device::new( + "SPKR".into(), + vec![ + &aml::Name::new( + "_HID".into(), + &aml::EISAName::new("PNP0800"), + ), + &aml::Name::new( + "_CRS".into(), + &aml::ResourceTemplate::new(vec![&aml::IO::new( + 0x0061, 0x0061, 0x01, 0x01, + )]), + ), + ], + ), + // Floating Point Coprocessor. + &aml::Device::new( + "FPU_".into(), + vec![ + &aml::Name::new( + "_HID".into(), + &aml::EISAName::new("PNP0C04"), + ), + &aml::Name::new( + "_CRS".into(), + &aml::ResourceTemplate::new(vec![ + &aml::IO::new(0x00f0, 0x00f0, 0x00, 0x10), + &aml::IrqNoFlags::new(13), + ]), + ), + ], + ), + // Generic motherboard devices and pieces that don't fit + // anywhere else. + &aml::Device::new( + "XTRA".into(), + vec![ + &aml::Name::new( + "_HID".into(), + &aml::EISAName::new("PNP0C02"), + ), + &aml::Name::new("_UID".into(), &aml::ONE), + &aml::Name::new( + "_CRS".into(), + &aml::ResourceTemplate::new(vec![ + &aml::IO::new(0x0010, 0x0010, 0x00, 0x10), + &aml::IO::new(0x0022, 0x0022, 0x00, 0x1e), + &aml::IO::new(0x0044, 0x0044, 0x00, 0x1c), + &aml::IO::new(0x0062, 0x0062, 0x00, 0x02), + &aml::IO::new(0x0065, 0x0065, 0x00, 0x0b), + &aml::IO::new(0x0072, 0x0072, 0x00, 0x0e), + &aml::IO::new(0x0080, 0x0080, 0x00, 0x01), + &aml::IO::new(0x0084, 0x0084, 0x00, 0x03), + &aml::IO::new(0x0088, 0x0088, 0x00, 0x01), + &aml::IO::new(0x008c, 0x008c, 0x00, 0x03), + &aml::IO::new(0x0090, 0x0090, 0x00, 0x10), + &aml::IO::new(0x00a2, 0x00a2, 0x00, 0x1e), + &aml::IO::new(0x00e0, 0x00e0, 0x00, 0x10), + &aml::IO::new(0x01e0, 0x01e0, 0x00, 0x10), + &aml::IO::new(0x0160, 0x0160, 0x00, 0x10), + &aml::IO::new(0x0370, 0x0370, 0x00, 0x02), + &aml::IO::new(0x0402, 0x0402, 0x00, 0x01), + &aml::IO::new(0x0440, 0x0440, 0x00, 0x10), + // QEMU GPE0 BLK. + &aml::IO::new(0xafe0, 0xafe0, 0x00, 0x04), + // PMBLK1. + &aml::IO::new(0xb000, 0xb000, 0x00, 0x40), + // IO APIC. + &aml::Memory32Fixed::new( + false, + IO_APIC_ADDR, + 0x0000_1000, + ), + // LAPIC. + &aml::Memory32Fixed::new( + false, + LOCAL_APIC_ADDR, + 0x0010_0000, + ), + ]), + ), + ], + ), + &DsdtGeneratorAml::new(self.generators, DsdtScope::Lpc), + // QEMU panic device. + // + // XXX(acpi): This code could be generated by the QemuPvpanic + // struct and passed as a DsdtGenerator, but it's + // only present if the enable_isa configuration is + // enabled for the instance. So it's always + // generated here for now to maintain consistency + // with the original EDK2 static tables. + &aml::Device::new( + "PEVT".into(), + vec![ + &aml::Name::new("_HID".into(), &"QEMU0001"), + &aml::Name::new( + "_CRS".into(), + &aml::ResourceTemplate::new(vec![&aml::IO::new( + 0x0505, 0x0505, 0x01, 0x01, + )]), + ), + &aml::OpRegion::new( + "PEOR".into(), + aml::OpRegionSpace::SystemIO, + &0x0505_u64, + &aml::ONE, + ), + &aml::Field::new( + "PEOR".into(), + aml::FieldAccessType::Byte, + aml::FieldLockRule::NoLock, + aml::FieldUpdateRule::Preserve, + vec![aml::FieldEntry::Named(*b"PEPT", 8)], + ), + &aml::Name::new("_STA".into(), &0x0f_u64), + &aml::Method::new( + "RDPT".into(), + 0, + false, + vec![ + &aml::Store::new( + &aml::Local(0), + &aml::Path::new("PEPT"), + ), + &aml::Return::new(&aml::Local(0)), + ], + ), + &aml::Method::new( + "WRPT".into(), + 1, + false, + vec![&aml::Store::new( + &aml::Path::new("PEPT"), + &aml::Arg(0), + )], + ), + ], + ), + ], + ) + .to_aml_bytes(sink); + } +} - let unsupported_mask = !(OSC_CTRL_PCIE_HP - | OSC_CTRL_SHPC_HP - | OSC_CTRL_PCIE_PME - | OSC_CTRL_PCIE_AER - | OSC_CTRL_PCIE_CAP); +/// Represents a PCI IRQ link in the LPC device. +struct Lnk<'a> { + letter: &'a str, + uid: u32, +} - let mut osc = dev.method("_OSC", 4, false); +impl<'a> Lnk<'a> { + fn new(letter: &'a str, uid: u32) -> Self { + Self { letter, uid } + } +} - osc.create_dword_field(ARG3_OP, 0, "CDW1"); - osc.create_dword_field(ARG3_OP, 4, "CDW2"); - osc.create_dword_field(ARG3_OP, 8, "CDW3"); +impl<'a> Lnk<'a> { + // AML code for the special SCI link. + // https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiTables/Dsdt.asl#L200-L220 + fn to_aml_bytes_sci(&self, sink: &mut dyn AmlSink) { + aml::Device::new( + "LNKS".into(), + vec![ + &aml::Name::new("_HID".into(), &aml::EISAName::new("PNP0C0F")), + &aml::Name::new("_UID".into(), &aml::ZERO), + &aml::Name::new("_STA".into(), &0x0b_u64), + &aml::Method::new("_SRS".into(), 1, false, vec![]), + &aml::Method::new("_DIS".into(), 0, false, vec![]), + &aml::Name::new( + "_PRS".into(), + &aml::ResourceTemplate::new(vec![&aml::Interrupt::new( + true, + false, + false, + true, + vec![0x09], + )]), + ), + &aml::Method::new( + "_CRS".into(), + 0, + false, + vec![&aml::Return::new(&aml::Path::new("_PRS"))], + ), + ], + ) + .to_aml_bytes(sink); + } +} - osc.if_uuid_equal(&PCIE_UUID, |osc| { - osc.store("CDW2", "SUPP"); - osc.and_to("CDW3", unsupported_mask); - }); +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; + } - osc.else_block(|osc| { - osc.or_to("CDW1", OSC_STATUS_UNSUPPORT_UUID); - }); + let pir = aml::Path::new(&format!("PIR{}", self.letter)); - osc.return_arg(3); + aml::Device::new( + aml::Path::new(&format!("LNK{}", self.letter)), + vec![ + &aml::Name::new("_HID".into(), &aml::EISAName::new("PNP0C0F")), + &aml::Name::new("_UID".into(), &self.uid), + &aml::Method::new( + "_STA".into(), + 0, + false, + vec![&aml::Return::new(&aml::MethodCall::new( + "PSTA".into(), + vec![&pir], + ))], + ), + &aml::Method::new( + "_DIS".into(), + 0, + false, + vec![&aml::Or::new(&pir, &pir, &0x80_u64)], + ), + &aml::Method::new( + "_CRS".into(), + 0, + false, + vec![&aml::Return::new(&aml::MethodCall::new( + "PCRS".into(), + vec![&pir], + ))], + ), + &aml::Method::new( + "_PRS".into(), + 0, + false, + vec![&aml::Return::new(&aml::MethodCall::new( + "PPRS".into(), + vec![], + ))], + ), + &aml::Method::new( + "_SRS".into(), + 1, + false, + 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); + } } -/// Build a PNP0C02 motherboard resources device to reserve ECAM space. +/// Length in bytes of the SSDT header. Used to calculate the offset of other +/// fields. +/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html?highlight=ssdt#secondary-system-description-table-fields-ssdt +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. /// -/// Per PCI Firmware Spec 3.2, sec 4.1.2, the ECAM region must be reserved -/// by declaring a motherboard resource with _HID PNP0C02. The ECAM must -/// not be declared in the PCI host bridge's _CRS. -fn build_motherboard_resources( - sb: &mut super::aml::ScopeGuard<'_>, - pcie: &PcieConfig, -) { - let mut mres = sb.device("MRES"); - - mres.name("_HID", &EisaId::from_str("PNP0C02")); - - let mut crs = ResourceTemplateBuilder::new(); - if pcie.ecam_base + pcie.ecam_size - 1 >= (1u64 << 32) { - crs.qword_memory( - false, - false, - pcie.ecam_base, - pcie.ecam_base + pcie.ecam_size - 1, - 0, - pcie.ecam_size, - ); - } else { - crs.dword_memory( - false, - false, - pcie.ecam_base as u32, - (pcie.ecam_base + pcie.ecam_size - 1) as u32, - 0, - pcie.ecam_size as u32, - ); +/// SSDT header (36 bytes) + External operation prefix (1 byte) + +/// OperationRegion prefix (1 byte) + OperationRegion name (4 bytes) + +/// OperationRegion space (1 byte) + DWordPrefix (1 byte) +/// +/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/20_AML_Specification/AML_Specification.html#defopregion +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. +/// +/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#secondary-system-description-table-ssdt +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 } + } +} + +// XXX(acpi): This implementation follows the original static EDK2 tables. It +// can probably be further simplified or eliminated entirely. +// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiPlatformDxe/Qemu.c#L426-L466 +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. + // + // XXX(acpi): This process can be removed if \_SB.PCI0._CRS returns a + // static ResourceTemplate that is generated with the right + // instance 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. + // XXX(acpi): These sleep states are kept to keep the SSDT consistent + // 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 consistent with 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); + + // XXX(acpi): OEM ID, table ID, and revision are kept the same as the + // original static EDK2 tables for consistency. They could + // be set to Propolis-specific values in the future. + let mut sdt = Sdt::new(*b"SSDT", 36, 1, *b"REDHAT", *b"OVMF ", 0x1); + 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. +// +// XXX(acpi): Created just to keep tables consistent with the original EDK2 +// tables. +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. +// +// XXX(acpi): Created just to keep tables consistent with the original EDK2 +// tables. +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); } - mres.name("_CRS", &crs); } #[cfg(test)] mod tests { 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 }, + ]; + + // Filter by SystemBus. + let mut got = Vec::new(); + DsdtGeneratorAml::new(&generators, DsdtScope::SystemBus) + .to_aml_bytes(&mut got); + + let mut expected = Vec::new(); + aml::Name::new("TEST".into(), &"SystemBus").to_aml_bytes(&mut expected); + assert_eq!(expected, got); + got.clear(); + + // Filter by PciRoot. + let mut got = Vec::new(); + DsdtGeneratorAml::new(&generators, DsdtScope::PciRoot) + .to_aml_bytes(&mut got); + + let mut expected = Vec::new(); + aml::Name::new("TEST".into(), &"PciRoot").to_aml_bytes(&mut expected); + assert_eq!(expected, got); + got.clear(); + + // Filter by Lpc. + let mut got = Vec::new(); + DsdtGeneratorAml::new(&generators, DsdtScope::Lpc) + .to_aml_bytes(&mut got); + + let mut expected = Vec::new(); + aml::Name::new("TEST".into(), &"Lpc").to_aml_bytes(&mut expected); + assert_eq!(expected, got); + got.clear(); + } + #[test] - fn basic() { + fn dsdt_valid_aml() { let config = DsdtConfig { - pcie: Some(PcieConfig { - ecam_base: 0xe000_0000, - ecam_size: 0x1000_0000, - bus_start: 0, - bus_end: 255, - mmio32_base: 0xc000_0000, - mmio32_limit: 0xfbff_ffff, - mmio64_base: 0x1_0000_0000, - mmio64_limit: 0xf_ffff_ffff, - }), + generators: &[ + &MockDsdtGenerator { scope: DsdtScope::SystemBus }, + &MockDsdtGenerator { scope: DsdtScope::PciRoot }, + &MockDsdtGenerator { scope: DsdtScope::Lpc }, + ], }; - let aml = build_dsdt_aml(&config, &[]); + 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"MRES")); + assert!(aml.windows(4).any(|w| w == b"_PRT")); + assert!(aml.windows(4).any(|w| w == b"LPC_")); } } diff --git a/lib/propolis/src/firmware/acpi/facs.rs b/lib/propolis/src/firmware/acpi/facs.rs new file mode 100644 index 000000000..bedc5fcb6 --- /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. +/// +/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#firmware-acpi-control-structure-facs +pub struct Facs {} + +impl Facs { + pub fn new() -> Self { + Self {} + } +} + +// XXX(acpi): 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..750ffa8bb --- /dev/null +++ b/lib/propolis/src/firmware/acpi/fadt.rs @@ -0,0 +1,151 @@ +// 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::{OEM_ID, OEM_REVISION, OEM_TABLE_ID, SCI_IRQ}; +use acpi_tables::{ + // XXX(acpi): Use version 3 to keep FADT table consistent with the original + // EKD2 static tables. + 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_EVT_BLK_ADDR: u32 = 0xb000; +const PM1A_CNT_BLK_ADDR: u32 = 0xb004; +const PM_TMR_BLK_ADDR: u32 = 0xb008; +const GPE0_BLK_ADDR: u32 = 0xafe0; + +const PM1A_EVT_BLK_LEN: u8 = 4; +const PM1A_CNT_BLK_LEN: u8 = 2; +const PM_TMR_BLK_LEN: u8 = 4; +const GPE0_BLK_LEN: u8 = 4; + +// Represent 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. +/// +/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#fixed-acpi-description-table-fadt +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 } + } +} + +// XXX(acpi): Values retained from the original EDK2 static tables. +// fwts reports 1 high failure for this table: +// - fadt: FADT X_GPE0_BLK Access width 0x00 but it should be 1 (byte access). +// +// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiTables/Platform.h#L25-L56 +impl Aml for Fadt { + 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 = (SCI_IRQ as u16).into(); + fadt.smi_cmd = 0xb2.into(); + fadt.acpi_enable = 0xf1.into(); + fadt.acpi_disable = 0xf0.into(); + + fadt.pm1a_evt_blk = PM1A_EVT_BLK_ADDR.into(); + fadt.pm1a_cnt_blk = PM1A_CNT_BLK_ADDR.into(); + fadt.pm_tmr_blk = PM_TMR_BLK_ADDR.into(); + fadt.gpe0_blk = GPE0_BLK_ADDR.into(); + + fadt.pm1_evt_len = PM1A_EVT_BLK_LEN.into(); + fadt.pm1_cnt_len = PM1A_CNT_BLK_LEN.into(); + fadt.pm_tmr_len = PM_TMR_BLK_LEN.into(); + fadt.gpe0_blk_len = GPE0_BLK_LEN.into(); + + fadt.p_lvl2_lat = 101.into(); + 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, + 0x0cf9, + ); + fadt.reset_value = 0x06; + + fadt.x_pm1a_evt_blk = GAS::new( + AddressSpace::SystemIo, + PM1A_EVT_BLK_LEN * 8, + 0, + AccessSize::Undefined, + PM1A_EVT_BLK_ADDR 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); + } +} diff --git a/lib/propolis/src/firmware/acpi/madt.rs b/lib/propolis/src/firmware/acpi/madt.rs new file mode 100644 index 000000000..0032dc167 --- /dev/null +++ b/lib/propolis/src/firmware/acpi/madt.rs @@ -0,0 +1,101 @@ +// 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. +/// +/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#multiple-apic-description-table-madt +pub struct Madt<'a> { + config: &'a MadtConfig, +} + +impl<'a> Madt<'a> { + pub fn new(config: &'a MadtConfig) -> Self { + Self { config } + } +} + +// XXX(acpi): Values retained from the original EDK2 static tables. +// 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 +// +// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiTables/Madt.aslc +// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiPlatformDxe/Qemu.c#L58 +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#i-o-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 + 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 index 0117a7149..c48f0e180 100644 --- a/lib/propolis/src/firmware/acpi/mod.rs +++ b/lib/propolis/src/firmware/acpi/mod.rs @@ -4,15 +4,45 @@ //! ACPI table and AML bytecode generation. -pub mod aml; pub mod dsdt; -pub mod names; -pub mod opcodes; -pub mod resources; -pub mod tables; - -pub use aml::{AmlBuilder, AmlWriter, DeviceGuard, MethodGuard, ScopeGuard}; -pub use dsdt::{build_dsdt_aml, DsdtConfig, DsdtGenerator, DsdtScope, PcieConfig}; -pub use names::{encode_uuid, EisaId}; -pub use resources::ResourceTemplateBuilder; -pub use tables::{Dsdt, Facs, Fadt, Hpet, Madt, Mcfg, Rsdt, Rsdp, Xsdt}; +pub mod facs; +pub mod fadt; +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 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. + +// XXX(acpi): 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; + +const SCI_IRQ: u8 = 0x09; +const PCI_LINK_IRQS: [u8; 4] = [0x05, SCI_IRQ, 0x0a, 0x0b]; + +const IO_APIC_ADDR: u32 = 0xfec0_0000; +const LOCAL_APIC_ADDR: u32 = 0xfee0_0000; diff --git a/lib/propolis/src/firmware/acpi/names.rs b/lib/propolis/src/firmware/acpi/names.rs deleted file mode 100644 index b1abd3805..000000000 --- a/lib/propolis/src/firmware/acpi/names.rs +++ /dev/null @@ -1,256 +0,0 @@ -// 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 name encoding utilities. -//! -//! See ACPI Specification 6.4, Section 20.2.2 for Name Objects Encoding. - -use super::opcodes::{DUAL_NAME_PREFIX, MULTI_NAME_PREFIX, ROOT_PREFIX}; - -pub const MAX_NAME_SEGS: usize = 255; - -pub const NAME_SEG_SIZE: usize = 4; - -/// Encode a 4-character ACPI name segment, padding shorter names with '_'. -pub fn encode_name_seg(name: &str) -> [u8; NAME_SEG_SIZE] { - assert!(name.len() <= NAME_SEG_SIZE, "name segment too long: {}", name); - assert!(!name.is_empty(), "name segment cannot be empty"); - - let bytes = name.as_bytes(); - - let first = bytes[0]; - assert!( - first.is_ascii_uppercase() || first == b'_', - "invalid first character in name segment: {}", - name - ); - - for &c in &bytes[1..] { - assert!( - c.is_ascii_uppercase() || c.is_ascii_digit() || c == b'_', - "invalid character in name segment: {}", - name - ); - } - - let mut seg = [b'_'; NAME_SEG_SIZE]; - seg[..bytes.len()].copy_from_slice(bytes); - seg -} - -/// Encode an ACPI name path (e.g. "\\_SB_.PCI0") into the buffer. -pub fn encode_name_string(path: &str, buf: &mut Vec) { - let mut chars = path.chars().peekable(); - - if chars.peek() == Some(&'\\') { - buf.push(ROOT_PREFIX); - chars.next(); - } - - while chars.peek() == Some(&'^') { - buf.push(super::opcodes::PARENT_PREFIX); - chars.next(); - } - - let remaining: String = chars.collect(); - if remaining.is_empty() { - return; - } - - let segments: Vec<&str> = remaining.split('.').collect(); - assert!( - segments.len() <= MAX_NAME_SEGS, - "too many name segments: {}", - segments.len() - ); - - match segments.len() { - 0 => {} - 1 => { - let seg = encode_name_seg(segments[0]); - buf.extend_from_slice(&seg); - } - 2 => { - buf.push(DUAL_NAME_PREFIX); - for s in &segments { - let seg = encode_name_seg(s); - buf.extend_from_slice(&seg); - } - } - n => { - buf.push(MULTI_NAME_PREFIX); - buf.push(n as u8); - for s in &segments { - let seg = encode_name_seg(s); - buf.extend_from_slice(&seg); - } - } - } -} - -/// Encode an EISA ID string (e.g. "PNP0A08") into a 32-bit value. -pub fn encode_eisaid(id: &str) -> u32 { - assert_eq!(id.len(), 7, "EISA ID must be exactly 7 characters: {}", id); - - let bytes = id.as_bytes(); - - for (i, &c) in bytes[0..3].iter().enumerate() { - assert!( - c.is_ascii_uppercase(), - "EISA ID manufacturer code must be A-Z at position {}: {}", - i, - id - ); - } - - let c1 = bytes[0] - b'A' + 1; - let c2 = bytes[1] - b'A' + 1; - let c3 = bytes[2] - b'A' + 1; - let mfg = ((c1 as u16) << 10) | ((c2 as u16) << 5) | (c3 as u16); - - let product = - u16::from_str_radix(&id[3..7], 16).expect("invalid hex in EISA ID"); - - let mfg_bytes = mfg.to_be_bytes(); - let product_bytes = product.to_be_bytes(); - - u32::from_le_bytes([ - mfg_bytes[0], - mfg_bytes[1], - product_bytes[0], - product_bytes[1], - ]) -} - -/// EISA ID wrapper that implements AmlWriter. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct EisaId(pub u32); - -impl EisaId { - pub fn from_str(id: &str) -> Self { - Self(encode_eisaid(id)) - } -} - -/// UUID byte size. -pub const UUID_SIZE: usize = 16; - -/// Encode a UUID string into ACPI ToUUID format at compile time. -/// -/// UUID format: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" -/// -/// ACPI ToUUID uses mixed-endian encoding: -/// - First 3 groups (8-4-4 hex digits): little-endian -/// - Last 2 groups (4-12 hex digits): big-endian -pub const fn encode_uuid(uuid: &str) -> [u8; UUID_SIZE] { - let b = uuid.as_bytes(); - assert!(b.len() == 36, "UUID must be 36 characters"); - assert!( - b[8] == b'-' && b[13] == b'-' && b[18] == b'-' && b[23] == b'-', - "UUID must have dashes at positions 8, 13, 18, 23" - ); - - const fn hex(c: u8) -> u8 { - match c { - b'0'..=b'9' => c - b'0', - b'A'..=b'F' => c - b'A' + 10, - b'a'..=b'f' => c - b'a' + 10, - _ => panic!("invalid hex"), - } - } - - const fn byte(b: &[u8], i: usize) -> u8 { - (hex(b[i]) << 4) | hex(b[i + 1]) - } - - [ - byte(b, 6), - byte(b, 4), - byte(b, 2), - byte(b, 0), - byte(b, 11), - byte(b, 9), - byte(b, 16), - byte(b, 14), - byte(b, 19), - byte(b, 21), - byte(b, 24), - byte(b, 26), - byte(b, 28), - byte(b, 30), - byte(b, 32), - byte(b, 34), - ] -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn name_seg_encoding() { - assert_eq!(encode_name_seg("_SB_"), *b"_SB_"); - assert_eq!(encode_name_seg("PCI0"), *b"PCI0"); - assert_eq!(encode_name_seg("A"), *b"A___"); - assert_eq!(encode_name_seg("AB"), *b"AB__"); - } - - #[test] - #[should_panic(expected = "name segment too long")] - fn name_seg_rejects_long() { - encode_name_seg("TOOLONG"); - } - - #[test] - #[should_panic(expected = "invalid first character")] - fn name_seg_rejects_leading_digit() { - encode_name_seg("1BAD"); - } - - #[test] - fn name_string_encoding() { - let mut buf = Vec::new(); - encode_name_string("_SB_", &mut buf); - assert_eq!(buf, b"_SB_"); - - buf.clear(); - encode_name_string("\\_SB_", &mut buf); - assert_eq!(buf, vec![ROOT_PREFIX, b'_', b'S', b'B', b'_']); - - buf.clear(); - encode_name_string("_SB_.PCI0", &mut buf); - assert_eq!(buf[0], DUAL_NAME_PREFIX); - - buf.clear(); - encode_name_string("_SB_.PCI0.ISA_", &mut buf); - assert_eq!(buf[0], MULTI_NAME_PREFIX); - assert_eq!(buf[1], 3); - } - - #[test] - fn eisaid_encoding() { - assert_eq!(encode_eisaid("PNP0A08"), 0x080AD041); - assert_eq!(encode_eisaid("PNP0A03"), 0x030AD041); - assert_eq!(encode_eisaid("PNP0501"), 0x0105D041); - assert_eq!(EisaId::from_str("PNP0A08").0, 0x080AD041); - } - - #[test] - #[should_panic(expected = "EISA ID manufacturer code must be A-Z")] - fn eisaid_rejects_lowercase() { - encode_eisaid("pnp0A08"); - } - - #[test] - fn uuid_encoding() { - let uuid = encode_uuid("33DB4D5B-1FF7-401C-9657-7441C03DD766"); - assert_eq!( - uuid, - [ - 0x5B, 0x4D, 0xDB, 0x33, 0xF7, 0x1F, 0x1C, 0x40, 0x96, 0x57, - 0x74, 0x41, 0xC0, 0x3D, 0xD7, 0x66, - ] - ); - } -} diff --git a/lib/propolis/src/firmware/acpi/opcodes.rs b/lib/propolis/src/firmware/acpi/opcodes.rs deleted file mode 100644 index 2c791c2c4..000000000 --- a/lib/propolis/src/firmware/acpi/opcodes.rs +++ /dev/null @@ -1,117 +0,0 @@ -// 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/. - -//! AML opcode constants. -//! -//! See ACPI spec section 20: - -// Namespace modifier objects -pub const SCOPE_OP: u8 = 0x10; -pub const NAME_OP: u8 = 0x08; - -// Named objects (require EXT_OP_PREFIX) -pub const EXT_OP_PREFIX: u8 = 0x5B; -pub const DEVICE_OP: u8 = 0x82; // ExtOp -pub const PROCESSOR_OP: u8 = 0x83; // ExtOp -pub const POWER_RES_OP: u8 = 0x84; // ExtOp -pub const THERMAL_ZONE_OP: u8 = 0x85; // ExtOp -pub const FIELD_OP: u8 = 0x81; // ExtOp -pub const OP_REGION_OP: u8 = 0x80; // ExtOp - -// Method -pub const METHOD_OP: u8 = 0x14; - -// Data objects -pub const ZERO_OP: u8 = 0x00; -pub const ONE_OP: u8 = 0x01; -pub const ONES_OP: u8 = 0xFF; -pub const BYTE_PREFIX: u8 = 0x0A; -pub const WORD_PREFIX: u8 = 0x0B; -pub const DWORD_PREFIX: u8 = 0x0C; -pub const QWORD_PREFIX: u8 = 0x0E; -pub const STRING_PREFIX: u8 = 0x0D; -pub const BUFFER_OP: u8 = 0x11; -pub const PACKAGE_OP: u8 = 0x12; -pub const VAR_PACKAGE_OP: u8 = 0x13; - -// Name prefixes -pub const DUAL_NAME_PREFIX: u8 = 0x2E; -pub const MULTI_NAME_PREFIX: u8 = 0x2F; -pub const ROOT_PREFIX: u8 = 0x5C; // '\' -pub const PARENT_PREFIX: u8 = 0x5E; // '^' -pub const NULL_NAME: u8 = 0x00; - -// Local and argument references -pub const LOCAL0_OP: u8 = 0x60; -pub const LOCAL1_OP: u8 = 0x61; -pub const LOCAL2_OP: u8 = 0x62; -pub const LOCAL3_OP: u8 = 0x63; -pub const LOCAL4_OP: u8 = 0x64; -pub const LOCAL5_OP: u8 = 0x65; -pub const LOCAL6_OP: u8 = 0x66; -pub const LOCAL7_OP: u8 = 0x67; -pub const ARG0_OP: u8 = 0x68; -pub const ARG1_OP: u8 = 0x69; -pub const ARG2_OP: u8 = 0x6A; -pub const ARG3_OP: u8 = 0x6B; -pub const ARG4_OP: u8 = 0x6C; -pub const ARG5_OP: u8 = 0x6D; -pub const ARG6_OP: u8 = 0x6E; - -// Control flow -pub const IF_OP: u8 = 0xA0; -pub const ELSE_OP: u8 = 0xA1; -pub const WHILE_OP: u8 = 0xA2; -pub const RETURN_OP: u8 = 0xA4; -pub const BREAK_OP: u8 = 0xA5; -pub const CONTINUE_OP: u8 = 0x9F; - -// Logical operators -pub const LAND_OP: u8 = 0x90; -pub const LOR_OP: u8 = 0x91; -pub const LNOT_OP: u8 = 0x92; -pub const LEQUAL_OP: u8 = 0x93; -pub const LGREATER_OP: u8 = 0x94; -pub const LLESS_OP: u8 = 0x95; - -// Arithmetic operators -pub const ADD_OP: u8 = 0x72; -pub const SUBTRACT_OP: u8 = 0x74; -pub const MULTIPLY_OP: u8 = 0x77; -pub const DIVIDE_OP: u8 = 0x78; -pub const AND_OP: u8 = 0x7B; -pub const OR_OP: u8 = 0x7D; -pub const XOR_OP: u8 = 0x7F; -pub const NOT_OP: u8 = 0x80; -pub const SHIFT_LEFT_OP: u8 = 0x79; -pub const SHIFT_RIGHT_OP: u8 = 0x7A; - -// Miscellaneous -pub const STORE_OP: u8 = 0x70; -pub const NOTIFY_OP: u8 = 0x86; -pub const SIZEOF_OP: u8 = 0x87; -pub const INDEX_OP: u8 = 0x88; -pub const DEREF_OF_OP: u8 = 0x83; -pub const REF_OF_OP: u8 = 0x71; -pub const CREATE_DWORD_FIELD_OP: u8 = 0x8A; - -// Resource template end tag -pub const END_TAG: u8 = 0x79; - -// Operation region address space types -pub const SYSTEM_MEMORY: u8 = 0x00; -pub const SYSTEM_IO: u8 = 0x01; -pub const PCI_CONFIG: u8 = 0x02; -pub const EMBEDDED_CONTROL: u8 = 0x03; -pub const SMBUS: u8 = 0x04; -pub const CMOS: u8 = 0x05; -pub const PCI_BAR_TARGET: u8 = 0x06; - -// Field access types -pub const ACCESS_ANY: u8 = 0x00; -pub const ACCESS_BYTE: u8 = 0x01; -pub const ACCESS_WORD: u8 = 0x02; -pub const ACCESS_DWORD: u8 = 0x03; -pub const ACCESS_QWORD: u8 = 0x04; -pub const ACCESS_BUFFER: u8 = 0x05; diff --git a/lib/propolis/src/firmware/acpi/resources.rs b/lib/propolis/src/firmware/acpi/resources.rs deleted file mode 100644 index 86004663b..000000000 --- a/lib/propolis/src/firmware/acpi/resources.rs +++ /dev/null @@ -1,359 +0,0 @@ -// 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 resource template encoding. -//! -//! See ACPI Specification 6.4, Section 6.4 "Resource Data Types for ACPI": -//! - -use super::aml::AmlWriter; - -// Table 6.27 "Small Resource Items" -// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#small-resource-data-type -const SMALL_IRQ_TAG: u8 = 0x04; -const SMALL_IO_TAG: u8 = 0x08; -const SMALL_END_TAG: u8 = 0x0F; - -// Table 6.40 "Large Resource Items" -// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#large-resource-data-type -const LARGE_RESOURCE_BIT: u8 = 0x80; -const LARGE_MEMORY32_FIXED: u8 = 0x06; -const LARGE_DWORD_ADDR_SPACE: u8 = 0x07; -const LARGE_WORD_ADDR_SPACE: u8 = 0x08; -const LARGE_EXT_IRQ: u8 = 0x09; -const LARGE_QWORD_ADDR_SPACE: u8 = 0x0A; - -// Table 6.48 "QWord Address Space Descriptor" -// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#qword-address-space-descriptor -const ADDR_SPACE_TYPE_MEMORY: u8 = 0x00; -const ADDR_SPACE_TYPE_IO: u8 = 0x01; -const ADDR_SPACE_TYPE_BUS: u8 = 0x02; - -// Table 6.49 "Memory Resource Flag (Resource Type = 0) Definitions" -// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#qword-address-space-descriptor -const MEM_FLAG_READ_WRITE: u8 = 1 << 0; -const MEM_FLAG_CACHEABLE: u8 = 1 << 1; -const MEM_FLAG_WRITE_COMBINING: u8 = 1 << 2; - -// Table 6.56 "Extended Interrupt Descriptor Definition" -// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#extended-interrupt-descriptor -const EXT_IRQ_FLAG_CONSUMER: u8 = 1 << 0; -const EXT_IRQ_FLAG_EDGE: u8 = 1 << 1; -const EXT_IRQ_FLAG_ACTIVE_LOW: u8 = 1 << 2; -const EXT_IRQ_FLAG_SHARED: u8 = 1 << 3; - -// Table 6.33 "I/O Port Descriptor Definition" -// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#i-o-port-descriptor -const IO_DECODE_16BIT: u8 = 1 << 0; - -fn mem_type_flags(cacheable: bool, read_write: bool) -> u8 { - let mut f = 0u8; - if cacheable { - f |= MEM_FLAG_CACHEABLE | MEM_FLAG_WRITE_COMBINING; - } - if read_write { - f |= MEM_FLAG_READ_WRITE; - } - f -} - -// Table 6.47 "General Flags" -// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#qword-address-space-descriptor -const ADDR_SPACE_FLAG_MIF: u8 = 1 << 2; -const ADDR_SPACE_FLAG_MAF: u8 = 1 << 3; - -// Table 6.50 "I/O Resource Flag (Resource Type = 1) Definitions" -// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#qword-address-space-descriptor -const IO_RANGE_ENTIRE: u8 = 0x03; - -/// Builder for ACPI resource templates used in _CRS, _PRS and _SRS methods. -#[must_use = "call .finish() to get the resource template bytes"] -pub struct ResourceTemplateBuilder { - buf: Vec, -} - -impl ResourceTemplateBuilder { - pub fn new() -> Self { - Self { buf: Vec::new() } - } - - pub fn qword_memory( - &mut self, - cacheable: bool, - read_write: bool, - min: u64, - max: u64, - translation: u64, - len: u64, - ) -> &mut Self { - self.qword_address_space( - ADDR_SPACE_TYPE_MEMORY, - cacheable, - read_write, - min, - max, - translation, - len, - ) - } - - #[allow(clippy::too_many_arguments)] - fn qword_address_space( - &mut self, - resource_type: u8, - cacheable: bool, - read_write: bool, - min: u64, - max: u64, - translation: u64, - len: u64, - ) -> &mut Self { - // 3 bytes of header + 5 u64 fields - let data_len = (3 + 5 * std::mem::size_of::()) as u16; - - self.buf.push(LARGE_RESOURCE_BIT | LARGE_QWORD_ADDR_SPACE); - self.buf.extend_from_slice(&data_len.to_le_bytes()); - - self.buf.push(resource_type); - self.buf.push(0x00); // General flags - - let type_flags = if resource_type == ADDR_SPACE_TYPE_MEMORY { - mem_type_flags(cacheable, read_write) - } else { - 0x00 - }; - self.buf.push(type_flags); - - self.buf.extend_from_slice(&0u64.to_le_bytes()); // Granularity - self.buf.extend_from_slice(&min.to_le_bytes()); - self.buf.extend_from_slice(&max.to_le_bytes()); - self.buf.extend_from_slice(&translation.to_le_bytes()); - self.buf.extend_from_slice(&len.to_le_bytes()); - - self - } - - pub fn word_bus_number( - &mut self, - min: u16, - max: u16, - translation: u16, - len: u16, - ) -> &mut Self { - // 3 bytes of header + 5 u16 fields - let data_len = (3 + 5 * std::mem::size_of::()) as u16; - - self.buf.push(LARGE_RESOURCE_BIT | LARGE_WORD_ADDR_SPACE); - self.buf.extend_from_slice(&data_len.to_le_bytes()); - - self.buf.push(ADDR_SPACE_TYPE_BUS); - self.buf.push(0x00); // General flags - self.buf.push(0x00); // Type specific flags - - self.buf.extend_from_slice(&0u16.to_le_bytes()); // Granularity - self.buf.extend_from_slice(&min.to_le_bytes()); - self.buf.extend_from_slice(&max.to_le_bytes()); - self.buf.extend_from_slice(&translation.to_le_bytes()); - self.buf.extend_from_slice(&len.to_le_bytes()); - - self - } - - pub fn dword_memory( - &mut self, - cacheable: bool, - read_write: bool, - min: u32, - max: u32, - translation: u32, - len: u32, - ) -> &mut Self { - // 3 bytes of header + 5 u32 fields - let data_len = (3 + 5 * std::mem::size_of::()) as u16; - - self.buf.push(LARGE_RESOURCE_BIT | LARGE_DWORD_ADDR_SPACE); - self.buf.extend_from_slice(&data_len.to_le_bytes()); - - self.buf.push(ADDR_SPACE_TYPE_MEMORY); - self.buf.push(0x00); // General flags - self.buf.push(mem_type_flags(cacheable, read_write)); - - self.buf.extend_from_slice(&0u32.to_le_bytes()); // Granularity - self.buf.extend_from_slice(&min.to_le_bytes()); - self.buf.extend_from_slice(&max.to_le_bytes()); - self.buf.extend_from_slice(&translation.to_le_bytes()); - self.buf.extend_from_slice(&len.to_le_bytes()); - - self - } - - pub fn io(&mut self, min: u16, max: u16, align: u8, len: u8) -> &mut Self { - // info(1) + min(2) + max(2) + align(1) + len(1) - let data_len = 1 + 2 * std::mem::size_of::() + 2; - - self.buf.push((SMALL_IO_TAG << 3) | data_len as u8); - self.buf.push(IO_DECODE_16BIT); - self.buf.extend_from_slice(&min.to_le_bytes()); - self.buf.extend_from_slice(&max.to_le_bytes()); - self.buf.push(align); - self.buf.push(len); - self - } - - pub fn io_range(&mut self, min: u16, max: u16, len: u16) -> &mut Self { - // 3 bytes of header + 5 u16 fields - let data_len = (3 + 5 * std::mem::size_of::()) as u16; - - self.buf.push(LARGE_RESOURCE_BIT | LARGE_WORD_ADDR_SPACE); - self.buf.extend_from_slice(&data_len.to_le_bytes()); - - self.buf.push(ADDR_SPACE_TYPE_IO); - self.buf.push(ADDR_SPACE_FLAG_MIF | ADDR_SPACE_FLAG_MAF); - self.buf.push(IO_RANGE_ENTIRE); - - self.buf.extend_from_slice(&0u16.to_le_bytes()); - self.buf.extend_from_slice(&min.to_le_bytes()); - self.buf.extend_from_slice(&max.to_le_bytes()); - self.buf.extend_from_slice(&0u16.to_le_bytes()); - self.buf.extend_from_slice(&len.to_le_bytes()); - - self - } - - pub fn fixed_memory(&mut self, base: u32, len: u32) -> &mut Self { - // info(1) + base(4) + len(4) - let data_len = (1 + 2 * std::mem::size_of::()) as u16; - - self.buf.push(LARGE_RESOURCE_BIT | LARGE_MEMORY32_FIXED); - self.buf.extend_from_slice(&data_len.to_le_bytes()); - self.buf.push(MEM_FLAG_READ_WRITE); - self.buf.extend_from_slice(&base.to_le_bytes()); - self.buf.extend_from_slice(&len.to_le_bytes()); - self - } - - pub fn irq(&mut self, irq_mask: u16) -> &mut Self { - let data_len = std::mem::size_of::(); - - self.buf.push((SMALL_IRQ_TAG << 3) | data_len as u8); - self.buf.extend_from_slice(&irq_mask.to_le_bytes()); - self - } - - pub fn extended_irq( - &mut self, - consumer: bool, - edge_triggered: bool, - active_low: bool, - shared: bool, - irqs: &[u32], - ) -> &mut Self { - let data_len = 2 + (irqs.len() * 4); - - self.buf.push(LARGE_RESOURCE_BIT | LARGE_EXT_IRQ); - self.buf.extend_from_slice(&(data_len as u16).to_le_bytes()); - - let mut flags = 0u8; - if consumer { - flags |= EXT_IRQ_FLAG_CONSUMER; - } - if edge_triggered { - flags |= EXT_IRQ_FLAG_EDGE; - } - if active_low { - flags |= EXT_IRQ_FLAG_ACTIVE_LOW; - } - if shared { - flags |= EXT_IRQ_FLAG_SHARED; - } - self.buf.push(flags); - self.buf.push(irqs.len() as u8); - - for &irq in irqs { - self.buf.extend_from_slice(&irq.to_le_bytes()); - } - - self - } - - pub fn finish(mut self) -> Vec { - self.buf.push((SMALL_END_TAG << 3) | 1); // 1 byte checksum - self.buf.push(0x00); - self.buf - } -} - -impl AmlWriter for ResourceTemplateBuilder { - fn write_aml(&self, buf: &mut Vec) { - let mut data = Vec::with_capacity(self.buf.len() + 2); - data.extend_from_slice(&self.buf); - data.push((SMALL_END_TAG << 3) | 1); - data.push(0x00); - data.as_slice().write_aml(buf); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn small_descriptors() { - let io_data_len = 1 + 2 * std::mem::size_of::() + 2; - let irq_data_len = std::mem::size_of::(); - - let mut builder = ResourceTemplateBuilder::new(); - builder.io(0x3F8, 0x3F8, 1, 8); - let data = builder.finish(); - assert_eq!(data[0], (SMALL_IO_TAG << 3) | io_data_len as u8); - assert_eq!(data[1], IO_DECODE_16BIT); - - let mut builder = ResourceTemplateBuilder::new(); - builder.irq(0x0010); - let data = builder.finish(); - assert_eq!(data[0], (SMALL_IRQ_TAG << 3) | irq_data_len as u8); - } - - #[test] - fn large_descriptors() { - let mut builder = ResourceTemplateBuilder::new(); - builder.word_bus_number(0, 255, 0, 256); - let data = builder.finish(); - assert_eq!(data[0], LARGE_RESOURCE_BIT | LARGE_WORD_ADDR_SPACE); - assert_eq!(data[3], ADDR_SPACE_TYPE_BUS); - - let mut builder = ResourceTemplateBuilder::new(); - builder.qword_memory( - false, - true, - 0xE000_0000, - 0xEFFF_FFFF, - 0, - 0x1000_0000, - ); - let data = builder.finish(); - assert_eq!(data[0], LARGE_RESOURCE_BIT | LARGE_QWORD_ADDR_SPACE); - assert_eq!(data[3], ADDR_SPACE_TYPE_MEMORY); - } - - #[test] - fn chained_resources() { - let mut builder = ResourceTemplateBuilder::new(); - builder - .word_bus_number(0, 255, 0, 256) - .io(0xCF8, 0xCFF, 1, 8) - .qword_memory( - false, - true, - 0xE000_0000, - 0xEFFF_FFFF, - 0, - 0x1000_0000, - ); - let data = builder.finish(); - - let len = data.len(); - assert_eq!(data[len - 2], (SMALL_END_TAG << 3) | 1); - } -} diff --git a/lib/propolis/src/firmware/acpi/rsdp.rs b/lib/propolis/src/firmware/acpi/rsdp.rs new file mode 100644 index 000000000..8fb5703c0 --- /dev/null +++ b/lib/propolis/src/firmware/acpi/rsdp.rs @@ -0,0 +1,49 @@ +// 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. +/// +/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#root-system-description-pointer-rsdp +pub struct Rsdp { + xsdt_addr: u64, +} + +impl Rsdp { + pub fn new(xsdt_addr: u64) -> Self { + Self { xsdt_addr } + } +} + +impl Aml for Rsdp { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + rsdp::Rsdp::new(*OEM_ID, self.xsdt_addr).to_aml_bytes(sink); + } +} diff --git a/lib/propolis/src/firmware/acpi/tables.rs b/lib/propolis/src/firmware/acpi/tables.rs deleted file mode 100644 index d845967cd..000000000 --- a/lib/propolis/src/firmware/acpi/tables.rs +++ /dev/null @@ -1,563 +0,0 @@ -// 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 builders. -//! -//! This module provides builders for generating ACPI tables that are used -//! to describe the system configuration to guest firmware. - -use std::mem::size_of; - -use zerocopy::{Immutable, IntoBytes}; - -pub const ACPI_TABLE_HEADER_SIZE: usize = 36; -const ACPI_TABLE_LENGTH_OFFSET: usize = 4; -pub const ACPI_TABLE_CHECKSUM_OFF: usize = 9; - -#[derive(Copy, Clone, IntoBytes, Immutable)] -#[repr(C, packed)] -struct AcpiTableHeader { - signature: [u8; 4], - length: u32, - revision: u8, - checksum: u8, - oem_id: [u8; 6], - oem_table_id: [u8; 8], - oem_revision: u32, - creator_id: [u8; 4], - creator_revision: u32, -} - -impl AcpiTableHeader { - fn new(signature: [u8; 4], revision: u8) -> Self { - Self { - signature, - length: 0, - revision, - checksum: 0, - oem_id: *b"OXIDE\0", - oem_table_id: *b"PROPOLIS", - oem_revision: 1, - creator_id: *b"OXDE", - creator_revision: 1, - } - } -} - -#[must_use = "call .finish() to get the table bytes"] -pub struct Rsdt { - data: Vec, -} - -impl Rsdt { - pub fn new() -> Self { - let header = AcpiTableHeader::new(*b"RSDT", 1); - let mut data = vec![0u8; ACPI_TABLE_HEADER_SIZE]; - data[..ACPI_TABLE_HEADER_SIZE].copy_from_slice(header.as_bytes()); - Self { data } - } - - pub fn add_entry(&mut self) -> u32 { - let offset = self.data.len() as u32; - self.data.extend_from_slice(&[0u8; size_of::()]); - offset - } - - pub fn finish(mut self) -> Vec { - let length = self.data.len() as u32; - self.data[ACPI_TABLE_LENGTH_OFFSET - ..ACPI_TABLE_LENGTH_OFFSET + size_of::()] - .copy_from_slice(&length.to_le_bytes()); - self.data - } -} - -#[must_use = "call .finish() to get the table bytes"] -pub struct Xsdt { - data: Vec, -} - -impl Xsdt { - pub fn new() -> Self { - let header = AcpiTableHeader::new(*b"XSDT", 1); - let mut data = vec![0u8; ACPI_TABLE_HEADER_SIZE]; - data[..ACPI_TABLE_HEADER_SIZE].copy_from_slice(header.as_bytes()); - Self { data } - } - - pub fn add_entry(&mut self) -> u32 { - let offset = self.data.len() as u32; - self.data.extend_from_slice(&[0u8; size_of::()]); - offset - } - - pub fn finish(mut self) -> Vec { - let length = self.data.len() as u32; - self.data[ACPI_TABLE_LENGTH_OFFSET - ..ACPI_TABLE_LENGTH_OFFSET + size_of::()] - .copy_from_slice(&length.to_le_bytes()); - self.data - } -} - -pub const RSDP_SIZE: usize = 36; -pub const RSDP_V1_SIZE: usize = 20; - -const RSDP_SIGNATURE_OFFSET: usize = 0; -const RSDP_SIGNATURE_LEN: usize = 8; -pub const RSDP_CHECKSUM_OFFSET: usize = 8; -const RSDP_OEMID_OFFSET: usize = 9; -const RSDP_OEMID_LEN: usize = 6; -const RSDP_REVISION_OFFSET: usize = 15; -const RSDP_LENGTH_OFFSET: usize = 20; -pub const RSDP_XSDT_ADDR_OFFSET: usize = 24; -pub const RSDP_EXT_CHECKSUM_OFFSET: usize = 32; - -#[must_use = "call .finish() to get the RSDP bytes"] -pub struct Rsdp { - data: Vec, -} - -impl Rsdp { - pub fn new() -> Self { - let mut data = vec![0u8; RSDP_SIZE]; - data[RSDP_SIGNATURE_OFFSET..RSDP_SIGNATURE_OFFSET + RSDP_SIGNATURE_LEN] - .copy_from_slice(b"RSD PTR "); - data[RSDP_OEMID_OFFSET..RSDP_OEMID_OFFSET + RSDP_OEMID_LEN] - .copy_from_slice(b"OXIDE\0"); - data[RSDP_REVISION_OFFSET] = 2; - data[RSDP_LENGTH_OFFSET..RSDP_LENGTH_OFFSET + size_of::()] - .copy_from_slice(&(RSDP_SIZE as u32).to_le_bytes()); - Self { data } - } - - pub fn finish(self) -> Vec { - self.data - } -} - -pub struct Dsdt { - data: Vec, -} - -impl Dsdt { - pub fn new() -> Self { - let header = AcpiTableHeader::new(*b"DSDT", 2); - let mut data = vec![0u8; ACPI_TABLE_HEADER_SIZE]; - data[..ACPI_TABLE_HEADER_SIZE].copy_from_slice(header.as_bytes()); - Self { data } - } - - pub fn append_aml(&mut self, aml: &[u8]) { - self.data.extend_from_slice(aml); - } - - pub fn finish(mut self) -> Vec { - let length = self.data.len() as u32; - self.data[ACPI_TABLE_LENGTH_OFFSET - ..ACPI_TABLE_LENGTH_OFFSET + size_of::()] - .copy_from_slice(&length.to_le_bytes()); - self.data - } -} - -pub const FADT_SIZE: usize = 276; -pub const FADT_REVISION: u8 = 6; -pub const FADT_MINOR_REVISION: u8 = 5; - -const FADT_FLAG_WBINVD: u32 = 1 << 0; -const FADT_FLAG_C1_SUPPORTED: u32 = 1 << 2; -const FADT_FLAG_SLP_BUTTON: u32 = 1 << 5; -const FADT_FLAG_TMR_VAL_EXT: u32 = 1 << 8; -const FADT_FLAG_RESET_REG_SUP: u32 = 1 << 10; -const FADT_FLAG_APIC_PHYSICAL: u32 = 1 << 19; -pub const FADT_FLAG_HW_REDUCED_ACPI: u32 = 1 << 20; - -pub const FADT_OFF_FACS32: usize = 36; -pub const FADT_OFF_DSDT32: usize = 40; -pub const FADT_OFF_DSDT64: usize = 140; -const FADT_OFF_SCI_INT: usize = 46; -const FADT_OFF_PM1A_EVT_BLK: usize = 56; -const FADT_OFF_PM1A_CNT_BLK: usize = 64; -const FADT_OFF_PM_TMR_BLK: usize = 76; -const FADT_OFF_PM1_EVT_LEN: usize = 88; -const FADT_OFF_PM1_CNT_LEN: usize = 89; -const FADT_OFF_PM_TMR_LEN: usize = 91; -const FADT_OFF_IAPC_BOOT_ARCH: usize = 109; -const FADT_OFF_FLAGS: usize = 112; -const FADT_OFF_RESET_REG: usize = 116; -const FADT_OFF_RESET_VALUE: usize = 128; -const FADT_OFF_MINOR_REV: usize = 131; -const FADT_OFF_X_PM1A_EVT_BLK: usize = 148; -const FADT_OFF_X_PM1A_CNT_BLK: usize = 172; -const FADT_OFF_X_PM_TMR_BLK: usize = 208; -const FADT_OFF_HYPERVISOR_ID: usize = 268; - -const GAS_OFF_SPACE_ID: usize = 0; -const GAS_OFF_BIT_WIDTH: usize = 1; -const GAS_OFF_ACCESS_WIDTH: usize = 3; -const GAS_OFF_ADDRESS: usize = 4; -const GAS_ADDRESS_LEN: usize = 8; -const GAS_SPACE_SYSTEM_IO: u8 = 1; -const GAS_ACCESS_BYTE: u8 = 1; -const GAS_ACCESS_WORD: u8 = 2; -const GAS_ACCESS_DWORD: u8 = 3; - -const ACPI_RESET_REG_PORT: u64 = 0xcf9; -const ACPI_RESET_VALUE: u8 = 0x06; - -const IAPC_BOOT_ARCH_LEGACY_DEVICES: u16 = 1 << 0; -const IAPC_BOOT_ARCH_8042: u16 = 1 << 1; - -const PIIX4_PM_BASE: u32 = 0xb000; -const PIIX4_PM1A_CNT_OFF: u32 = 4; -const PIIX4_PM_TMR_OFF: u32 = 8; -const PIIX4_PM1_EVT_LEN: u8 = 4; -const PIIX4_PM1_CNT_LEN: u8 = 2; -const PIIX4_PM_TMR_LEN: u8 = 4; -const PIIX4_SCI_IRQ: u16 = 9; - -const HYPERVISOR_ID: &[u8] = b"OXIDE"; - -pub struct Fadt { - data: Vec, -} - -impl Fadt { - pub fn new() -> Self { - let header = AcpiTableHeader::new(*b"FACP", FADT_REVISION); - let mut data = vec![0u8; FADT_SIZE]; - data[..ACPI_TABLE_HEADER_SIZE].copy_from_slice(header.as_bytes()); - data[ACPI_TABLE_LENGTH_OFFSET - ..ACPI_TABLE_LENGTH_OFFSET + size_of::()] - .copy_from_slice(&(FADT_SIZE as u32).to_le_bytes()); - - data[FADT_OFF_SCI_INT..FADT_OFF_SCI_INT + size_of::()] - .copy_from_slice(&PIIX4_SCI_IRQ.to_le_bytes()); - - data[FADT_OFF_PM1A_EVT_BLK..FADT_OFF_PM1A_EVT_BLK + size_of::()] - .copy_from_slice(&PIIX4_PM_BASE.to_le_bytes()); - data[FADT_OFF_PM1A_CNT_BLK..FADT_OFF_PM1A_CNT_BLK + size_of::()] - .copy_from_slice(&(PIIX4_PM_BASE + PIIX4_PM1A_CNT_OFF).to_le_bytes()); - data[FADT_OFF_PM_TMR_BLK..FADT_OFF_PM_TMR_BLK + size_of::()] - .copy_from_slice(&(PIIX4_PM_BASE + PIIX4_PM_TMR_OFF).to_le_bytes()); - - data[FADT_OFF_PM1_EVT_LEN] = PIIX4_PM1_EVT_LEN; - data[FADT_OFF_PM1_CNT_LEN] = PIIX4_PM1_CNT_LEN; - data[FADT_OFF_PM_TMR_LEN] = PIIX4_PM_TMR_LEN; - - let boot_arch = IAPC_BOOT_ARCH_LEGACY_DEVICES | IAPC_BOOT_ARCH_8042; - data[FADT_OFF_IAPC_BOOT_ARCH - ..FADT_OFF_IAPC_BOOT_ARCH + size_of::()] - .copy_from_slice(&boot_arch.to_le_bytes()); - - let flags = FADT_FLAG_WBINVD - | FADT_FLAG_C1_SUPPORTED - | FADT_FLAG_SLP_BUTTON - | FADT_FLAG_TMR_VAL_EXT - | FADT_FLAG_RESET_REG_SUP - | FADT_FLAG_APIC_PHYSICAL; - data[FADT_OFF_FLAGS..FADT_OFF_FLAGS + size_of::()] - .copy_from_slice(&flags.to_le_bytes()); - - data[FADT_OFF_RESET_REG + GAS_OFF_SPACE_ID] = GAS_SPACE_SYSTEM_IO; - data[FADT_OFF_RESET_REG + GAS_OFF_BIT_WIDTH] = u8::BITS as u8; - data[FADT_OFF_RESET_REG + GAS_OFF_ACCESS_WIDTH] = GAS_ACCESS_BYTE; - data[FADT_OFF_RESET_REG + GAS_OFF_ADDRESS - ..FADT_OFF_RESET_REG + GAS_OFF_ADDRESS + GAS_ADDRESS_LEN] - .copy_from_slice(&ACPI_RESET_REG_PORT.to_le_bytes()); - data[FADT_OFF_RESET_VALUE] = ACPI_RESET_VALUE; - - data[FADT_OFF_MINOR_REV] = FADT_MINOR_REVISION; - - data[FADT_OFF_X_PM1A_EVT_BLK + GAS_OFF_SPACE_ID] = GAS_SPACE_SYSTEM_IO; - data[FADT_OFF_X_PM1A_EVT_BLK + GAS_OFF_BIT_WIDTH] = PIIX4_PM1_EVT_LEN * 8; - data[FADT_OFF_X_PM1A_EVT_BLK + GAS_OFF_ACCESS_WIDTH] = GAS_ACCESS_DWORD; - data[FADT_OFF_X_PM1A_EVT_BLK + GAS_OFF_ADDRESS - ..FADT_OFF_X_PM1A_EVT_BLK + GAS_OFF_ADDRESS + GAS_ADDRESS_LEN] - .copy_from_slice(&(PIIX4_PM_BASE as u64).to_le_bytes()); - - data[FADT_OFF_X_PM1A_CNT_BLK + GAS_OFF_SPACE_ID] = GAS_SPACE_SYSTEM_IO; - data[FADT_OFF_X_PM1A_CNT_BLK + GAS_OFF_BIT_WIDTH] = PIIX4_PM1_CNT_LEN * 8; - data[FADT_OFF_X_PM1A_CNT_BLK + GAS_OFF_ACCESS_WIDTH] = GAS_ACCESS_WORD; - data[FADT_OFF_X_PM1A_CNT_BLK + GAS_OFF_ADDRESS - ..FADT_OFF_X_PM1A_CNT_BLK + GAS_OFF_ADDRESS + GAS_ADDRESS_LEN] - .copy_from_slice( - &((PIIX4_PM_BASE + PIIX4_PM1A_CNT_OFF) as u64).to_le_bytes(), - ); - - data[FADT_OFF_X_PM_TMR_BLK + GAS_OFF_SPACE_ID] = GAS_SPACE_SYSTEM_IO; - data[FADT_OFF_X_PM_TMR_BLK + GAS_OFF_BIT_WIDTH] = PIIX4_PM_TMR_LEN * 8; - data[FADT_OFF_X_PM_TMR_BLK + GAS_OFF_ACCESS_WIDTH] = GAS_ACCESS_DWORD; - data[FADT_OFF_X_PM_TMR_BLK + GAS_OFF_ADDRESS - ..FADT_OFF_X_PM_TMR_BLK + GAS_OFF_ADDRESS + GAS_ADDRESS_LEN] - .copy_from_slice( - &((PIIX4_PM_BASE + PIIX4_PM_TMR_OFF) as u64).to_le_bytes(), - ); - - data[FADT_OFF_HYPERVISOR_ID..FADT_OFF_HYPERVISOR_ID + HYPERVISOR_ID.len()] - .copy_from_slice(HYPERVISOR_ID); - Self { data } - } - - pub fn new_reduced() -> Self { - let mut fadt = Self::new(); - fadt.data[FADT_OFF_FLAGS..FADT_OFF_FLAGS + size_of::()] - .copy_from_slice(&FADT_FLAG_HW_REDUCED_ACPI.to_le_bytes()); - fadt - } - - pub fn finish(self) -> Vec { - self.data - } -} - -const MADT_LOCAL_APIC_ADDR_OFF: usize = ACPI_TABLE_HEADER_SIZE; -const MADT_FLAGS_OFF: usize = ACPI_TABLE_HEADER_SIZE + size_of::(); -const MADT_ENTRIES_OFF: usize = ACPI_TABLE_HEADER_SIZE + 2 * size_of::(); - -const MADT_TYPE_LOCAL_APIC: u8 = 0; -const MADT_TYPE_IO_APIC: u8 = 1; -const MADT_TYPE_INT_SRC_OVERRIDE: u8 = 2; -const MADT_TYPE_LAPIC_NMI: u8 = 4; - -const MADT_LOCAL_APIC_LEN: u8 = 8; -const MADT_IO_APIC_LEN: u8 = 12; -const MADT_INT_SRC_OVERRIDE_LEN: u8 = 10; -const MADT_LAPIC_NMI_LEN: u8 = 6; - -pub const MADT_FLAG_PCAT_COMPAT: u32 = 1; -pub const MADT_LAPIC_ENABLED: u32 = 1; - -const MADT_INT_POLARITY_ACTIVE_HIGH: u16 = 0x01; -const MADT_INT_POLARITY_ACTIVE_LOW: u16 = 0x03; -const MADT_INT_TRIGGER_EDGE: u16 = 0x04; -const MADT_INT_TRIGGER_LEVEL: u16 = 0x0c; -pub const MADT_INT_LEVEL_ACTIVE_LOW: u16 = - MADT_INT_POLARITY_ACTIVE_LOW | MADT_INT_TRIGGER_LEVEL; -pub const MADT_INT_EDGE_ACTIVE_HIGH: u16 = - MADT_INT_POLARITY_ACTIVE_HIGH | MADT_INT_TRIGGER_EDGE; -pub const MADT_INT_LEVEL_ACTIVE_HIGH: u16 = - MADT_INT_POLARITY_ACTIVE_HIGH | MADT_INT_TRIGGER_LEVEL; - -pub const ISA_BUS: u8 = 0; -pub const ISA_IRQ_TIMER: u8 = 0; -pub const ISA_IRQ_SCI: u8 = 9; -pub const GSI_TIMER: u32 = 2; -pub const GSI_SCI: u32 = 9; - -pub const ACPI_PROCESSOR_ALL: u8 = 0xff; -pub const MADT_INT_FLAGS_DEFAULT: u16 = 0; -pub const LINT1: u8 = 1; - -pub struct Madt { - data: Vec, -} - -impl Madt { - pub fn new(local_apic_addr: u32) -> Self { - let header = AcpiTableHeader::new(*b"APIC", 5); - let mut data = vec![0u8; MADT_ENTRIES_OFF]; - data[..ACPI_TABLE_HEADER_SIZE].copy_from_slice(header.as_bytes()); - data[MADT_LOCAL_APIC_ADDR_OFF - ..MADT_LOCAL_APIC_ADDR_OFF + size_of::()] - .copy_from_slice(&local_apic_addr.to_le_bytes()); - data[MADT_FLAGS_OFF..MADT_FLAGS_OFF + size_of::()] - .copy_from_slice(&MADT_FLAG_PCAT_COMPAT.to_le_bytes()); - Self { data } - } - - pub fn add_local_apic(&mut self, processor_id: u8, apic_id: u8, flags: u32) { - self.data.push(MADT_TYPE_LOCAL_APIC); - self.data.push(MADT_LOCAL_APIC_LEN); - self.data.push(processor_id); - self.data.push(apic_id); - self.data.extend_from_slice(&flags.to_le_bytes()); - } - - pub fn add_io_apic(&mut self, id: u8, addr: u32, gsi_base: u32) { - self.data.push(MADT_TYPE_IO_APIC); - self.data.push(MADT_IO_APIC_LEN); - self.data.push(id); - self.data.push(0); - self.data.extend_from_slice(&addr.to_le_bytes()); - self.data.extend_from_slice(&gsi_base.to_le_bytes()); - } - - pub fn add_int_src_override( - &mut self, - bus: u8, - source: u8, - gsi: u32, - flags: u16, - ) { - self.data.push(MADT_TYPE_INT_SRC_OVERRIDE); - self.data.push(MADT_INT_SRC_OVERRIDE_LEN); - self.data.push(bus); - self.data.push(source); - self.data.extend_from_slice(&gsi.to_le_bytes()); - self.data.extend_from_slice(&flags.to_le_bytes()); - } - - pub fn add_lapic_nmi(&mut self, processor_uid: u8, flags: u16, lint: u8) { - self.data.push(MADT_TYPE_LAPIC_NMI); - self.data.push(MADT_LAPIC_NMI_LEN); - self.data.push(processor_uid); - self.data.extend_from_slice(&flags.to_le_bytes()); - self.data.push(lint); - } - - pub fn finish(mut self) -> Vec { - let length = self.data.len() as u32; - self.data[ACPI_TABLE_LENGTH_OFFSET - ..ACPI_TABLE_LENGTH_OFFSET + size_of::()] - .copy_from_slice(&length.to_le_bytes()); - self.data - } -} - -const MCFG_ENTRIES_OFF: usize = ACPI_TABLE_HEADER_SIZE + 8; - -pub struct Mcfg { - data: Vec, -} - -impl Mcfg { - pub fn new() -> Self { - let header = AcpiTableHeader::new(*b"MCFG", 1); - let mut data = vec![0u8; MCFG_ENTRIES_OFF]; - data[..ACPI_TABLE_HEADER_SIZE].copy_from_slice(header.as_bytes()); - Self { data } - } - - pub fn add_allocation( - &mut self, - base_addr: u64, - segment_group: u16, - start_bus: u8, - end_bus: u8, - ) { - assert!(start_bus <= end_bus); - self.data.extend_from_slice(&base_addr.to_le_bytes()); - self.data.extend_from_slice(&segment_group.to_le_bytes()); - self.data.push(start_bus); - self.data.push(end_bus); - self.data.extend_from_slice(&[0u8; 4]); - } - - pub fn finish(mut self) -> Vec { - let length = self.data.len() as u32; - self.data[ACPI_TABLE_LENGTH_OFFSET - ..ACPI_TABLE_LENGTH_OFFSET + size_of::()] - .copy_from_slice(&length.to_le_bytes()); - self.data - } -} - -const HPET_HW_ID: u32 = 0x8086_0701; -const HPET_BASE_ADDR: u64 = 0xfed0_0000; -const HPET_DATA_SIZE: usize = 20; -const HPET_PAGE_PROTECT4: u8 = 1; - -const HPET_OFF_HW_ID: usize = ACPI_TABLE_HEADER_SIZE; -const HPET_OFF_BASE_ADDR: usize = ACPI_TABLE_HEADER_SIZE + 8; -const HPET_OFF_FLAGS: usize = ACPI_TABLE_HEADER_SIZE + 19; - -#[must_use = "call .finish() to get the HPET table bytes"] -pub struct Hpet { - data: Vec, -} - -impl Hpet { - pub fn new() -> Self { - let header = AcpiTableHeader::new(*b"HPET", 1); - let mut data = vec![0u8; ACPI_TABLE_HEADER_SIZE + HPET_DATA_SIZE]; - data[..ACPI_TABLE_HEADER_SIZE].copy_from_slice(header.as_bytes()); - - data[HPET_OFF_HW_ID..HPET_OFF_HW_ID + size_of::()] - .copy_from_slice(&HPET_HW_ID.to_le_bytes()); - data[HPET_OFF_BASE_ADDR..HPET_OFF_BASE_ADDR + size_of::()] - .copy_from_slice(&HPET_BASE_ADDR.to_le_bytes()); - data[HPET_OFF_FLAGS] = HPET_PAGE_PROTECT4; - - Self { data } - } - - pub fn finish(mut self) -> Vec { - let length = self.data.len() as u32; - self.data[ACPI_TABLE_LENGTH_OFFSET - ..ACPI_TABLE_LENGTH_OFFSET + size_of::()] - .copy_from_slice(&length.to_le_bytes()); - self.data - } -} - -pub const FACS_SIZE: usize = 64; -const FACS_SIGNATURE_OFF: usize = 0; -const FACS_LENGTH_OFF: usize = size_of::(); -const FACS_HW_SIGNATURE_OFF: usize = 2 * size_of::(); -const FACS_VERSION_OFF: usize = 32; - -pub struct Facs { - data: Vec, -} - -impl Facs { - pub fn new() -> Self { - let mut data = vec![0u8; FACS_SIZE]; - data[FACS_SIGNATURE_OFF..FACS_SIGNATURE_OFF + size_of::()] - .copy_from_slice(b"FACS"); - data[FACS_LENGTH_OFF..FACS_LENGTH_OFF + size_of::()] - .copy_from_slice(&(FACS_SIZE as u32).to_le_bytes()); - data[FACS_HW_SIGNATURE_OFF..FACS_HW_SIGNATURE_OFF + size_of::()] - .copy_from_slice(&0u32.to_le_bytes()); - data[FACS_VERSION_OFF] = 2; - Self { data } - } - - pub fn finish(self) -> Vec { - self.data - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn basic() { - let mut xsdt = Xsdt::new(); - xsdt.add_entry(); - let xsdt_data = xsdt.finish(); - assert_eq!(&xsdt_data[0..4], b"XSDT"); - - let rsdp = Rsdp::new(); - let rsdp_data = rsdp.finish(); - assert_eq!(&rsdp_data[0..8], b"RSD PTR "); - - let dsdt = Dsdt::new(); - let dsdt_data = dsdt.finish(); - assert_eq!(&dsdt_data[0..4], b"DSDT"); - - let fadt = Fadt::new(); - let fadt_data = fadt.finish(); - assert_eq!(&fadt_data[0..4], b"FACP"); - - let madt = Madt::new(0xFEE0_0000); - let madt_data = madt.finish(); - assert_eq!(&madt_data[0..4], b"APIC"); - - let mcfg = Mcfg::new(); - let mcfg_data = mcfg.finish(); - assert_eq!(&mcfg_data[0..4], b"MCFG"); - - let hpet = Hpet::new(); - let hpet_data = hpet.finish(); - assert_eq!(&hpet_data[0..4], b"HPET"); - - let facs = Facs::new(); - let facs_data = facs.finish(); - assert_eq!(&facs_data[0..4], b"FACS"); - } -} diff --git a/lib/propolis/src/firmware/acpi/xsdt.rs b/lib/propolis/src/firmware/acpi/xsdt.rs new file mode 100644 index 000000000..332813403 --- /dev/null +++ b/lib/propolis/src/firmware/acpi/xsdt.rs @@ -0,0 +1,36 @@ +// 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. +/// +/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#extended-system-description-table-xsdt +pub struct Xsdt { + entries: Vec, +} + +impl Xsdt { + pub fn new(entries: Vec) -> Self { + Self { entries } + } +} + +impl Aml for Xsdt { + 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/hw/ps2/ctrl.rs b/lib/propolis/src/hw/ps2/ctrl.rs index 8023ceb37..5cbbf33d4 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,9 +607,7 @@ impl Lifecycle for PS2Ctrl { fn migrate(&self) -> Migrator<'_> { Migrator::Single(self) } - fn as_dsdt_generator( - &self, - ) -> Option<&dyn crate::firmware::acpi::DsdtGenerator> { + fn as_dsdt_generator(&self) -> Option<&dyn acpi::DsdtGenerator> { Some(self) } } @@ -1095,25 +1095,42 @@ impl Default for PS2Mouse { } } -impl crate::firmware::acpi::DsdtGenerator for PS2Ctrl { - fn dsdt_scope(&self) -> crate::firmware::acpi::DsdtScope { - crate::firmware::acpi::DsdtScope::SystemBus +impl acpi::DsdtGenerator for PS2Ctrl { + fn dsdt_scope(&self) -> acpi::DsdtScope { + acpi::DsdtScope::Lpc } +} - fn generate_dsdt(&self, scope: &mut crate::firmware::acpi::ScopeGuard<'_>) { - use crate::firmware::acpi::{EisaId, ResourceTemplateBuilder}; - use crate::hw::ibmpc; - - const PS2_KBD_IRQ: u8 = 1; - - let mut kbd = scope.device("KBD_"); - kbd.name("_HID", &EisaId::from_str("PNP0303")); - - let mut crs = ResourceTemplateBuilder::new(); - crs.io(ibmpc::PORT_PS2_DATA, ibmpc::PORT_PS2_DATA, 1, 1); - crs.io(ibmpc::PORT_PS2_CMD_STATUS, ibmpc::PORT_PS2_CMD_STATUS, 1, 1); - crs.irq(1u16 << PS2_KBD_IRQ); - kbd.name("_CRS", &crs); +const PS2_KBD_IRQ: u8 = 1; + +impl Aml for PS2Ctrl { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + aml::Device::new( + "PS2K".into(), + vec![ + &aml::Name::new("_HID".into(), &aml::EISAName::new("PNP0303")), + &aml::Name::new("_CID".into(), &aml::EISAName::new("PNP030B")), + &aml::Name::new( + "_CRS".into(), + &aml::ResourceTemplate::new(vec![ + &aml::IO::new( + ibmpc::PORT_PS2_DATA, + ibmpc::PORT_PS2_DATA, + 0x00, + 0x01, + ), + &aml::IO::new( + ibmpc::PORT_PS2_CMD_STATUS, + ibmpc::PORT_PS2_CMD_STATUS, + 0x00, + 0x01, + ), + &aml::IrqNoFlags::new(PS2_KBD_IRQ), + ]), + ), + ], + ) + .to_aml_bytes(sink); } } diff --git a/lib/propolis/src/hw/qemu/fwcfg.rs b/lib/propolis/src/hw/qemu/fwcfg.rs index ac3c1fc52..c0249d346 100644 --- a/lib/propolis/src/hw/qemu/fwcfg.rs +++ b/lib/propolis/src/hw/qemu/fwcfg.rs @@ -1056,8 +1056,9 @@ mod test { pub mod formats { use super::Entry; + use crate::firmware::acpi; use crate::hw::pci; - use thiserror::Error; + use acpi_tables::Aml; use zerocopy::{Immutable, IntoBytes}; /// A type for a range described in an E820 map entry. @@ -1304,87 +1305,330 @@ pub mod formats { } } - pub const TABLE_LOADER_FILESZ: usize = 56; - pub const TABLE_LOADER_COMMAND_SIZE: usize = 128; + /// 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], + } - #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoBytes, Immutable)] - #[repr(u8)] - pub enum AllocZone { - High = 0x1, - FSeg = 0x2, + /// A range of address to be used for PCI MMIO. + pub struct PciWindow { + pub base: u64, + pub end: u64, + } + impl PciWindow { + pub fn len(&self) -> u64 { + self.end - self.base + 1 + } } - #[derive(Debug, Error)] - pub enum TableLoaderError { - #[error( - "file name too long: {len} bytes exceeds max of {}", - TABLE_LOADER_FILESZ - 1 - )] - FileNameTooLong { len: usize }, + /// The resulting values to be loaded into QEMU fw_cfg data. + pub struct AcpiTables { + pub tables: Entry, + pub rsdp: Entry, + pub table_loader: Entry, + } - #[error("invalid pointer size: {0} (must be 1, 2, 4, or 8)")] - InvalidPointerSize(u8), + const FW_CFG_ACPI_TABLES_PATH: &str = "etc/acpi/tables"; + const FW_CFG_ACPI_RSDP_PATH: &str = "etc/acpi/rsdp"; - #[error("alignment must be a power of two, got {0}")] - InvalidAlignment(u32), - } + /// 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. + /// + /// ┌─────────┐ ┌─────────┐ ┌─────────────┐ + /// │ 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 https://docs.kernel.org/firmware-guide/acpi/namespace.html + /// + /// 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 { + let mut tables = Self { + config, + tables: Vec::new(), + rsdp: Vec::new(), + loader: TableLoader::new(), + }; - #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoBytes, Immutable)] - #[repr(u32)] - enum CommandType { - Allocate = 1, - AddPointer = 2, - AddChecksum = 3, - #[allow(dead_code)] - WritePointer = 4, - } + tables.build(); + tables + } - #[derive(Clone, IntoBytes, Immutable)] - #[repr(C)] - struct LoaderFileName([u8; TABLE_LOADER_FILESZ]); + pub fn finish(self) -> AcpiTables { + AcpiTables { + tables: Entry::Bytes(self.tables), + rsdp: Entry::Bytes(self.rsdp), + table_loader: self.loader.finish(), + } + } - impl LoaderFileName { - fn new(name: &str) -> Result { - let bytes = name.as_bytes(); - if bytes.len() >= TABLE_LOADER_FILESZ { - return Err(TableLoaderError::FileNameTooLong { - len: bytes.len(), - }); + fn build(&mut self) { + 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(); + + let xsdt_entries = vec![fadt_offset, madt_offset, ssdt_offset]; + let xsdt_offset = self.add_xsdt(xsdt_entries); + + self.add_rsdp(xsdt_offset); + } + + fn add_facs(&mut self) -> usize { + let facs = acpi::Facs::new(); + let facs_offset = self.tables.len(); + facs.to_aml_bytes(&mut self.tables); + + 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); + + 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); + + // Mark the FWDT Operatioon 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); + + // 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); + + 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); + + // 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, + ); } - let mut buf = [0u8; TABLE_LOADER_FILESZ]; - buf[..bytes.len()].copy_from_slice(bytes); - Ok(Self(buf)) + // Recalculate checksum after changes. + self.reset_checksum(xsdt_offset); + + xsdt_offset } - } - #[derive(IntoBytes, Immutable)] - #[repr(C, packed)] - struct AllocateCommand { - file: LoaderFileName, - align: u32, - zone: AllocZone, - } + fn add_rsdp(&mut self, xsdt_offset: usize) { + let rsdp = acpi::Rsdp::new(xsdt_offset as u64); + rsdp.to_aml_bytes(&mut self.rsdp); - #[derive(IntoBytes, Immutable)] - #[repr(C, packed)] - struct AddPointerCommand { - dest_file: LoaderFileName, - src_file: LoaderFileName, - offset: u32, - size: u8, - } + // 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, + ); - #[derive(IntoBytes, Immutable)] - #[repr(C, packed)] - struct AddChecksumCommand { - file: LoaderFileName, - result_offset: u32, - start: u32, - length: u32, + // 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] + .copy_from_slice(&0_u8.to_le_bytes()); + + 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] + .copy_from_slice(&0_u8.to_le_bytes()); + + self.loader.add_checksum( + FW_CFG_ACPI_TABLES_PATH, + checksum_start as u32, + table_offset as u32, + (self.tables.len() - table_offset) as u32, + ); + } } - #[must_use = "call .finish() to get the table-loader entry"] + pub const TABLE_LOADER_FILESZ: usize = 56; + pub 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. + /// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiPlatformDxe/QemuLoader.h pub struct TableLoader { commands: Vec, } @@ -1399,19 +1643,15 @@ pub mod formats { file: &str, align: u32, zone: AllocZone, - ) -> Result<(), TableLoaderError> { - if !align.is_power_of_two() { - return Err(TableLoaderError::InvalidAlignment(align)); - } + ) { + assert!(align.is_power_of_two()); let cmd = AllocateCommand { - file: LoaderFileName::new(file)?, + file: LoaderFileName::new(file), align, zone, }; - self.write_command(CommandType::Allocate, cmd.as_bytes()); - Ok(()) } pub fn add_pointer( @@ -1420,20 +1660,16 @@ pub mod formats { src_file: &str, offset: u32, size: u8, - ) -> Result<(), TableLoaderError> { - if !matches!(size, 1 | 2 | 4 | 8) { - return Err(TableLoaderError::InvalidPointerSize(size)); - } + ) { + assert!(matches!(size, 1 | 2 | 4 | 8)); let cmd = AddPointerCommand { - dest_file: LoaderFileName::new(dest_file)?, - src_file: LoaderFileName::new(src_file)?, + dest_file: LoaderFileName::new(dest_file), + src_file: LoaderFileName::new(src_file), offset, size, }; - self.write_command(CommandType::AddPointer, cmd.as_bytes()); - Ok(()) } pub fn add_checksum( @@ -1442,16 +1678,14 @@ pub mod formats { result_offset: u32, start: u32, length: u32, - ) -> Result<(), TableLoaderError> { + ) { let cmd = AddChecksumCommand { - file: LoaderFileName::new(file)?, + file: LoaderFileName::new(file), result_offset, start, length, }; - self.write_command(CommandType::AddChecksum, cmd.as_bytes()); - Ok(()) } pub fn finish(self) -> Entry { @@ -1472,278 +1706,74 @@ pub mod formats { } } - pub const TABLE_LOADER_FWCFG_NAME: &str = "etc/table-loader"; - pub const ACPI_TABLES_FWCFG_NAME: &str = "etc/acpi/tables"; - pub const ACPI_RSDP_FWCFG_NAME: &str = "etc/acpi/rsdp"; - - pub use crate::firmware::acpi::{Dsdt, Facs, Fadt, Hpet, Madt, Mcfg, Rsdt, Rsdp, Xsdt}; - use crate::firmware::acpi::tables::{ - ACPI_PROCESSOR_ALL, ACPI_TABLE_CHECKSUM_OFF, FADT_OFF_DSDT32, - FADT_OFF_DSDT64, FADT_OFF_FACS32, GSI_SCI, GSI_TIMER, ISA_BUS, - ISA_IRQ_SCI, ISA_IRQ_TIMER, LINT1, MADT_INT_FLAGS_DEFAULT, - MADT_INT_LEVEL_ACTIVE_HIGH, MADT_LAPIC_ENABLED, RSDP_CHECKSUM_OFFSET, - RSDP_EXT_CHECKSUM_OFFSET, RSDP_SIZE, RSDP_V1_SIZE, RSDP_XSDT_ADDR_OFFSET, - }; - - use std::mem::size_of; - - pub struct AcpiConfig { - pub num_cpus: u8, - pub local_apic_addr: u32, - pub io_apic_id: u8, - pub io_apic_addr: u32, - pub pcie_ecam_base: u64, - pub pcie_ecam_size: u64, - pub pcie_bus_start: u8, - pub pcie_bus_end: u8, - pub pcie_mmio32_base: u64, - pub pcie_mmio32_limit: u64, - pub pcie_mmio64_base: u64, - pub pcie_mmio64_limit: u64, - } - - impl Default for AcpiConfig { - fn default() -> Self { - Self { - num_cpus: 1, - local_apic_addr: 0xfee0_0000, - io_apic_id: 0, - io_apic_addr: 0xfec0_0000, - pcie_ecam_base: 0xe000_0000, - pcie_ecam_size: 0x1000_0000, - pcie_bus_start: 0, - pcie_bus_end: 255, - pcie_mmio32_base: 0xc000_0000, - pcie_mmio32_limit: 0xfbff_ffff, - pcie_mmio64_base: 0x1_0000_0000, - pcie_mmio64_limit: 0xf_ffff_ffff, - } - } + #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoBytes, Immutable)] + #[repr(u8)] + pub enum AllocZone { + High = 0x1, + FSeg = 0x2, } - pub struct AcpiTables { - pub tables: Vec, - pub rsdp: Vec, - pub loader: Vec, - } - - pub fn build_acpi_tables( - config: &AcpiConfig, - generators: &[&dyn crate::firmware::acpi::DsdtGenerator], - ) -> AcpiTables { - use crate::firmware::acpi::{build_dsdt_aml, DsdtConfig, PcieConfig}; - - let mut tables = Vec::new(); - let mut loader = TableLoader::new(); - - let dsdt_config = DsdtConfig { - pcie: Some(PcieConfig { - ecam_base: config.pcie_ecam_base, - ecam_size: config.pcie_ecam_size, - bus_start: config.pcie_bus_start, - bus_end: config.pcie_bus_end, - mmio32_base: config.pcie_mmio32_base, - mmio32_limit: config.pcie_mmio32_limit, - mmio64_base: config.pcie_mmio64_base, - mmio64_limit: config.pcie_mmio64_limit, - }), - }; - let aml = build_dsdt_aml(&dsdt_config, generators); - let mut dsdt = Dsdt::new(); - dsdt.append_aml(&aml); - let dsdt_offset = tables.len(); - tables.extend_from_slice(&dsdt.finish()); - - let fadt = Fadt::new(); - let fadt_offset = tables.len(); - tables.extend_from_slice(&fadt.finish()); - - let mut madt = Madt::new(config.local_apic_addr); - for i in 0..config.num_cpus { - madt.add_local_apic(i, i, MADT_LAPIC_ENABLED); - } - madt.add_io_apic(config.io_apic_id, config.io_apic_addr, 0); - madt.add_int_src_override(ISA_BUS, ISA_IRQ_TIMER, GSI_TIMER, 0); - madt.add_int_src_override( - ISA_BUS, - ISA_IRQ_SCI, - GSI_SCI, - MADT_INT_LEVEL_ACTIVE_HIGH, - ); - madt.add_lapic_nmi(ACPI_PROCESSOR_ALL, MADT_INT_FLAGS_DEFAULT, LINT1); - let madt_offset = tables.len(); - tables.extend_from_slice(&madt.finish()); - - let mut mcfg = Mcfg::new(); - mcfg.add_allocation( - config.pcie_ecam_base, - 0, - config.pcie_bus_start, - config.pcie_bus_end, - ); - let mcfg_offset = tables.len(); - tables.extend_from_slice(&mcfg.finish()); - - let hpet = Hpet::new(); - let hpet_offset = tables.len(); - tables.extend_from_slice(&hpet.finish()); - - let mut xsdt = Xsdt::new(); - let xsdt_fadt_off = xsdt.add_entry(); - let xsdt_madt_off = xsdt.add_entry(); - let xsdt_mcfg_off = xsdt.add_entry(); - let xsdt_hpet_off = xsdt.add_entry(); - let xsdt_offset = tables.len(); - tables.extend_from_slice(&xsdt.finish()); - - let facs = Facs::new(); - let facs_offset = tables.len(); - tables.extend_from_slice(&facs.finish()); - - let rsdp_data = Rsdp::new().finish(); - - tables[fadt_offset + FADT_OFF_FACS32 - ..fadt_offset + FADT_OFF_FACS32 + size_of::()] - .copy_from_slice(&(facs_offset as u32).to_le_bytes()); - - tables[fadt_offset + FADT_OFF_DSDT32 - ..fadt_offset + FADT_OFF_DSDT32 + size_of::()] - .copy_from_slice(&(dsdt_offset as u32).to_le_bytes()); - tables[fadt_offset + FADT_OFF_DSDT64 - ..fadt_offset + FADT_OFF_DSDT64 + size_of::()] - .copy_from_slice(&(dsdt_offset as u64).to_le_bytes()); - - let xsdt_entries = [ - (xsdt_fadt_off, fadt_offset), - (xsdt_madt_off, madt_offset), - (xsdt_mcfg_off, mcfg_offset), - (xsdt_hpet_off, hpet_offset), - ]; - for (entry_off, table_offset) in xsdt_entries { - let off = xsdt_offset + entry_off as usize; - tables[off..off + size_of::()] - .copy_from_slice(&(table_offset as u64).to_le_bytes()); - } - - loader - .add_allocate(ACPI_TABLES_FWCFG_NAME, 64, AllocZone::High) - .unwrap(); - loader.add_allocate(ACPI_RSDP_FWCFG_NAME, 16, AllocZone::FSeg).unwrap(); - - let table_pointers: &[(u32, u8)] = &[ - ( - fadt_offset as u32 + FADT_OFF_FACS32 as u32, - size_of::() as u8, - ), - ( - fadt_offset as u32 + FADT_OFF_DSDT32 as u32, - size_of::() as u8, - ), - ( - fadt_offset as u32 + FADT_OFF_DSDT64 as u32, - size_of::() as u8, - ), - (xsdt_offset as u32 + xsdt_fadt_off, size_of::() as u8), - (xsdt_offset as u32 + xsdt_madt_off, size_of::() as u8), - (xsdt_offset as u32 + xsdt_mcfg_off, size_of::() as u8), - (xsdt_offset as u32 + xsdt_hpet_off, size_of::() as u8), - ]; - for &(offset, size) in table_pointers { - loader - .add_pointer( - ACPI_TABLES_FWCFG_NAME, - ACPI_TABLES_FWCFG_NAME, - offset, - size, - ) - .unwrap(); - } + #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoBytes, Immutable)] + #[repr(u32)] + enum CommandType { + Allocate = 1, + AddPointer = 2, + AddChecksum = 3, + #[allow(dead_code)] + WritePointer = 4, + } - loader - .add_pointer( - ACPI_RSDP_FWCFG_NAME, - ACPI_TABLES_FWCFG_NAME, - RSDP_XSDT_ADDR_OFFSET as u32, - size_of::() as u8, - ) - .unwrap(); + #[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 table_offsets = [ - dsdt_offset, - fadt_offset, - madt_offset, - mcfg_offset, - hpet_offset, - xsdt_offset, - facs_offset, - ]; - for pair in table_offsets.windows(2) { - let (start, end) = (pair[0] as u32, pair[1] as u32); - loader - .add_checksum( - ACPI_TABLES_FWCFG_NAME, - start + ACPI_TABLE_CHECKSUM_OFF as u32, - start, - end - start, - ) - .unwrap(); + let mut buf = [0u8; TABLE_LOADER_FILESZ]; + buf[..bytes.len()].copy_from_slice(bytes); + Self(buf) } + } - let rsdp_checksums = [ - (RSDP_CHECKSUM_OFFSET, RSDP_V1_SIZE), - (RSDP_EXT_CHECKSUM_OFFSET, RSDP_SIZE), - ]; - for (checksum_off, length) in rsdp_checksums { - loader - .add_checksum( - ACPI_RSDP_FWCFG_NAME, - checksum_off as u32, - 0, - length as u32, - ) - .unwrap(); - } + #[derive(IntoBytes, Immutable)] + #[repr(C, packed)] + struct AllocateCommand { + file: LoaderFileName, + align: u32, + zone: AllocZone, + } - let loader_entry = loader.finish(); - let loader_bytes = match loader_entry { - super::Entry::Bytes(b) => b, - _ => unreachable!(), - }; + #[derive(IntoBytes, Immutable)] + #[repr(C, packed)] + struct AddPointerCommand { + dest_file: LoaderFileName, + src_file: LoaderFileName, + offset: u32, + size: u8, + } - AcpiTables { tables, rsdp: rsdp_data, loader: loader_bytes } + #[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 struct_sizes() { - assert_eq!( - std::mem::size_of::(), - TABLE_LOADER_FILESZ - ); - assert_eq!( - std::mem::size_of::(), - TABLE_LOADER_FILESZ + 5 - ); - assert_eq!( - std::mem::size_of::(), - TABLE_LOADER_FILESZ * 2 + 5 - ); - assert_eq!( - std::mem::size_of::(), - TABLE_LOADER_FILESZ + 12 - ); - } - #[test] fn basic() { let mut loader = TableLoader::new(); - loader.add_allocate("rsdp", 16, AllocZone::FSeg).unwrap(); - loader.add_allocate("tables", 64, AllocZone::High).unwrap(); - loader.add_pointer("rsdp", "tables", 16, 4).unwrap(); - loader.add_checksum("rsdp", 8, 0, 20).unwrap(); + 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"); @@ -1755,43 +1785,5 @@ pub mod formats { assert_eq!(bytes[256], CommandType::AddPointer as u8); assert_eq!(bytes[384], CommandType::AddChecksum as u8); } - - #[test] - fn validation() { - let mut loader = TableLoader::new(); - - let long_name = "a".repeat(TABLE_LOADER_FILESZ); - assert!(matches!( - loader.add_allocate(&long_name, 64, AllocZone::High), - Err(TableLoaderError::FileNameTooLong { .. }) - )); - - assert!(matches!( - loader.add_allocate("test", 3, AllocZone::High), - Err(TableLoaderError::InvalidAlignment(3)) - )); - - assert!(matches!( - loader.add_pointer("a", "b", 0, 3), - Err(TableLoaderError::InvalidPointerSize(3)) - )); - } - } - - #[cfg(test)] - mod test_acpi_tables { - use super::*; - - #[test] - fn basic() { - let mut xsdt = Xsdt::new(); - xsdt.add_entry(); - let xsdt_data = xsdt.finish(); - assert_eq!(&xsdt_data[0..4], b"XSDT"); - - let rsdp = Rsdp::new(); - let rsdp_data = rsdp.finish(); - assert_eq!(&rsdp_data[0..8], b"RSD PTR "); - } } } diff --git a/lib/propolis/src/hw/qemu/pvpanic.rs b/lib/propolis/src/hw/qemu/pvpanic.rs index 3bd61359e..093d658e4 100644 --- a/lib/propolis/src/hw/qemu/pvpanic.rs +++ b/lib/propolis/src/hw/qemu/pvpanic.rs @@ -108,29 +108,4 @@ impl Lifecycle for QemuPvpanic { fn type_name(&self) -> &'static str { DEVICE_NAME } - - fn as_dsdt_generator( - &self, - ) -> Option<&dyn crate::firmware::acpi::DsdtGenerator> { - Some(self) - } -} - -impl crate::firmware::acpi::DsdtGenerator for QemuPvpanic { - fn dsdt_scope(&self) -> crate::firmware::acpi::DsdtScope { - crate::firmware::acpi::DsdtScope::SystemBus - } - - fn generate_dsdt(&self, scope: &mut crate::firmware::acpi::ScopeGuard<'_>) { - use crate::firmware::acpi::ResourceTemplateBuilder; - - let mut dev = scope.device("PEVT"); - dev.name("_HID", &"QEMU0001"); - - let mut crs = ResourceTemplateBuilder::new(); - crs.io(Self::IOPORT, Self::IOPORT, 1, 1); - dev.name("_CRS", &crs); - - dev.name("_STA", &0x0Fu32); - } } diff --git a/lib/propolis/src/hw/uart/lpc.rs b/lib/propolis/src/hw/uart/lpc.rs index 9bb17f27e..514bd82be 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,41 +39,43 @@ impl UartState { } pub struct LpcUart { + name: &'static str, + irq: u8, state: Mutex, + port: Mutex>, notify_readable: NotifierCell, notify_writable: NotifierCell, - io_base: u16, - irq: u8, - name: &'static str, } impl LpcUart { pub fn new( - irq_pin: Box, - io_base: u16, - irq: u8, 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(), - io_base, - irq, - name, }) } - pub fn attach(self: &Arc, bus: &PioBus) { + pub fn attach(self: &Arc, bus: &PioBus, port: u16) { let this = self.clone(); let piofn = Arc::new(move |_port: u16, rwo: RWOp| this.pio_rw(rwo)) as Arc; - bus.register(self.io_base, REGISTER_LEN as u16, piofn).unwrap(); + 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); @@ -185,9 +190,7 @@ impl Lifecycle for LpcUart { state.paused = false; } - fn as_dsdt_generator( - &self, - ) -> Option<&dyn crate::firmware::acpi::DsdtGenerator> { + fn as_dsdt_generator(&self) -> Option<&dyn acpi::DsdtGenerator> { Some(self) } } @@ -213,29 +216,44 @@ impl MigrateSingle for LpcUart { } } -impl crate::firmware::acpi::DsdtGenerator for LpcUart { - fn dsdt_scope(&self) -> crate::firmware::acpi::DsdtScope { - crate::firmware::acpi::DsdtScope::SystemBus +impl acpi::DsdtGenerator for LpcUart { + fn dsdt_scope(&self) -> acpi::DsdtScope { + acpi::DsdtScope::Lpc } +} - fn generate_dsdt(&self, scope: &mut crate::firmware::acpi::ScopeGuard<'_>) { - use crate::firmware::acpi::{EisaId, ResourceTemplateBuilder}; +impl Aml for LpcUart { + fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { + let port = match *self.port.lock().unwrap() { + Some(p) => p, + None => return, // Device is not attached to any port. + }; let uid: u32 = match self.name { - "COM1" => 0, - "COM2" => 1, - "COM3" => 2, - "COM4" => 3, - _ => 0, + "COM1" => 1, + "COM2" => 2, + "COM3" | "COM4" | _ => { + // XXX(acpi): COM3 and COM4 are also attached to the instance + // but the original EDK2 static tables didn't include them. + return; + } }; - let mut dev = scope.device(self.name); - dev.name("_HID", &EisaId::from_str("PNP0501")); - dev.name("_UID", &uid); - - let mut crs = ResourceTemplateBuilder::new(); - crs.io(self.io_base, self.io_base, 1, REGISTER_LEN as u8); - crs.irq(1u16 << self.irq); - dev.name("_CRS", &crs); + aml::Device::new( + aml::Path::new(&format!("UAR{}", uid)), + vec![ + &aml::Name::new("_HID".into(), &aml::EISAName::new("PNP0501")), + &aml::Name::new("_DDN".into(), &self.name), + &aml::Name::new("_UID".into(), &uid), + &aml::Name::new( + "_CRS".into(), + &aml::ResourceTemplate::new(vec![ + &aml::IO::new(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 e4356b8f7..da8adeb30 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. @@ -97,13 +98,14 @@ pub trait Lifecycle: Send + Sync + 'static { Migrator::Empty } - /// Returns this device as a [`DsdtGenerator`] if it contributes to DSDT. + /// 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 crate::firmware::acpi::DsdtGenerator> { + fn as_dsdt_generator(&self) -> Option<&dyn acpi::DsdtGenerator> { None } } diff --git a/phd-tests/framework/src/test_vm/config.rs b/phd-tests/framework/src/test_vm/config.rs index e855bd8d7..3be06080c 100644 --- a/phd-tests/framework/src/test_vm/config.rs +++ b/phd-tests/framework/src/test_vm/config.rs @@ -56,7 +56,6 @@ pub struct VmConfig<'dr> { disks: Vec>, migration_failure: Option, guest_hv_interface: Option, - native_acpi_tables: Option, } impl<'dr> VmConfig<'dr> { @@ -77,7 +76,6 @@ impl<'dr> VmConfig<'dr> { disks: Vec::new(), migration_failure: None, guest_hv_interface: None, - native_acpi_tables: Some(true), }; config.boot_disk( @@ -153,11 +151,6 @@ impl<'dr> VmConfig<'dr> { self } - pub fn native_acpi_tables(&mut self, enabled: Option) -> &mut Self { - self.native_acpi_tables = enabled; - self - } - /// Add a new disk to the VM config, and add it to the front of the VM's /// boot order. /// @@ -228,7 +221,6 @@ impl<'dr> VmConfig<'dr> { disks, migration_failure, guest_hv_interface, - native_acpi_tables, } = self; let bootrom_path = framework @@ -310,7 +302,6 @@ impl<'dr> VmConfig<'dr> { .as_ref() .cloned() .unwrap_or_default(), - native_acpi_tables: *native_acpi_tables, }, components: Default::default(), smbios: None, diff --git a/phd-tests/tests/src/acpi.rs b/phd-tests/tests/src/acpi.rs deleted file mode 100644 index e0894da76..000000000 --- a/phd-tests/tests/src/acpi.rs +++ /dev/null @@ -1,118 +0,0 @@ -// 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_framework::{artifacts, lifecycle::Action}; -use phd_testcase::*; -use propolis_client::instance_spec::InstanceSpecStatus; - -#[phd_testcase] -async fn native_acpi_tables_in_spec(ctx: &Framework) { - let mut vm = ctx.spawn_default_vm("native_acpi_tables_in_spec").await?; - vm.launch().await?; - vm.wait_to_boot().await?; - - let InstanceSpecStatus::Present(spec) = vm.get_spec().await?.spec else { - panic!("instance should have a spec"); - }; - - assert_eq!(spec.board.native_acpi_tables, Some(true)); -} - -#[phd_testcase] -async fn native_tables_preserved_on_migration(ctx: &Framework) { - let mut source = - ctx.spawn_default_vm("native_tables_migration_source").await?; - - source.launch().await?; - source.wait_to_boot().await?; - - ctx.lifecycle_test( - source, - &[ - Action::MigrateToPropolis(artifacts::DEFAULT_PROPOLIS_ARTIFACT), - Action::MigrateToPropolis(artifacts::DEFAULT_PROPOLIS_ARTIFACT), - ], - |vm| { - Box::pin(async { - let InstanceSpecStatus::Present(spec) = - vm.get_spec().await.unwrap().spec - else { - panic!("should have a spec"); - }; - assert_eq!(spec.board.native_acpi_tables, Some(true)); - }) - }, - ) - .await?; -} - -#[phd_testcase] -async fn ovmf_tables_preserved_on_migration(ctx: &Framework) { - let mut cfg = ctx.vm_config_builder("ovmf_tables_migration_source"); - cfg.native_acpi_tables(Some(false)); - - let mut source = ctx.spawn_vm(&cfg, None).await?; - - source.launch().await?; - source.wait_to_boot().await?; - - ctx.lifecycle_test( - source, - &[ - Action::MigrateToPropolis(artifacts::DEFAULT_PROPOLIS_ARTIFACT), - Action::MigrateToPropolis(artifacts::DEFAULT_PROPOLIS_ARTIFACT), - ], - |vm| { - Box::pin(async { - let InstanceSpecStatus::Present(spec) = - vm.get_spec().await.unwrap().spec - else { - panic!("should have a spec"); - }; - assert_eq!(spec.board.native_acpi_tables, Some(false)); - }) - }, - ) - .await?; -} - -mod from_base { - use super::*; - - #[phd_testcase] - async fn ovmf_tables_preserved_through_migrations(ctx: &Framework) { - if !ctx.migration_base_enabled() { - phd_skip!("No 'migration base' Propolis revision available"); - } - - let mut env = ctx.environment_builder(); - env.propolis(artifacts::BASE_PROPOLIS_ARTIFACT); - let mut cfg = ctx.vm_config_builder("ovmf_tables_from_base"); - cfg.clear_boot_order(); - cfg.native_acpi_tables(None); - - let mut source = ctx.spawn_vm(&cfg, Some(&env)).await?; - source.launch().await?; - source.wait_to_boot().await?; - - ctx.lifecycle_test( - source, - &[ - Action::MigrateToPropolis(artifacts::DEFAULT_PROPOLIS_ARTIFACT), - Action::MigrateToPropolis(artifacts::DEFAULT_PROPOLIS_ARTIFACT), - ], - |vm| { - Box::pin(async { - let InstanceSpecStatus::Present(spec) = - vm.get_spec().await.unwrap().spec - else { - panic!("should have a spec"); - }; - assert!(spec.board.native_acpi_tables != Some(true)); - }) - }, - ) - .await?; - } -} diff --git a/phd-tests/tests/src/lib.rs b/phd-tests/tests/src/lib.rs index 8e2a559a9..da2437a87 100644 --- a/phd-tests/tests/src/lib.rs +++ b/phd-tests/tests/src/lib.rs @@ -4,7 +4,6 @@ pub use phd_testcase; -mod acpi; mod boot_order; mod cpuid; mod crucible; diff --git a/phd-tests/tests/src/migrate.rs b/phd-tests/tests/src/migrate.rs index 8893fc85e..e75978887 100644 --- a/phd-tests/tests/src/migrate.rs +++ b/phd-tests/tests/src/migrate.rs @@ -100,9 +100,6 @@ mod from_base { // because a newer base Propolis will understand `boot_settings` just // fine. cfg.clear_boot_order(); - // Base Propolis predates native ACPI table support. None ensures the - // field isn't serialized and is preserved through migration round trips. - cfg.native_acpi_tables(None); ctx.spawn_vm(&cfg, Some(&env)).await } } From 13b8439160c646b8f57b6f9830c847ac1fb903cb Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Mon, 20 Apr 2026 17:55:53 +0000 Subject: [PATCH 20/47] docs: minor docs touch-ups --- lib/propolis/src/firmware/acpi/dsdt.rs | 26 +++++++++++++++----------- lib/propolis/src/firmware/acpi/facs.rs | 2 +- lib/propolis/src/firmware/acpi/fadt.rs | 8 +++++--- lib/propolis/src/firmware/acpi/madt.rs | 6 +++--- lib/propolis/src/firmware/acpi/rsdp.rs | 2 +- lib/propolis/src/firmware/acpi/xsdt.rs | 2 +- lib/propolis/src/hw/qemu/fwcfg.rs | 7 +++++-- 7 files changed, 31 insertions(+), 22 deletions(-) diff --git a/lib/propolis/src/firmware/acpi/dsdt.rs b/lib/propolis/src/firmware/acpi/dsdt.rs index 945184609..2f6c87ad1 100644 --- a/lib/propolis/src/firmware/acpi/dsdt.rs +++ b/lib/propolis/src/firmware/acpi/dsdt.rs @@ -65,7 +65,8 @@ impl<'a> Aml for DsdtGeneratorAml<'a> { } /// Values for the PM1a_CNT.SLP_TYP register to enter different sleep states. -/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/07_Power_and_Performance_Mgmt/oem-supplied-system-level-control-methods.html?highlight=_sx#sx-system-states +/// +/// const PM1A_CNT_SLP_TYP_S0: u8 = 5; const PM1A_CNT_SLP_TYP_S3: u8 = 1; const PM1A_CNT_SLP_TYP_S4: u8 = 2; @@ -78,7 +79,7 @@ pub struct DsdtConfig<'a> { /// The DSDT table is part of the fixed ACPI tables and is used to describe /// system resources. /// -/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#differentiated-system-description-table-dsdt +/// pub struct Dsdt<'a> { config: DsdtConfig<'a>, } @@ -185,7 +186,8 @@ impl<'a> Aml for PciRootBridge<'a> { } /// I/O port range for PCI configuration. -/// https://wiki.osdev.org/PCI#Configuration_Space_Access_Mechanism_#1 +/// +/// const PCI_CONFIG_IO_BASE: u16 = 0x0cf8; const PCI_CONFIG_IO_SIZE: u8 = 8; @@ -194,7 +196,8 @@ const PCI_BUS_START: u16 = 0x00; const PCI_BUS_END: u16 = 0xff; /// MMIO address region used for legacy VGA devices. -/// https://en.wikipedia.org/wiki/Video_Graphics_Array#Use +/// +/// const LEGACY_VGA_BASE: u32 = 0x000a_0000; const LEGACY_VGA_LIMIT: u32 = 0x000b_ffff; @@ -442,7 +445,7 @@ const PCI_INT_PINS: u8 = 4; /// _PRT method for the PCI0 device (\_SB.PCI0._PRT) /// -/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html?highlight=_prt#prt-pci-routing-table +/// struct PciRootBridgePrt {} impl Aml for PciRootBridgePrt { @@ -497,7 +500,7 @@ impl Aml for PciRootBridgePrt { /// Low-word of an _ADR value that refers to all PCI functions. /// -/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#adr-object-address-encodings +/// const PCI_ADR_ALL_FUNC: u32 = 0xffff; /// Representation of an entry in the _PRT table. @@ -526,7 +529,7 @@ impl<'a> Aml for PrtEntry<'a> { /// Refer to the original _PRT table from EDK2 for more details on what is being /// defined here. /// -/// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiTables/Dsdt.asl#L299-L303 +/// struct PciRootBridgeLpc<'a> { generators: &'a [&'a dyn DsdtGenerator], } @@ -869,7 +872,7 @@ impl<'a> Lnk<'a> { impl<'a> Lnk<'a> { // AML code for the special SCI link. - // https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiTables/Dsdt.asl#L200-L220 + // fn to_aml_bytes_sci(&self, sink: &mut dyn AmlSink) { aml::Device::new( "LNKS".into(), @@ -969,7 +972,8 @@ impl<'a> Aml for Lnk<'a> { /// Length in bytes of the SSDT header. Used to calculate the offset of other /// fields. -/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html?highlight=ssdt#secondary-system-description-table-fields-ssdt +/// +/// const SSDT_HEADER_LEN: usize = 36; /// Byte offset of the FWDT OperationRegion offset address field in the SSDT @@ -979,7 +983,7 @@ const SSDT_HEADER_LEN: usize = 36; /// OperationRegion prefix (1 byte) + OperationRegion name (4 bytes) + /// OperationRegion space (1 byte) + DWordPrefix (1 byte) /// -/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/20_AML_Specification/AML_Specification.html#defopregion +/// pub const SSDT_FWDT_ADDR_OFFSET: usize = SSDT_HEADER_LEN + 8; /// Number of bytes used to store the offset address value in the FWDT @@ -989,7 +993,7 @@ 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. /// -/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#secondary-system-description-table-ssdt +/// pub struct Ssdt { /// Offset of the area reserved for the FWDT OperationRegion data in the /// overall ACPI tables storage. diff --git a/lib/propolis/src/firmware/acpi/facs.rs b/lib/propolis/src/firmware/acpi/facs.rs index bedc5fcb6..5bf804121 100644 --- a/lib/propolis/src/firmware/acpi/facs.rs +++ b/lib/propolis/src/firmware/acpi/facs.rs @@ -11,7 +11,7 @@ use acpi_tables::{facs, Aml, AmlSink}; /// The FACS table stores information about the firmware. /// -/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#firmware-acpi-control-structure-facs +/// pub struct Facs {} impl Facs { diff --git a/lib/propolis/src/firmware/acpi/fadt.rs b/lib/propolis/src/firmware/acpi/fadt.rs index 750ffa8bb..dcb49c9f1 100644 --- a/lib/propolis/src/firmware/acpi/fadt.rs +++ b/lib/propolis/src/firmware/acpi/fadt.rs @@ -10,7 +10,9 @@ use super::{OEM_ID, OEM_REVISION, OEM_TABLE_ID, SCI_IRQ}; use acpi_tables::{ // XXX(acpi): Use version 3 to keep FADT table consistent with the original - // EKD2 static tables. + // EKD2 static tables. The acpi_tables crate also generates the + // MADT table using revision 1, which fwts reports not being + // compatible with FADT 6.5. fadt_3::{FADTBuilder, Flags}, gas::{AccessSize, AddressSpace, GAS}, Aml, @@ -41,7 +43,7 @@ const GPE0_BLK_LEN: u8 = 4; // Represent 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; @@ -55,7 +57,7 @@ bitflags! { /// The FADT table stores fixed hardware ACPI information. /// -/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#fixed-acpi-description-table-fadt +/// pub struct Fadt { facs_offset: u32, dsdt_offset: u32, diff --git a/lib/propolis/src/firmware/acpi/madt.rs b/lib/propolis/src/firmware/acpi/madt.rs index 0032dc167..6919bfd32 100644 --- a/lib/propolis/src/firmware/acpi/madt.rs +++ b/lib/propolis/src/firmware/acpi/madt.rs @@ -27,7 +27,7 @@ pub struct MadtConfig { /// The MADT/APIC table describes the interrupts for the entire system. /// -/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#multiple-apic-description-table-madt +/// pub struct Madt<'a> { config: &'a MadtConfig, } @@ -44,8 +44,8 @@ impl<'a> Madt<'a> { // - madt: LAPIC has no matching processor UID 1 // - madt: LAPICNMI has no matching processor UID 255 // -// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiTables/Madt.aslc -// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiPlatformDxe/Qemu.c#L58 +// +// impl<'a> Aml for Madt<'a> { fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { let mut table = madt::MADT::new( diff --git a/lib/propolis/src/firmware/acpi/rsdp.rs b/lib/propolis/src/firmware/acpi/rsdp.rs index 8fb5703c0..8eb4d33af 100644 --- a/lib/propolis/src/firmware/acpi/rsdp.rs +++ b/lib/propolis/src/firmware/acpi/rsdp.rs @@ -31,7 +31,7 @@ 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. /// -/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#root-system-description-pointer-rsdp +/// pub struct Rsdp { xsdt_addr: u64, } diff --git a/lib/propolis/src/firmware/acpi/xsdt.rs b/lib/propolis/src/firmware/acpi/xsdt.rs index 332813403..b35b05a91 100644 --- a/lib/propolis/src/firmware/acpi/xsdt.rs +++ b/lib/propolis/src/firmware/acpi/xsdt.rs @@ -16,7 +16,7 @@ pub const XSDT_HEADER_LEN: usize = 36; /// The XSDT table provides the addresses of additional tables. /// -/// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#extended-system-description-table-xsdt +/// pub struct Xsdt { entries: Vec, } diff --git a/lib/propolis/src/hw/qemu/fwcfg.rs b/lib/propolis/src/hw/qemu/fwcfg.rs index c0249d346..006c00523 100644 --- a/lib/propolis/src/hw/qemu/fwcfg.rs +++ b/lib/propolis/src/hw/qemu/fwcfg.rs @@ -1342,6 +1342,7 @@ pub mod formats { /// The ACPI tables are organized in a hierarchy, with some tables having /// fields that hold the address of another table. /// + /// ```text /// ┌─────────┐ ┌─────────┐ ┌─────────────┐ /// │ RSDP │ ┌─▶│ XSDT │ ┌──▶│ FADT │ /// ├─────────┤ │ ├─────────┤ │ ├─────────────┤ ┌──────────┐ @@ -1365,7 +1366,8 @@ pub mod formats { /// │Interrupt Controller│ /// │ Structures │ /// └────────────────────┘ - /// Adapted from https://docs.kernel.org/firmware-guide/acpi/namespace.html + /// ``` + /// Adapted from /// /// These addresses are only know at boot time, so each reference has a /// corresponding [`AddPointerCommand`] that the firmware executes on boot. @@ -1628,7 +1630,8 @@ pub mod formats { /// ACPI tables are loaded. /// /// Refer to the EDK2 source code for more information on the commands. - /// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiPlatformDxe/QemuLoader.h + /// + /// pub struct TableLoader { commands: Vec, } From fee46d5a38d9dc8faa94580921c7e49024011e62 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Mon, 20 Apr 2026 18:24:38 +0000 Subject: [PATCH 21/47] fix clippy --- bin/propolis-server/src/lib/initializer.rs | 7 +++---- lib/propolis/src/firmware/acpi/dsdt.rs | 5 ++--- lib/propolis/src/firmware/acpi/fadt.rs | 12 ++++++------ lib/propolis/src/hw/uart/lpc.rs | 1 + 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/bin/propolis-server/src/lib/initializer.rs b/bin/propolis-server/src/lib/initializer.rs index 9a3d31c8f..738478ba2 100644 --- a/bin/propolis-server/src/lib/initializer.rs +++ b/bin/propolis-server/src/lib/initializer.rs @@ -915,13 +915,12 @@ impl MachineInitializer<'_> { // // NOTE: SoftNpu squats on com4. let uart = LpcUart::new( - chipset.irq_pin(ibmpc::IRQ_COM4).unwrap(), - ibmpc::PORT_COM4, - ibmpc::IRQ_COM4, "COM4", + ibmpc::IRQ_COM4, + chipset.irq_pin(ibmpc::IRQ_COM4).unwrap(), ); uart.set_autodiscard(true); - uart.attach(&self.machine.bus_pio); + uart.attach(&self.machine.bus_pio, ibmpc::PORT_COM4); self.devices .insert(SpecKey::Name("softnpu-uart".to_string()), uart.clone()); diff --git a/lib/propolis/src/firmware/acpi/dsdt.rs b/lib/propolis/src/firmware/acpi/dsdt.rs index 2f6c87ad1..53b00ebf6 100644 --- a/lib/propolis/src/firmware/acpi/dsdt.rs +++ b/lib/propolis/src/firmware/acpi/dsdt.rs @@ -450,8 +450,7 @@ struct PciRootBridgePrt {} impl Aml for PciRootBridgePrt { fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { - let sources = - vec!["^LPC_.LNKA", "^LPC_.LNKB", "^LPC_.LNKC", "^LPC_.LNKD"]; + let sources = ["^LPC_.LNKA", "^LPC_.LNKB", "^LPC_.LNKC", "^LPC_.LNKD"]; let sources_len: u8 = sources.len() as u8; let mut ptr_entries = Vec::new(); @@ -1115,7 +1114,7 @@ impl Aml for Byte { } #[cfg(test)] -mod tests { +mod test { use super::*; struct MockDsdtGenerator { diff --git a/lib/propolis/src/firmware/acpi/fadt.rs b/lib/propolis/src/firmware/acpi/fadt.rs index dcb49c9f1..9ad19c4d2 100644 --- a/lib/propolis/src/firmware/acpi/fadt.rs +++ b/lib/propolis/src/firmware/acpi/fadt.rs @@ -89,18 +89,18 @@ impl Aml for Fadt { fadt.sci_int = (SCI_IRQ as u16).into(); fadt.smi_cmd = 0xb2.into(); - fadt.acpi_enable = 0xf1.into(); - fadt.acpi_disable = 0xf0.into(); + fadt.acpi_enable = 0xf1; + fadt.acpi_disable = 0xf0; fadt.pm1a_evt_blk = PM1A_EVT_BLK_ADDR.into(); fadt.pm1a_cnt_blk = PM1A_CNT_BLK_ADDR.into(); fadt.pm_tmr_blk = PM_TMR_BLK_ADDR.into(); fadt.gpe0_blk = GPE0_BLK_ADDR.into(); - fadt.pm1_evt_len = PM1A_EVT_BLK_LEN.into(); - fadt.pm1_cnt_len = PM1A_CNT_BLK_LEN.into(); - fadt.pm_tmr_len = PM_TMR_BLK_LEN.into(); - fadt.gpe0_blk_len = GPE0_BLK_LEN.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; fadt.p_lvl2_lat = 101.into(); fadt.p_lvl3_lat = 1001.into(); diff --git a/lib/propolis/src/hw/uart/lpc.rs b/lib/propolis/src/hw/uart/lpc.rs index 514bd82be..517d3818f 100644 --- a/lib/propolis/src/hw/uart/lpc.rs +++ b/lib/propolis/src/hw/uart/lpc.rs @@ -229,6 +229,7 @@ impl Aml for LpcUart { None => return, // Device is not attached to any port. }; + #[allow(clippy::wildcard_in_or_patterns)] let uid: u32 = match self.name { "COM1" => 1, "COM2" => 2, From a6b3ae37407d314569752ca063e92f38861a1120 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Tue, 21 Apr 2026 01:58:00 +0000 Subject: [PATCH 22/47] minor fixes --- lib/propolis/src/firmware/acpi/dsdt.rs | 131 +++++++++++++++++++++---- lib/propolis/src/firmware/acpi/fadt.rs | 3 +- lib/propolis/src/firmware/acpi/madt.rs | 2 +- lib/propolis/src/hw/qemu/fwcfg.rs | 10 +- 4 files changed, 117 insertions(+), 29 deletions(-) diff --git a/lib/propolis/src/firmware/acpi/dsdt.rs b/lib/propolis/src/firmware/acpi/dsdt.rs index 53b00ebf6..b64db9b07 100644 --- a/lib/propolis/src/firmware/acpi/dsdt.rs +++ b/lib/propolis/src/firmware/acpi/dsdt.rs @@ -201,6 +201,17 @@ const PCI_BUS_END: u16 = 0xff; 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 @@ -351,17 +362,17 @@ impl Aml for PciRootBridgeCrs { &aml::CreateDWordField::new( &aml::Path::new("PS32"), &aml::Path::new("CRES"), - &(mmio32_offset + 0x0a), // Byte offset for min. + &(mmio32_offset + ADDRESS_SPACE_32_MIN_OFFSET), ), &aml::CreateDWordField::new( &aml::Path::new("PE32"), &aml::Path::new("CRES"), - &(mmio32_offset + 0x0e), // Byte offset for max. + &(mmio32_offset + ADDRESS_SPACE_32_MAX_OFFSET), ), &aml::CreateDWordField::new( &aml::Path::new("PL32"), &aml::Path::new("CRES"), - &(mmio32_offset + 0x16), // Byte offset for len. + &(mmio32_offset + ADDRESS_SPACE_32_LEN_OFFSET), ), // Update the values of mmio32 based on the FWDT. &aml::Store::new( @@ -390,17 +401,17 @@ impl Aml for PciRootBridgeCrs { &aml::CreateQWordField::new( &aml::Path::new("PS64"), &aml::Path::new("CR64"), - &(mmio64_offset + 0x0e), + &(mmio64_offset + ADDRESS_SPACE_64_MIN_OFFSET), ), &aml::CreateQWordField::new( &aml::Path::new("PE64"), &aml::Path::new("CR64"), - &(mmio64_offset + 0x16), + &(mmio64_offset + ADDRESS_SPACE_64_MAX_OFFSET), ), &aml::CreateQWordField::new( &aml::Path::new("PL64"), &aml::Path::new("CR64"), - &(mmio64_offset + 0x26), + &(mmio64_offset + ADDRESS_SPACE_64_LEN_OFFSET), ), // Update the values of mmio64 based on the FWDT. &aml::Store::new( @@ -431,10 +442,10 @@ impl Aml for PciRootBridgeCrs { fn aml_len(vec: &[&dyn Aml]) -> usize { let mut sink = Vec::new(); - vec.iter().fold(0, |_acc, aml| { + for aml in vec { aml.to_aml_bytes(&mut sink); - sink.len() - }) + } + sink.len() } /// Number of devices in the PCI0 root bridge. @@ -596,7 +607,7 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { &aml::CreateDWordField::new( &aml::Path::new("IRQW"), &aml::Path::new("BUF0"), - &0x05_u64, // TODO: document. + &INTERRUPT_LIST_OFFSET, ), &aml::If::new( &aml::LogicalNot::new(&aml::And::new( @@ -1140,35 +1151,32 @@ mod test { &MockDsdtGenerator { scope: DsdtScope::Lpc }, ]; - // Filter by SystemBus. let mut got = Vec::new(); + let mut expected = Vec::new(); + + // Filter by SystemBus. DsdtGeneratorAml::new(&generators, DsdtScope::SystemBus) .to_aml_bytes(&mut got); - - let mut expected = Vec::new(); aml::Name::new("TEST".into(), &"SystemBus").to_aml_bytes(&mut expected); assert_eq!(expected, got); + got.clear(); + expected.clear(); // Filter by PciRoot. - let mut got = Vec::new(); DsdtGeneratorAml::new(&generators, DsdtScope::PciRoot) .to_aml_bytes(&mut got); - - let mut expected = Vec::new(); aml::Name::new("TEST".into(), &"PciRoot").to_aml_bytes(&mut expected); assert_eq!(expected, got); + got.clear(); + expected.clear(); // Filter by Lpc. - let mut got = Vec::new(); DsdtGeneratorAml::new(&generators, DsdtScope::Lpc) .to_aml_bytes(&mut got); - - let mut expected = Vec::new(); aml::Name::new("TEST".into(), &"Lpc").to_aml_bytes(&mut expected); assert_eq!(expected, got); - got.clear(); } #[test] @@ -1190,4 +1198,87 @@ mod test { 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/fadt.rs b/lib/propolis/src/firmware/acpi/fadt.rs index 9ad19c4d2..eb54de675 100644 --- a/lib/propolis/src/firmware/acpi/fadt.rs +++ b/lib/propolis/src/firmware/acpi/fadt.rs @@ -10,7 +10,7 @@ use super::{OEM_ID, OEM_REVISION, OEM_TABLE_ID, SCI_IRQ}; use acpi_tables::{ // XXX(acpi): Use version 3 to keep FADT table consistent with the original - // EKD2 static tables. The acpi_tables crate also generates the + // EDK2 static tables. The acpi_tables crate also generates the // MADT table using revision 1, which fwts reports not being // compatible with FADT 6.5. fadt_3::{FADTBuilder, Flags}, @@ -102,6 +102,7 @@ impl Aml for Fadt { fadt.pm_tmr_len = PM_TMR_BLK_LEN; fadt.gpe0_blk_len = GPE0_BLK_LEN; + // Disable C2 and C3 state support. fadt.p_lvl2_lat = 101.into(); fadt.p_lvl3_lat = 1001.into(); diff --git a/lib/propolis/src/firmware/acpi/madt.rs b/lib/propolis/src/firmware/acpi/madt.rs index 6919bfd32..40fb69303 100644 --- a/lib/propolis/src/firmware/acpi/madt.rs +++ b/lib/propolis/src/firmware/acpi/madt.rs @@ -57,7 +57,7 @@ impl<'a> Aml for Madt<'a> { .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#i-o-apic-structure + // 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, diff --git a/lib/propolis/src/hw/qemu/fwcfg.rs b/lib/propolis/src/hw/qemu/fwcfg.rs index 006c00523..14d854a2c 100644 --- a/lib/propolis/src/hw/qemu/fwcfg.rs +++ b/lib/propolis/src/hw/qemu/fwcfg.rs @@ -1465,7 +1465,7 @@ pub mod formats { let ssdt_offset = self.tables.len(); ssdt.to_aml_bytes(&mut self.tables); - // Mark the FWDT Operatioon offset field as a pointer to the + // Mark the FWDT OperationRegion offset field as a pointer to the // FWDT data. self.loader.add_pointer( FW_CFG_ACPI_TABLES_PATH, @@ -1586,9 +1586,7 @@ pub mod formats { checksum_offset + acpi::TABLE_HEADER_CHECKSUM_LEN; // Zero existing checksum so it doesn't affect the new value. - self.rsdp[checksum_offset..checksum_end] - .copy_from_slice(&0_u8.to_le_bytes()); - + self.rsdp[checksum_offset..checksum_end].copy_from_slice(&[0_u8]); self.loader.add_checksum( FW_CFG_ACPI_RSDP_PATH, checksum_offset as u32, @@ -1611,9 +1609,7 @@ pub mod formats { + acpi::TABLE_HEADER_CHECKSUM_LEN; // Zero existing checksum so it doesn't affect the new value. - self.tables[checksum_start..checksum_end] - .copy_from_slice(&0_u8.to_le_bytes()); - + self.tables[checksum_start..checksum_end].copy_from_slice(&[0u8]); self.loader.add_checksum( FW_CFG_ACPI_TABLES_PATH, checksum_start as u32, From b7132f210f7311225d65c6a0267ecf9100695059 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Tue, 21 Apr 2026 19:00:42 +0000 Subject: [PATCH 23/47] tests: add phd-test for ACPI tables --- Cargo.lock | 1 + phd-tests/tests/Cargo.toml | 1 + phd-tests/tests/src/firmware.rs | 64 +++++++++++++++++++++++++++++++++ phd-tests/tests/src/lib.rs | 1 + 4 files changed, 67 insertions(+) create mode 100644 phd-tests/tests/src/firmware.rs diff --git a/Cargo.lock b/Cargo.lock index 185008917..46ea6f7e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4910,6 +4910,7 @@ dependencies = [ name = "phd-tests" version = "0.1.0" dependencies = [ + "acpi_tables", "anyhow", "backoff", "byteorder", diff --git a/phd-tests/tests/Cargo.toml b/phd-tests/tests/Cargo.toml index 3b0dc3ba9..c9ab223e1 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 diff --git a/phd-tests/tests/src/firmware.rs b/phd-tests/tests/src/firmware.rs new file mode 100644 index 000000000..4176c580b --- /dev/null +++ b/phd-tests/tests/src/firmware.rs @@ -0,0 +1,64 @@ +// 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 acpi_tables; +use phd_testcase::*; +use tracing::info; + +#[phd_testcase] +async fn acpi_tables_generation(ctx: &Framework) { + let mut vm = ctx + .spawn_vm(&ctx.vm_config_builder("lspci_lifecycle_test"), 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"; + + // Verify ACPI tables have the expected Creator ID value. + let creator_id_hex = acpi_tables::CREATOR_ID + .iter() + .map(|i| format!("{:02x?}", i)) + .collect::(); + + struct CreatorIdTestCase<'a> { + table: &'a str, + offset: u8, + len: u8, + } + + let creator_id_test_cases = [ + CreatorIdTestCase { table: "APIC", offset: 28, len: 4 }, + CreatorIdTestCase { table: "DSDT", offset: 28, len: 4 }, + CreatorIdTestCase { table: "FACP", offset: 28, len: 4 }, + CreatorIdTestCase { table: "SSDT", offset: 28, len: 4 }, + ]; + for case in creator_id_test_cases.iter() { + let cmd = format!( + "xxd -s {1} -l {2} -c {2} -p {3}/{0}", + case.table, case.offset, case.len, ACPI_TABLES_PATH, + ); + let out = vm.run_shell_command(&cmd).await?; + info!(out, "{} creator ID", case.table); + + assert!(out.contains(&creator_id_hex)); + } + + // 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?; + info!(facs_version, "FACS table version"); + assert_eq!(facs_version, "01"); +} diff --git a/phd-tests/tests/src/lib.rs b/phd-tests/tests/src/lib.rs index da2437a87..193e1915f 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; From 5fed2fc8be06c92f544d7e68956c19ef8db97f50 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Tue, 21 Apr 2026 21:49:36 +0000 Subject: [PATCH 24/47] fix clippy --- phd-tests/tests/src/firmware.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/phd-tests/tests/src/firmware.rs b/phd-tests/tests/src/firmware.rs index 4176c580b..3d0e989f4 100644 --- a/phd-tests/tests/src/firmware.rs +++ b/phd-tests/tests/src/firmware.rs @@ -2,12 +2,11 @@ // 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 acpi_tables; use phd_testcase::*; use tracing::info; #[phd_testcase] -async fn acpi_tables_generation(ctx: &Framework) { +async fn acpi_tables_generation(ctx: &TestCtx) { let mut vm = ctx .spawn_vm(&ctx.vm_config_builder("lspci_lifecycle_test"), None) .await?; @@ -60,5 +59,6 @@ async fn acpi_tables_generation(ctx: &Framework) { ); let facs_version = vm.run_shell_command(&facs_version_cmd).await?; info!(facs_version, "FACS table version"); + assert_eq!(facs_version, "01"); } From e9f7fb13ecf6bdab32b0a56298cb105e50d7b173 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Wed, 22 Apr 2026 01:07:05 +0000 Subject: [PATCH 25/47] fwcg: handle invalid configuration --- bin/propolis-server/src/lib/initializer.rs | 18 +++++--- bin/propolis-standalone/src/main.rs | 13 +++--- lib/propolis/src/hw/qemu/fwcfg.rs | 50 +++++++++++++++------- 3 files changed, 53 insertions(+), 28 deletions(-) diff --git a/bin/propolis-server/src/lib/initializer.rs b/bin/propolis-server/src/lib/initializer.rs index d5131cc19..41377f4d7 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), @@ -1446,23 +1449,24 @@ impl MachineInitializer<'_> { .filter_map(|dev| dev.as_dsdt_generator()) .collect(); + 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: fwcfg::formats::PciWindow { - base: lowmem as u64, - end: PCI_MMIO32_END as u64, - }, + pci_window_32, // XXX(acpi): Value inherited from the original EDK2 static tables, // where the 64-bit PCI MMIO region was never set. It // should match the actual memory regions registered in // the instance. // https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiPlatformDxe/Qemu.c#L284-L286 - pci_window_64: fwcfg::formats::PciWindow { base: 0, end: 0 }, + pci_window_64: fwcfg::formats::PciWindow::empty(), dsdt_generators: &generators, }; let acpi_tables = fwcfg::formats::AcpiTablesBuilder::new(config); - - Ok(acpi_tables.finish()) + Ok(acpi_tables.build()) } /// Initialize qemu `fw_cfg` device, and populate it with data including CPU diff --git a/bin/propolis-standalone/src/main.rs b/bin/propolis-standalone/src/main.rs index e0def5f5a..22263b37a 100644 --- a/bin/propolis-standalone/src/main.rs +++ b/bin/propolis-standalone/src/main.rs @@ -1088,23 +1088,24 @@ fn generate_acpi_tables( .filter_map(|dev| dev.as_dsdt_generator()) .collect(); + 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: fwcfg::formats::PciWindow { - base: lowmem as u64, - end: PCI_MMIO32_END as u64, - }, + pci_window_32, // XXX(acpi): Value inherited from the original EDK2 static tables, // where the 64-bit PCI MMIO region was never set. It // should match the actual memory regions registered in // the instance. // https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiPlatformDxe/Qemu.c#L284-L286 - pci_window_64: fwcfg::formats::PciWindow { base: 0, end: 0 }, + pci_window_64: fwcfg::formats::PciWindow::empty(), dsdt_generators: &generators, }; let acpi_tables = fwcfg::formats::AcpiTablesBuilder::new(config); - Ok(acpi_tables.finish()) + Ok(acpi_tables.build()) } fn setup_instance( diff --git a/lib/propolis/src/hw/qemu/fwcfg.rs b/lib/propolis/src/hw/qemu/fwcfg.rs index 30559a75f..cf05dbc73 100644 --- a/lib/propolis/src/hw/qemu/fwcfg.rs +++ b/lib/propolis/src/hw/qemu/fwcfg.rs @@ -1304,6 +1304,14 @@ pub mod formats { } } + #[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, @@ -1313,12 +1321,29 @@ pub mod formats { } /// A range of address to be used for PCI MMIO. + #[derive(PartialEq)] pub struct PciWindow { - pub base: u64, - pub end: u64, + base: u64, + end: u64, } impl PciWindow { + pub fn new(base: u64, end: u64) -> Result { + 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 } } @@ -1381,26 +1406,15 @@ pub mod formats { impl<'a> AcpiTablesBuilder<'a> { pub fn new(config: &'a AcpiConfig) -> Self { - let mut tables = Self { + Self { config, tables: Vec::new(), rsdp: Vec::new(), loader: TableLoader::new(), - }; - - tables.build(); - tables - } - - pub fn finish(self) -> AcpiTables { - AcpiTables { - tables: Entry::Bytes(self.tables), - rsdp: Entry::Bytes(self.rsdp), - table_loader: self.loader.finish(), } } - fn build(&mut self) { + pub fn build(mut self) -> AcpiTables { self.loader.add_allocate( FW_CFG_ACPI_TABLES_PATH, 64, @@ -1423,6 +1437,12 @@ pub mod formats { 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 { From d2f8d375fa01926a202901daa288824f980b2092 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Wed, 22 Apr 2026 01:55:12 +0000 Subject: [PATCH 26/47] minor fixes and more tests --- lib/propolis/src/firmware/acpi/fadt.rs | 23 +++++++++++++++++++++++ lib/propolis/src/firmware/acpi/rsdp.rs | 16 ++++++++++++++++ lib/propolis/src/hw/qemu/fwcfg.rs | 6 +++--- phd-tests/tests/src/firmware.rs | 2 +- 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/lib/propolis/src/firmware/acpi/fadt.rs b/lib/propolis/src/firmware/acpi/fadt.rs index eb54de675..7e295ddb7 100644 --- a/lib/propolis/src/firmware/acpi/fadt.rs +++ b/lib/propolis/src/firmware/acpi/fadt.rs @@ -152,3 +152,26 @@ impl Aml for Fadt { 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/rsdp.rs b/lib/propolis/src/firmware/acpi/rsdp.rs index 8eb4d33af..080699b09 100644 --- a/lib/propolis/src/firmware/acpi/rsdp.rs +++ b/lib/propolis/src/firmware/acpi/rsdp.rs @@ -47,3 +47,19 @@ impl Aml for Rsdp { 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/hw/qemu/fwcfg.rs b/lib/propolis/src/hw/qemu/fwcfg.rs index cf05dbc73..cfc77521a 100644 --- a/lib/propolis/src/hw/qemu/fwcfg.rs +++ b/lib/propolis/src/hw/qemu/fwcfg.rs @@ -1328,7 +1328,7 @@ pub mod formats { } impl PciWindow { pub fn new(base: u64, end: u64) -> Result { - if base > end { + if base >= end { return Err(AcpiTablesError::InvalidPCIWindowRange(base, end)); } Ok(Self { base, end }) @@ -1638,8 +1638,8 @@ pub mod formats { } } - pub const TABLE_LOADER_FILESZ: usize = 56; - pub const TABLE_LOADER_COMMAND_SIZE: usize = 128; + 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. diff --git a/phd-tests/tests/src/firmware.rs b/phd-tests/tests/src/firmware.rs index 3d0e989f4..7bcb533f7 100644 --- a/phd-tests/tests/src/firmware.rs +++ b/phd-tests/tests/src/firmware.rs @@ -8,7 +8,7 @@ use tracing::info; #[phd_testcase] async fn acpi_tables_generation(ctx: &TestCtx) { let mut vm = ctx - .spawn_vm(&ctx.vm_config_builder("lspci_lifecycle_test"), None) + .spawn_vm(&ctx.vm_config_builder("acpi_tables_generation"), None) .await?; if !vm.guest_os_kind().is_linux() { From e9204ea2bf0f867df256958d1c71169e2b675381 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Thu, 23 Apr 2026 18:10:45 +0000 Subject: [PATCH 27/47] minor fixes and improvements --- bin/propolis-server/src/lib/initializer.rs | 10 +-- bin/propolis-standalone/src/main.rs | 8 +- lib/propolis/src/firmware/acpi/dsdt.rs | 77 ++++++++++-------- lib/propolis/src/firmware/acpi/fadt.rs | 10 ++- lib/propolis/src/firmware/acpi/madt.rs | 4 +- lib/propolis/src/firmware/acpi/rsdp.rs | 4 + lib/propolis/src/firmware/acpi/xsdt.rs | 4 + lib/propolis/src/hw/ps2/ctrl.rs | 4 +- lib/propolis/src/hw/qemu/fwcfg.rs | 5 ++ lib/propolis/src/hw/uart/lpc.rs | 1 - phd-tests/tests/src/firmware.rs | 94 +++++++++++++++++++++- 11 files changed, 167 insertions(+), 54 deletions(-) diff --git a/bin/propolis-server/src/lib/initializer.rs b/bin/propolis-server/src/lib/initializer.rs index 41377f4d7..ef1c181a7 100644 --- a/bin/propolis-server/src/lib/initializer.rs +++ b/bin/propolis-server/src/lib/initializer.rs @@ -415,18 +415,18 @@ impl MachineInitializer<'_> { continue; } - let (irq, port, uart_name) = match desc.num { + let (uart_name, irq, port) = match desc.num { SerialPortNumber::Com1 => { - (ibmpc::IRQ_COM1, ibmpc::PORT_COM1, "COM1") + ("COM1", ibmpc::IRQ_COM1, ibmpc::PORT_COM1) } SerialPortNumber::Com2 => { - (ibmpc::IRQ_COM2, ibmpc::PORT_COM2, "COM2") + ("COM2", ibmpc::IRQ_COM2, ibmpc::PORT_COM2) } SerialPortNumber::Com3 => { - (ibmpc::IRQ_COM3, ibmpc::PORT_COM3, "COM3") + ("COM3", ibmpc::IRQ_COM3, ibmpc::PORT_COM3) } SerialPortNumber::Com4 => { - (ibmpc::IRQ_COM4, ibmpc::PORT_COM4, "COM4") + ("COM4", ibmpc::IRQ_COM4, ibmpc::PORT_COM4) } }; diff --git a/bin/propolis-standalone/src/main.rs b/bin/propolis-standalone/src/main.rs index 22263b37a..d3a2731df 100644 --- a/bin/propolis-standalone/src/main.rs +++ b/bin/propolis-standalone/src/main.rs @@ -1479,16 +1479,16 @@ fn setup_instance( fwcfg.insert_named("etc/e820", e820_entry).unwrap(); let acpi_entries = generate_acpi_tables(cpus, lowmem, &guard.inventory) - .expect("failed to build ACPI tables"); + .expect("can build ACPI tables"); fwcfg .insert_named("etc/acpi/tables", acpi_entries.tables) - .context("failed to insert ACPI tables")?; + .context("Failed to insert ACPI tables")?; fwcfg .insert_named("etc/acpi/rsdp", acpi_entries.rsdp) - .context("failed to insert ACPI RSDP")?; + .context("Failed to insert ACPI RSDP")?; fwcfg .insert_named("etc/table-loader", acpi_entries.table_loader) - .context("failed to insert ACPI table-loader")?; + .context("Failed to insert ACPI table-loader")?; fwcfg.attach(pio, &machine.acc_mem); diff --git a/lib/propolis/src/firmware/acpi/dsdt.rs b/lib/propolis/src/firmware/acpi/dsdt.rs index b64db9b07..2d1e65e5d 100644 --- a/lib/propolis/src/firmware/acpi/dsdt.rs +++ b/lib/propolis/src/firmware/acpi/dsdt.rs @@ -275,8 +275,8 @@ impl Aml for PciRootBridgeCrs { let mmio32 = aml::AddressSpace::new_memory( aml::AddressSpaceCacheable::NotCacheable, true, - 0xf800_0000_u32, - 0xfffb_ffff_u32, + 0xf800_0000_u32, // Value overwritten by _CRS. + 0xfffb_ffff_u32, // Value overwritten by _CRS. None, ); cres.push(&mmio32); @@ -299,8 +299,8 @@ impl Aml for PciRootBridgeCrs { let mmio64 = aml::AddressSpace::new_memory( aml::AddressSpaceCacheable::Cacheable, true, - 0x0080_0000_0000_u64, - 0x0fff_ffff_ffff_u64, + 0x0080_0000_0000_u64, // Value overwritten by _CRS. + 0x0fff_ffff_ffff_u64, // Value overwritten by _CRS. None, ); cr64.push(&mmio64); @@ -313,7 +313,7 @@ impl Aml for PciRootBridgeCrs { // This method returns a ResourceTemplate describing the PCI root // bridge resources. // - // It read the FWDT OperationRegion that is declared in the SSDT. This + // 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. @@ -322,8 +322,8 @@ impl Aml for PciRootBridgeCrs { 0, true, vec![ + // Create references to values in the FWDT OperationRegion. &aml::Field::new( - // Create references to values in the FWDT OperationRegion. "FWDT".into(), aml::FieldAccessType::QWord, aml::FieldLockRule::NoLock, @@ -357,8 +357,7 @@ impl Aml for PciRootBridgeCrs { aml::FieldEntry::Named(*b"P1LH", 32), ], ), - // Create fields that reference values from the mmio32 - // AddressSpace from CRES. + // Create references to values in the mmio32 AddressSpace. &aml::CreateDWordField::new( &aml::Path::new("PS32"), &aml::Path::new("CRES"), @@ -376,28 +375,35 @@ impl Aml for PciRootBridgeCrs { ), // Update the values of mmio32 based on the FWDT. &aml::Store::new( - &aml::Path::new("PS32"), - &aml::Path::new("P0SL"), + &aml::Path::new("PS32"), // mmio32.min + &aml::Path::new("P0SL"), // FWDT.32bit.min (low bits) ), &aml::Store::new( - &aml::Path::new("PE32"), - &aml::Path::new("P0EL"), + &aml::Path::new("PE32"), // mmio32.max + &aml::Path::new("P0EL"), // FWDT.32bit.max (low bits) ), &aml::Store::new( - &aml::Path::new("PL32"), - &aml::Path::new("P0LL"), + &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"), &aml::ZERO), - &aml::Equal::new(&aml::Path::new("P1SH"), &aml::ZERO), + &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 fields that reference values from the mmio64 - // AddressSpace from CR64. + // Create references to values in the mmio64 AddressSpace. &aml::CreateQWordField::new( &aml::Path::new("PS64"), &aml::Path::new("CR64"), @@ -415,16 +421,16 @@ impl Aml for PciRootBridgeCrs { ), // Update the values of mmio64 based on the FWDT. &aml::Store::new( - &aml::Path::new("PS64"), - &aml::Path::new("P1S_"), + &aml::Path::new("PS64"), // mmio64.min + &aml::Path::new("P1S_"), // FWDT.64bit.min ), &aml::Store::new( - &aml::Path::new("PE64"), - &aml::Path::new("P1E_"), + &aml::Path::new("PE64"), // mmio64.max + &aml::Path::new("P1E_"), // FWDT.64bit.max ), &aml::Store::new( - &aml::Path::new("PL64"), - &aml::Path::new("P1L_"), + &aml::Path::new("PL64"), // mmio64.max + &aml::Path::new("P1L_"), // FWDT.64bit.max ), // Concatenate CRES and CR64. &aml::ConcatRes::new( @@ -553,7 +559,7 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { "LPC_".into(), vec![ &aml::Name::new("_ADR".into(), &0x0001_0000_u64), - &Lnk::new("S", 0), + &Lnk::new("LNKS", 0), // PCI Interrupt Routing Configuration Registers, PIRQRC[A:D]. &aml::OpRegion::new( "PRR0".into(), @@ -638,10 +644,10 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { .collect(), )]), ), - &Lnk::new("A", 1), - &Lnk::new("B", 2), - &Lnk::new("C", 3), - &Lnk::new("D", 4), + &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(), @@ -870,13 +876,13 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { /// Represents a PCI IRQ link in the LPC device. struct Lnk<'a> { - letter: &'a str, + name: &'a str, uid: u32, } impl<'a> Lnk<'a> { - fn new(letter: &'a str, uid: u32) -> Self { - Self { letter, uid } + fn new(name: &'a str, uid: u32) -> Self { + Self { name, uid } } } @@ -921,10 +927,13 @@ impl<'a> Aml for Lnk<'a> { return; } - let pir = aml::Path::new(&format!("PIR{}", self.letter)); + let pir = aml::Path::new(&format!( + "PIR{}", + self.name.chars().last().unwrap() + )); aml::Device::new( - aml::Path::new(&format!("LNK{}", self.letter)), + aml::Path::new(self.name), vec![ &aml::Name::new("_HID".into(), &aml::EISAName::new("PNP0C0F")), &aml::Name::new("_UID".into(), &self.uid), diff --git a/lib/propolis/src/firmware/acpi/fadt.rs b/lib/propolis/src/firmware/acpi/fadt.rs index 7e295ddb7..ee46f10c4 100644 --- a/lib/propolis/src/firmware/acpi/fadt.rs +++ b/lib/propolis/src/firmware/acpi/fadt.rs @@ -11,8 +11,10 @@ use super::{OEM_ID, OEM_REVISION, OEM_TABLE_ID, SCI_IRQ}; use acpi_tables::{ // XXX(acpi): 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 fwts reports not being - // compatible with FADT 6.5. + // 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, @@ -41,9 +43,9 @@ const PM1A_CNT_BLK_LEN: u8 = 2; const PM_TMR_BLK_LEN: u8 = 4; const GPE0_BLK_LEN: u8 = 4; -// Represent a bit flag for the FADT IA-PC boot architecture flags. +// 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; diff --git a/lib/propolis/src/firmware/acpi/madt.rs b/lib/propolis/src/firmware/acpi/madt.rs index 40fb69303..860c4a709 100644 --- a/lib/propolis/src/firmware/acpi/madt.rs +++ b/lib/propolis/src/firmware/acpi/madt.rs @@ -44,8 +44,8 @@ impl<'a> Madt<'a> { // - madt: LAPIC has no matching processor UID 1 // - madt: LAPICNMI has no matching processor UID 255 // -// -// +// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiTables/Madt.aslc +// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiPlatformDxe/Qemu.c#L58 impl<'a> Aml for Madt<'a> { fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { let mut table = madt::MADT::new( diff --git a/lib/propolis/src/firmware/acpi/rsdp.rs b/lib/propolis/src/firmware/acpi/rsdp.rs index 080699b09..487d131da 100644 --- a/lib/propolis/src/firmware/acpi/rsdp.rs +++ b/lib/propolis/src/firmware/acpi/rsdp.rs @@ -43,6 +43,10 @@ impl Rsdp { } 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); } diff --git a/lib/propolis/src/firmware/acpi/xsdt.rs b/lib/propolis/src/firmware/acpi/xsdt.rs index b35b05a91..a88bce821 100644 --- a/lib/propolis/src/firmware/acpi/xsdt.rs +++ b/lib/propolis/src/firmware/acpi/xsdt.rs @@ -28,6 +28,10 @@ impl Xsdt { } 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)); diff --git a/lib/propolis/src/hw/ps2/ctrl.rs b/lib/propolis/src/hw/ps2/ctrl.rs index 5cbbf33d4..abd694fcc 100644 --- a/lib/propolis/src/hw/ps2/ctrl.rs +++ b/lib/propolis/src/hw/ps2/ctrl.rs @@ -1101,8 +1101,6 @@ impl acpi::DsdtGenerator for PS2Ctrl { } } -const PS2_KBD_IRQ: u8 = 1; - impl Aml for PS2Ctrl { fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { aml::Device::new( @@ -1125,7 +1123,7 @@ impl Aml for PS2Ctrl { 0x00, 0x01, ), - &aml::IrqNoFlags::new(PS2_KBD_IRQ), + &aml::IrqNoFlags::new(ibmpc::IRQ_PS2_PRI), ]), ), ], diff --git a/lib/propolis/src/hw/qemu/fwcfg.rs b/lib/propolis/src/hw/qemu/fwcfg.rs index cfc77521a..c52fa7c4e 100644 --- a/lib/propolis/src/hw/qemu/fwcfg.rs +++ b/lib/propolis/src/hw/qemu/fwcfg.rs @@ -1433,6 +1433,11 @@ pub mod formats { 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); diff --git a/lib/propolis/src/hw/uart/lpc.rs b/lib/propolis/src/hw/uart/lpc.rs index 517d3818f..47cfe9c71 100644 --- a/lib/propolis/src/hw/uart/lpc.rs +++ b/lib/propolis/src/hw/uart/lpc.rs @@ -67,7 +67,6 @@ impl LpcUart { notify_writable: NotifierCell::new(), }) } - pub fn attach(self: &Arc, bus: &PioBus, port: u16) { let this = self.clone(); let piofn = Arc::new(move |_port: u16, rwo: RWOp| this.pio_rw(rwo)) diff --git a/phd-tests/tests/src/firmware.rs b/phd-tests/tests/src/firmware.rs index 7bcb533f7..af6e940ef 100644 --- a/phd-tests/tests/src/firmware.rs +++ b/phd-tests/tests/src/firmware.rs @@ -46,7 +46,7 @@ async fn acpi_tables_generation(ctx: &TestCtx) { let out = vm.run_shell_command(&cmd).await?; info!(out, "{} creator ID", case.table); - assert!(out.contains(&creator_id_hex)); + assert_eq!(out, creator_id_hex); } // Verify FACS table have the expected version. @@ -62,3 +62,95 @@ async fn acpi_tables_generation(ctx: &TestCtx) { assert_eq!(facs_version, "01"); } + +#[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(); + + // XXX(acpi): 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], + ); +} From bb6f678936a2592c8237682e6b60bad667c68dd2 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Thu, 30 Apr 2026 23:49:39 +0000 Subject: [PATCH 28/47] acpi: add more shared constants b/w fadt and dsdt --- lib/propolis/src/firmware/acpi/dsdt.rs | 19 ++++++++++++++++--- lib/propolis/src/firmware/acpi/fadt.rs | 12 ++++++------ lib/propolis/src/firmware/acpi/mod.rs | 5 +++++ 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/lib/propolis/src/firmware/acpi/dsdt.rs b/lib/propolis/src/firmware/acpi/dsdt.rs index 2d1e65e5d..8d81627e4 100644 --- a/lib/propolis/src/firmware/acpi/dsdt.rs +++ b/lib/propolis/src/firmware/acpi/dsdt.rs @@ -23,7 +23,10 @@ // for some devices when possible. // https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiTables/Dsdt.asl -use super::{IO_APIC_ADDR, LOCAL_APIC_ADDR, PCI_LINK_IRQS, SCI_IRQ}; +use super::{ + GPE0_BLK_ADDR, GPE0_BLK_LEN, IO_APIC_ADDR, LOCAL_APIC_ADDR, PCI_LINK_IRQS, + PM1A_EVT_BLK_ADDR, SCI_IRQ, +}; use acpi_tables::{aml, sdt::Sdt, Aml, AmlSink}; /// The ACPI scope in which DsdtGenerators are placed. @@ -793,9 +796,19 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { &aml::IO::new(0x0402, 0x0402, 0x00, 0x01), &aml::IO::new(0x0440, 0x0440, 0x00, 0x10), // QEMU GPE0 BLK. - &aml::IO::new(0xafe0, 0xafe0, 0x00, 0x04), + &aml::IO::new( + GPE0_BLK_ADDR, + GPE0_BLK_ADDR, + 0x00, + GPE0_BLK_LEN, + ), // PMBLK1. - &aml::IO::new(0xb000, 0xb000, 0x00, 0x40), + &aml::IO::new( + PM1A_EVT_BLK_ADDR, + PM1A_EVT_BLK_ADDR, + 0x00, + 0x40, + ), // IO APIC. &aml::Memory32Fixed::new( false, diff --git a/lib/propolis/src/firmware/acpi/fadt.rs b/lib/propolis/src/firmware/acpi/fadt.rs index ee46f10c4..8bab1e17d 100644 --- a/lib/propolis/src/firmware/acpi/fadt.rs +++ b/lib/propolis/src/firmware/acpi/fadt.rs @@ -7,7 +7,10 @@ //! 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::{OEM_ID, OEM_REVISION, OEM_TABLE_ID, SCI_IRQ}; +use super::{ + GPE0_BLK_ADDR, GPE0_BLK_LEN, OEM_ID, OEM_REVISION, OEM_TABLE_ID, + PM1A_EVT_BLK_ADDR, SCI_IRQ, +}; use acpi_tables::{ // XXX(acpi): Use version 3 to keep FADT table consistent with the original // EDK2 static tables. The acpi_tables crate also generates the @@ -33,15 +36,12 @@ 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_EVT_BLK_ADDR: u32 = 0xb000; const PM1A_CNT_BLK_ADDR: u32 = 0xb004; const PM_TMR_BLK_ADDR: u32 = 0xb008; -const GPE0_BLK_ADDR: u32 = 0xafe0; const PM1A_EVT_BLK_LEN: u8 = 4; const PM1A_CNT_BLK_LEN: u8 = 2; const PM_TMR_BLK_LEN: u8 = 4; -const GPE0_BLK_LEN: u8 = 4; // Represents a bit flag for the FADT IA-PC boot architecture flags. // @@ -94,10 +94,10 @@ impl Aml for Fadt { fadt.acpi_enable = 0xf1; fadt.acpi_disable = 0xf0; - fadt.pm1a_evt_blk = PM1A_EVT_BLK_ADDR.into(); + fadt.pm1a_evt_blk = (PM1A_EVT_BLK_ADDR as u32).into(); fadt.pm1a_cnt_blk = PM1A_CNT_BLK_ADDR.into(); fadt.pm_tmr_blk = PM_TMR_BLK_ADDR.into(); - fadt.gpe0_blk = GPE0_BLK_ADDR.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; diff --git a/lib/propolis/src/firmware/acpi/mod.rs b/lib/propolis/src/firmware/acpi/mod.rs index c48f0e180..c122be5c2 100644 --- a/lib/propolis/src/firmware/acpi/mod.rs +++ b/lib/propolis/src/firmware/acpi/mod.rs @@ -46,3 +46,8 @@ const PCI_LINK_IRQS: [u8; 4] = [0x05, SCI_IRQ, 0x0a, 0x0b]; const IO_APIC_ADDR: u32 = 0xfec0_0000; const LOCAL_APIC_ADDR: u32 = 0xfee0_0000; + +const PM1A_EVT_BLK_ADDR: u16 = 0xb000; + +const GPE0_BLK_ADDR: u16 = 0xafe0; +const GPE0_BLK_LEN: u8 = 4; From 0e1447a9e37582634d3a324ee355cb7d7f9097d6 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Thu, 21 May 2026 22:11:49 +0000 Subject: [PATCH 29/47] acpi: add test for ACPI table generation Test that the generated ACPI tables by comparing them to a snapshot of known and expected tables. --- .gitignore | 5 + Cargo.lock | 1 + bin/propolis-server/Cargo.toml | 1 + bin/propolis-standalone/Cargo.toml | 1 + lib/propolis/Cargo.toml | 1 + lib/propolis/src/firmware/acpi/file_sink.rs | 32 +++++ lib/propolis/src/firmware/acpi/mod.rs | 2 + lib/propolis/src/hw/qemu/fwcfg.rs | 21 +++ phd-tests/framework/src/test_vm/config.rs | 18 ++- phd-tests/tests/Cargo.toml | 1 + phd-tests/tests/src/firmware.rs | 147 ++++++++++++++++---- phd-tests/tests/testdata/acpi/v0/dsdt.dat | Bin 0 -> 3362 bytes phd-tests/tests/testdata/acpi/v0/facs.dat | Bin 0 -> 64 bytes phd-tests/tests/testdata/acpi/v0/fadt.dat | Bin 0 -> 244 bytes phd-tests/tests/testdata/acpi/v0/madt.dat | Bin 0 -> 128 bytes phd-tests/tests/testdata/acpi/v0/rsdp.dat | Bin 0 -> 36 bytes phd-tests/tests/testdata/acpi/v0/ssdt.dat | Bin 0 -> 87 bytes phd-tests/tests/testdata/acpi/v0/xsdt.dat | Bin 0 -> 60 bytes 18 files changed, 199 insertions(+), 31 deletions(-) create mode 100644 lib/propolis/src/firmware/acpi/file_sink.rs create mode 100644 phd-tests/tests/testdata/acpi/v0/dsdt.dat create mode 100644 phd-tests/tests/testdata/acpi/v0/facs.dat create mode 100644 phd-tests/tests/testdata/acpi/v0/fadt.dat create mode 100644 phd-tests/tests/testdata/acpi/v0/madt.dat create mode 100644 phd-tests/tests/testdata/acpi/v0/rsdp.dat create mode 100644 phd-tests/tests/testdata/acpi/v0/ssdt.dat create mode 100644 phd-tests/tests/testdata/acpi/v0/xsdt.dat 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 9d8c5554a..f72ef7fea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5896,6 +5896,7 @@ dependencies = [ "oximeter", "oximeter-producer", "phd-testcase", + "propolis", "propolis-client 0.1.0", "reqwest 0.13.2", "slog", 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-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/lib/propolis/Cargo.toml b/lib/propolis/Cargo.toml index 7220b25fc..721ec6ee0 100644 --- a/lib/propolis/Cargo.toml +++ b/lib/propolis/Cargo.toml @@ -66,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/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/mod.rs b/lib/propolis/src/firmware/acpi/mod.rs index c122be5c2..4bc2e27f8 100644 --- a/lib/propolis/src/firmware/acpi/mod.rs +++ b/lib/propolis/src/firmware/acpi/mod.rs @@ -7,6 +7,7 @@ pub mod dsdt; pub mod facs; pub mod fadt; +pub mod file_sink; pub mod madt; pub mod rsdp; pub mod xsdt; @@ -20,6 +21,7 @@ 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, diff --git a/lib/propolis/src/hw/qemu/fwcfg.rs b/lib/propolis/src/hw/qemu/fwcfg.rs index c52fa7c4e..e18777790 100644 --- a/lib/propolis/src/hw/qemu/fwcfg.rs +++ b/lib/propolis/src/hw/qemu/fwcfg.rs @@ -1455,6 +1455,9 @@ pub mod formats { 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 } @@ -1465,6 +1468,9 @@ pub mod formats { 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 } @@ -1489,6 +1495,9 @@ pub mod formats { 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( @@ -1513,6 +1522,9 @@ pub mod formats { 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 @@ -1542,6 +1554,9 @@ pub mod formats { 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 } @@ -1551,6 +1566,9 @@ pub mod formats { 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: @@ -1576,6 +1594,9 @@ pub mod formats { 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, 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 c9ab223e1..568b24ee2 100644 --- a/phd-tests/tests/Cargo.toml +++ b/phd-tests/tests/Cargo.toml @@ -24,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 index af6e940ef..bece55645 100644 --- a/phd-tests/tests/src/firmware.rs +++ b/phd-tests/tests/src/firmware.rs @@ -3,12 +3,54 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use phd_testcase::*; -use tracing::info; +// 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"), None) + .spawn_vm(ctx.vm_config_builder("acpi_tables_generation").cpus(2), None) .await?; if !vm.guest_os_kind().is_linux() { @@ -20,33 +62,87 @@ async fn acpi_tables_generation(ctx: &TestCtx) { const ACPI_TABLES_PATH: &str = "/sys/firmware/acpi/tables"; - // Verify ACPI tables have the expected Creator ID value. - let creator_id_hex = acpi_tables::CREATOR_ID - .iter() - .map(|i| format!("{:02x?}", i)) - .collect::(); + // 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 CreatorIdTestCase<'a> { + struct TestCase<'a> { table: &'a str, - offset: u8, - len: u8, + expect: Vec, + patches: Vec, } - let creator_id_test_cases = [ - CreatorIdTestCase { table: "APIC", offset: 28, len: 4 }, - CreatorIdTestCase { table: "DSDT", offset: 28, len: 4 }, - CreatorIdTestCase { table: "FACP", offset: 28, len: 4 }, - CreatorIdTestCase { table: "SSDT", offset: 28, len: 4 }, - ]; - for case in creator_id_test_cases.iter() { - let cmd = format!( - "xxd -s {1} -l {2} -c {2} -p {3}/{0}", - case.table, case.offset, case.len, ACPI_TABLES_PATH, - ); - let out = vm.run_shell_command(&cmd).await?; - info!(out, "{} creator ID", case.table); - - assert_eq!(out, creator_id_hex); + 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. @@ -58,7 +154,6 @@ async fn acpi_tables_generation(ctx: &TestCtx) { "FACS", FACS_VERSION_OFFSET, FACS_VERSION_LEN, ACPI_TABLES_PATH ); let facs_version = vm.run_shell_command(&facs_version_cmd).await?; - info!(facs_version, "FACS table version"); assert_eq!(facs_version, "01"); } 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 0000000000000000000000000000000000000000..fa6c52316acdc6d29a7bec84429f43ddd4f6343c GIT binary patch literal 3362 zcma)9&1)M+6o0e3mRF+>ON#PAX^T)u4=J>gov)*%U0G4Qb~d|mQc&nRApxI?$jy|3 zY(f*8KtlTuI6?RH+*8ga2mcBB69PR{&=U2%S-sss6q*&x&b;@2zxSIryE`K{bo=!J zfcT@g-S2p{+jqA%YIs=y5ZrC`@g`0#z}frXT2a`Je5coloFLq|=k_JoQP_wo%a&Zi zFdSTsDj5PoY4bfr4-TF8gs`KVJvZMvm8KoF+yFz{?QLv#dM?;b&=HcpkY<&b<1S*n!%cFea+kx2%^`1X=XMYggj#Ilsf*uYB*?l_7>l zY++(c*$`Td<1{`5voU2vC5q9|7!f_UrHl+VKx0I(Vnk!g zh%-@Wd^{74tlNZ?k-?_Uh+xHt#*~ru;CqxXvTjRbWU!?(aw*W5GO`|gNfSoay`(WR zcu8mEQlK$qWIY*;k#%P@Mh0heMlJ;!Q%2UatTD3gWsQ-+%Q_>M0*xsn>#;RP)@^Hy z47PPfE(ID>M%I(n7+H5#V`OkvXXH|#F=b>uQe$M@Qe$MW)ET)HXiOPd4}JrawPxKp zjgi4Qosmm{#*~ru;HNiXWZijSLkxPNbl#%t|u9GmbZv50IHU<}TMlJ;!Q%2TP z)EHTJQDbCqQD@{*pfP1+J@~~>^sw#~jgi4CIwO|?jVWUr28W#ieG=6kh~*S$*|hKj z8S?&^UzzWTQP^+ETs0GgK`8L%_Cnk){U9tCW$q{gS%D8B?L|4-ieOBx!N?DSh9P$) zZa1G%AN*d>vaxAX<51(;)w4d5i<9h+dvXM0nSE`IG1UzpiL!QkXQPqiplxp-j>#sM zdcmE0Cxu@WYPd(EF*$6XRGpJ&j*5^q=^TI$Gtd^ikJIxqIK9@w^h9-q<_BXELK3TG zHNl^#FGtF<63>f7sd0{GhzM+}=T%jX_hL;$l0q?q!!63zUdSemg>-E#U0bDVV}vT> zJY5^h`hV%#^K{N)I&&eNqos3HI%grB^9kvkc{+D7owbn8)zY~tox70E{e*Px;Bd|F z(W8n7w_NMgruvj?@{6vhK@E>qU2y^@>V9gST42;Iwcgv7yVXicV&F4NxEu0nH$H`x zy5ay10L-p<2G7(P?+A|6_$9nl<9F~5pmw>W_%8Lgf>;Ee9>tulBnw@!4(k9`UBqEQ zf36k7a(dVhhN!X*f}X~D=x+vGKzklmkg_WtzylB}=F*10lVr?W@dQ-H5AXpGwsoix z)vdyJeSEj(=i0oX-dhnzZ|CIiT~UG(hId6B>HuGMg$pjg=Uwp-9;&)MhR67xbj1^R z0#NLV5scLQpTbjBkLU1QjbFeEHGT!J6yFpQKW$x~axNxANJ!MVNF#ERjG zeLl;wMA1`oY6|;eDX#Ag1g`!Ea5V->tP(vAG18bCh{T~Gw=Wbsi3wJtOJB>zzWKtTnuuFKUg&p~w+kwd8XmF5I ieZS-PZT!=MU)q&wF>w7p49P~v+5El0?~CCcjsFEg4<0T6 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..63282ea5e080a4199cd469649dae834cff73308a GIT binary patch literal 64 TcmZ>BbPjf4zzrC2OMztoRNw($ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..36a1ba66b5a9258d84382b742ba87e5edfdecc03 GIT binary patch literal 244 zcmZ>BbPo8!z`(%#$Un^2O+f)fy1IB9DKZHw1cf<<0Hqj#7z#KUHUU{5KLCjh5Fr*Q z4dQVC*$>u3#hDmbSb)q_hL_9`5eT`I6=)y_1H(@q2%il}(gDh5Q~)xN%!LXwFbXg* YAlnBMN2VDSAnpK(F)AS31LVR108C^S0RR91 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2dc831fb3e5ee95aae4376013207e3a7b44546e4 GIT binary patch literal 128 zcmZ<^@N{lqU|?XJ<{#$krl0^KU0uA56q$q-g2Eg_KvE105B>oO1_ll=2S_tAg7}O) qOhEF$Kadm?7Z8IOAQmetkmd!kI5}Y~E}#O47&kYV#lrR;Ra1dKqjpfJY}kW_SoTewRI1J5-+1_mAjpa@4y Ye6Vr65Ely Date: Wed, 20 May 2026 16:04:56 +0000 Subject: [PATCH 30/47] acpi: use stable references to ACPI names The ACPI specification defines several reserved names that are used in multiple places in the code that generates the tables. To avoid typos and repetition, this commit defines them as functions and also provides helpers to use them in different contexts, such as `Path`, `Name`, or `Method`. --- lib/propolis/src/firmware/acpi/dsdt.rs | 309 ++++++++++--------------- lib/propolis/src/firmware/acpi/mod.rs | 170 ++++++++++++++ lib/propolis/src/hw/ps2/ctrl.rs | 41 ++-- lib/propolis/src/hw/uart/lpc.rs | 19 +- 4 files changed, 328 insertions(+), 211 deletions(-) diff --git a/lib/propolis/src/firmware/acpi/dsdt.rs b/lib/propolis/src/firmware/acpi/dsdt.rs index 8d81627e4..312892905 100644 --- a/lib/propolis/src/firmware/acpi/dsdt.rs +++ b/lib/propolis/src/firmware/acpi/dsdt.rs @@ -23,6 +23,7 @@ // for some devices when possible. // https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiTables/Dsdt.asl +use super::{devids, methods, names, paths}; use super::{ GPE0_BLK_ADDR, GPE0_BLK_LEN, IO_APIC_ADDR, LOCAL_APIC_ADDR, PCI_LINK_IRQS, PM1A_EVT_BLK_ADDR, SCI_IRQ, @@ -174,10 +175,10 @@ impl<'a> Aml for PciRootBridge<'a> { aml::Device::new( "PCI0".into(), vec![ - &aml::Name::new("_HID".into(), &aml::EISAName::new("PNP0A03")), - &aml::Name::new("_ADR".into(), &aml::ZERO), - &aml::Name::new("_BBN".into(), &aml::ZERO), - &aml::Name::new("_UID".into(), &aml::ZERO), + &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 }, @@ -320,8 +321,7 @@ impl Aml for PciRootBridgeCrs { // 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. - aml::Method::new( - "_CRS".into(), + methods::crs( 0, true, vec![ @@ -507,8 +507,7 @@ impl Aml for PciRootBridgePrt { } let ptr = ptr_entries.iter().map(|p| p as &dyn Aml).collect(); - aml::Method::new( - "_PRT".into(), + methods::prt( 0, false, vec![&aml::Return::new(&aml::Package::new(ptr))], @@ -561,7 +560,7 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { aml::Device::new( "LPC_".into(), vec![ - &aml::Name::new("_ADR".into(), &0x0001_0000_u64), + &names::adr(&0x0001_0000_u64), &Lnk::new("LNKS", 0), // PCI Interrupt Routing Configuration Registers, PIRQRC[A:D]. &aml::OpRegion::new( @@ -655,113 +654,87 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { &aml::Device::new( "PIC_".into(), vec![ - &aml::Name::new( - "_HID".into(), - &aml::EISAName::new("PNP0000"), - ), - &aml::Name::new( - "_CRS".into(), - &aml::ResourceTemplate::new(vec![ - &aml::IO::new(0x0020, 0x0020, 0x00, 0x02), - &aml::IO::new(0x00A0, 0x00A0, 0x00, 0x02), - &aml::IO::new(0x04d0, 0x04d0, 0x00, 0x02), - &aml::IrqNoFlags::new(2), - ]), - ), + &names::hid(&aml::EISAName::new( + devids::AT_INT_CONTROLLER, + )), + &names::crs(&aml::ResourceTemplate::new(vec![ + &aml::IO::new(0x0020, 0x0020, 0x00, 0x02), + &aml::IO::new(0x00A0, 0x00A0, 0x00, 0x02), + &aml::IO::new(0x04d0, 0x04d0, 0x00, 0x02), + &aml::IrqNoFlags::new(2), + ])), ], ), // ISA DMA. &aml::Device::new( "DMAC".into(), vec![ - &aml::Name::new( - "_HID".into(), - &aml::EISAName::new("PNP0200"), - ), - &aml::Name::new( - "_CRS".into(), - &aml::ResourceTemplate::new(vec![ - &aml::IO::new(0x0000, 0x0000, 0x00, 0x10), - &aml::IO::new(0x0081, 0x0081, 0x00, 0x03), - &aml::IO::new(0x0087, 0x0087, 0x00, 0x01), - &aml::IO::new(0x0089, 0x0089, 0x00, 0x03), - &aml::IO::new(0x008f, 0x008f, 0x00, 0x01), - &aml::IO::new(0x00c0, 0x00c0, 0x00, 0x20), - &aml::Dma::new( - aml::DmaChannelSpeed::Compatibility, - aml::DmaMasterStatus::NotMaster, - aml::DmaTransferType::Transfer8, - vec![4], - ), - ]), - ), + &names::hid(&aml::EISAName::new( + devids::AT_DMA_CONTROLLER, + )), + &names::crs(&aml::ResourceTemplate::new(vec![ + &aml::IO::new(0x0000, 0x0000, 0x00, 0x10), + &aml::IO::new(0x0081, 0x0081, 0x00, 0x03), + &aml::IO::new(0x0087, 0x0087, 0x00, 0x01), + &aml::IO::new(0x0089, 0x0089, 0x00, 0x03), + &aml::IO::new(0x008f, 0x008f, 0x00, 0x01), + &aml::IO::new(0x00c0, 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![ - &aml::Name::new( - "_HID".into(), - &aml::EISAName::new("PNP0100"), - ), - &aml::Name::new( - "_CRS".into(), - &aml::ResourceTemplate::new(vec![ - &aml::IO::new(0x0040, 0x0040, 0x00, 0x04), - &aml::IrqNoFlags::new(0), - ]), - ), + &names::hid(&aml::EISAName::new(devids::AT_TIMER)), + &names::crs(&aml::ResourceTemplate::new(vec![ + &aml::IO::new(0x0040, 0x0040, 0x00, 0x04), + &aml::IrqNoFlags::new(0), + ])), ], ), // Real Time Clock. &aml::Device::new( "RTC_".into(), vec![ - &aml::Name::new( - "_HID".into(), - &aml::EISAName::new("PNP0B00"), - ), - &aml::Name::new( - "_CRS".into(), - &aml::ResourceTemplate::new(vec![ - &aml::IO::new(0x0070, 0x0070, 0x00, 0x02), - &aml::IrqNoFlags::new(8), - ]), - ), + &names::hid(&aml::EISAName::new( + devids::AT_REAL_TIME_CLOCK, + )), + &names::crs(&aml::ResourceTemplate::new(vec![ + &aml::IO::new(0x0070, 0x0070, 0x00, 0x02), + &aml::IrqNoFlags::new(8), + ])), ], ), // PCAT Speaker. &aml::Device::new( "SPKR".into(), vec![ - &aml::Name::new( - "_HID".into(), - &aml::EISAName::new("PNP0800"), - ), - &aml::Name::new( - "_CRS".into(), - &aml::ResourceTemplate::new(vec![&aml::IO::new( - 0x0061, 0x0061, 0x01, 0x01, - )]), - ), + &names::hid(&aml::EISAName::new( + devids::AT_SPEAKER_SOUND, + )), + &names::crs(&aml::ResourceTemplate::new(vec![ + &aml::IO::new(0x0061, 0x0061, 0x01, 0x01), + ])), ], ), // Floating Point Coprocessor. &aml::Device::new( "FPU_".into(), vec![ - &aml::Name::new( - "_HID".into(), - &aml::EISAName::new("PNP0C04"), - ), - &aml::Name::new( - "_CRS".into(), - &aml::ResourceTemplate::new(vec![ - &aml::IO::new(0x00f0, 0x00f0, 0x00, 0x10), - &aml::IrqNoFlags::new(13), - ]), - ), + &names::hid(&aml::EISAName::new( + devids::MATH_COPROCESSOR, + )), + &names::crs(&aml::ResourceTemplate::new(vec![ + &aml::IO::new(0x00f0, 0x00f0, 0x00, 0x10), + &aml::IrqNoFlags::new(13), + ])), ], ), // Generic motherboard devices and pieces that don't fit @@ -769,60 +742,54 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { &aml::Device::new( "XTRA".into(), vec![ - &aml::Name::new( - "_HID".into(), - &aml::EISAName::new("PNP0C02"), - ), - &aml::Name::new("_UID".into(), &aml::ONE), - &aml::Name::new( - "_CRS".into(), - &aml::ResourceTemplate::new(vec![ - &aml::IO::new(0x0010, 0x0010, 0x00, 0x10), - &aml::IO::new(0x0022, 0x0022, 0x00, 0x1e), - &aml::IO::new(0x0044, 0x0044, 0x00, 0x1c), - &aml::IO::new(0x0062, 0x0062, 0x00, 0x02), - &aml::IO::new(0x0065, 0x0065, 0x00, 0x0b), - &aml::IO::new(0x0072, 0x0072, 0x00, 0x0e), - &aml::IO::new(0x0080, 0x0080, 0x00, 0x01), - &aml::IO::new(0x0084, 0x0084, 0x00, 0x03), - &aml::IO::new(0x0088, 0x0088, 0x00, 0x01), - &aml::IO::new(0x008c, 0x008c, 0x00, 0x03), - &aml::IO::new(0x0090, 0x0090, 0x00, 0x10), - &aml::IO::new(0x00a2, 0x00a2, 0x00, 0x1e), - &aml::IO::new(0x00e0, 0x00e0, 0x00, 0x10), - &aml::IO::new(0x01e0, 0x01e0, 0x00, 0x10), - &aml::IO::new(0x0160, 0x0160, 0x00, 0x10), - &aml::IO::new(0x0370, 0x0370, 0x00, 0x02), - &aml::IO::new(0x0402, 0x0402, 0x00, 0x01), - &aml::IO::new(0x0440, 0x0440, 0x00, 0x10), - // QEMU GPE0 BLK. - &aml::IO::new( - GPE0_BLK_ADDR, - GPE0_BLK_ADDR, - 0x00, - GPE0_BLK_LEN, - ), - // PMBLK1. - &aml::IO::new( - PM1A_EVT_BLK_ADDR, - PM1A_EVT_BLK_ADDR, - 0x00, - 0x40, - ), - // IO APIC. - &aml::Memory32Fixed::new( - false, - IO_APIC_ADDR, - 0x0000_1000, - ), - // LAPIC. - &aml::Memory32Fixed::new( - false, - LOCAL_APIC_ADDR, - 0x0010_0000, - ), - ]), - ), + &names::hid(&aml::EISAName::new(devids::GENERAL_ID)), + &names::uid(&aml::ONE), + &names::crs(&aml::ResourceTemplate::new(vec![ + &aml::IO::new(0x0010, 0x0010, 0x00, 0x10), + &aml::IO::new(0x0022, 0x0022, 0x00, 0x1e), + &aml::IO::new(0x0044, 0x0044, 0x00, 0x1c), + &aml::IO::new(0x0062, 0x0062, 0x00, 0x02), + &aml::IO::new(0x0065, 0x0065, 0x00, 0x0b), + &aml::IO::new(0x0072, 0x0072, 0x00, 0x0e), + &aml::IO::new(0x0080, 0x0080, 0x00, 0x01), + &aml::IO::new(0x0084, 0x0084, 0x00, 0x03), + &aml::IO::new(0x0088, 0x0088, 0x00, 0x01), + &aml::IO::new(0x008c, 0x008c, 0x00, 0x03), + &aml::IO::new(0x0090, 0x0090, 0x00, 0x10), + &aml::IO::new(0x00a2, 0x00a2, 0x00, 0x1e), + &aml::IO::new(0x00e0, 0x00e0, 0x00, 0x10), + &aml::IO::new(0x01e0, 0x01e0, 0x00, 0x10), + &aml::IO::new(0x0160, 0x0160, 0x00, 0x10), + &aml::IO::new(0x0370, 0x0370, 0x00, 0x02), + &aml::IO::new(0x0402, 0x0402, 0x00, 0x01), + &aml::IO::new(0x0440, 0x0440, 0x00, 0x10), + // QEMU GPE0 BLK. + &aml::IO::new( + GPE0_BLK_ADDR, + GPE0_BLK_ADDR, + 0x00, + GPE0_BLK_LEN, + ), + // PMBLK1. + &aml::IO::new( + PM1A_EVT_BLK_ADDR, + PM1A_EVT_BLK_ADDR, + 0x00, + 0x40, + ), + // IO APIC. + &aml::Memory32Fixed::new( + false, + IO_APIC_ADDR, + 0x0000_1000, + ), + // LAPIC. + &aml::Memory32Fixed::new( + false, + LOCAL_APIC_ADDR, + 0x0010_0000, + ), + ])), ], ), &DsdtGeneratorAml::new(self.generators, DsdtScope::Lpc), @@ -837,13 +804,10 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { &aml::Device::new( "PEVT".into(), vec![ - &aml::Name::new("_HID".into(), &"QEMU0001"), - &aml::Name::new( - "_CRS".into(), - &aml::ResourceTemplate::new(vec![&aml::IO::new( - 0x0505, 0x0505, 0x01, 0x01, - )]), - ), + &names::hid(&devids::QEMU_PVPANIC), + &names::crs(&aml::ResourceTemplate::new(vec![ + &aml::IO::new(0x0505, 0x0505, 0x01, 0x01), + ])), &aml::OpRegion::new( "PEOR".into(), aml::OpRegionSpace::SystemIO, @@ -857,7 +821,7 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { aml::FieldUpdateRule::Preserve, vec![aml::FieldEntry::Named(*b"PEPT", 8)], ), - &aml::Name::new("_STA".into(), &0x0f_u64), + &names::sta(&0x0f_u64), &aml::Method::new( "RDPT".into(), 0, @@ -906,27 +870,15 @@ impl<'a> Lnk<'a> { aml::Device::new( "LNKS".into(), vec![ - &aml::Name::new("_HID".into(), &aml::EISAName::new("PNP0C0F")), - &aml::Name::new("_UID".into(), &aml::ZERO), - &aml::Name::new("_STA".into(), &0x0b_u64), - &aml::Method::new("_SRS".into(), 1, false, vec![]), - &aml::Method::new("_DIS".into(), 0, false, vec![]), - &aml::Name::new( - "_PRS".into(), - &aml::ResourceTemplate::new(vec![&aml::Interrupt::new( - true, - false, - false, - true, - vec![0x09], - )]), - ), - &aml::Method::new( - "_CRS".into(), - 0, - false, - vec![&aml::Return::new(&aml::Path::new("_PRS"))], - ), + &names::hid(&aml::EISAName::new(devids::PCI_INT_LINK)), + &names::uid(&aml::ZERO), + &names::sta(&0x0b_u64), + &methods::srs(1, false, vec![]), + &methods::dis(0, false, vec![]), + &names::prs(&aml::ResourceTemplate::new(vec![ + &aml::Interrupt::new(true, false, false, true, vec![0x09]), + ])), + &methods::crs(0, false, vec![&aml::Return::new(&paths::prs())]), ], ) .to_aml_bytes(sink); @@ -948,10 +900,9 @@ impl<'a> Aml for Lnk<'a> { aml::Device::new( aml::Path::new(self.name), vec![ - &aml::Name::new("_HID".into(), &aml::EISAName::new("PNP0C0F")), - &aml::Name::new("_UID".into(), &self.uid), - &aml::Method::new( - "_STA".into(), + &names::hid(&aml::EISAName::new(devids::PCI_INT_LINK)), + &names::uid(&self.uid), + &methods::sta( 0, false, vec![&aml::Return::new(&aml::MethodCall::new( @@ -959,14 +910,12 @@ impl<'a> Aml for Lnk<'a> { vec![&pir], ))], ), - &aml::Method::new( - "_DIS".into(), + &methods::dis( 0, false, vec![&aml::Or::new(&pir, &pir, &0x80_u64)], ), - &aml::Method::new( - "_CRS".into(), + &methods::crs( 0, false, vec![&aml::Return::new(&aml::MethodCall::new( @@ -974,8 +923,7 @@ impl<'a> Aml for Lnk<'a> { vec![&pir], ))], ), - &aml::Method::new( - "_PRS".into(), + &methods::prs( 0, false, vec![&aml::Return::new(&aml::MethodCall::new( @@ -983,8 +931,7 @@ impl<'a> Aml for Lnk<'a> { vec![], ))], ), - &aml::Method::new( - "_SRS".into(), + &methods::srs( 1, false, vec![ diff --git a/lib/propolis/src/firmware/acpi/mod.rs b/lib/propolis/src/firmware/acpi/mod.rs index 4bc2e27f8..0bae0cd29 100644 --- a/lib/propolis/src/firmware/acpi/mod.rs +++ b/lib/propolis/src/firmware/acpi/mod.rs @@ -53,3 +53,173 @@ const PM1A_EVT_BLK_ADDR: u16 = 0xb000; const GPE0_BLK_ADDR: u16 = 0xafe0; const GPE0_BLK_LEN: u8 = 4; + +/// 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/hw/ps2/ctrl.rs b/lib/propolis/src/hw/ps2/ctrl.rs index abd694fcc..4c7632567 100644 --- a/lib/propolis/src/hw/ps2/ctrl.rs +++ b/lib/propolis/src/hw/ps2/ctrl.rs @@ -1106,26 +1106,27 @@ impl Aml for PS2Ctrl { aml::Device::new( "PS2K".into(), vec![ - &aml::Name::new("_HID".into(), &aml::EISAName::new("PNP0303")), - &aml::Name::new("_CID".into(), &aml::EISAName::new("PNP030B")), - &aml::Name::new( - "_CRS".into(), - &aml::ResourceTemplate::new(vec![ - &aml::IO::new( - ibmpc::PORT_PS2_DATA, - ibmpc::PORT_PS2_DATA, - 0x00, - 0x01, - ), - &aml::IO::new( - ibmpc::PORT_PS2_CMD_STATUS, - ibmpc::PORT_PS2_CMD_STATUS, - 0x00, - 0x01, - ), - &aml::IrqNoFlags::new(ibmpc::IRQ_PS2_PRI), - ]), - ), + &acpi::names::hid(&aml::EISAName::new( + acpi::devids::IBM_ENHANCED_KEYBOARD, + )), + &acpi::names::cid(&aml::EISAName::new( + acpi::devids::MICROSOFT_RESERVED_KEYBOARD, + )), + &acpi::names::crs(&aml::ResourceTemplate::new(vec![ + &aml::IO::new( + ibmpc::PORT_PS2_DATA, + ibmpc::PORT_PS2_DATA, + 0x00, + 0x01, + ), + &aml::IO::new( + ibmpc::PORT_PS2_CMD_STATUS, + ibmpc::PORT_PS2_CMD_STATUS, + 0x00, + 0x01, + ), + &aml::IrqNoFlags::new(ibmpc::IRQ_PS2_PRI), + ])), ], ) .to_aml_bytes(sink); diff --git a/lib/propolis/src/hw/uart/lpc.rs b/lib/propolis/src/hw/uart/lpc.rs index 47cfe9c71..961c55ded 100644 --- a/lib/propolis/src/hw/uart/lpc.rs +++ b/lib/propolis/src/hw/uart/lpc.rs @@ -242,16 +242,15 @@ impl Aml for LpcUart { aml::Device::new( aml::Path::new(&format!("UAR{}", uid)), vec![ - &aml::Name::new("_HID".into(), &aml::EISAName::new("PNP0501")), - &aml::Name::new("_DDN".into(), &self.name), - &aml::Name::new("_UID".into(), &uid), - &aml::Name::new( - "_CRS".into(), - &aml::ResourceTemplate::new(vec![ - &aml::IO::new(port, port, 1, REGISTER_LEN as u8), - &aml::Irq::new(true, false, false, self.irq), - ]), - ), + &acpi::names::hid(&aml::EISAName::new( + acpi::devids::COM_PORT_16550A, + )), + &acpi::names::ddn(&self.name), + &acpi::names::uid(&uid), + &acpi::names::crs(&aml::ResourceTemplate::new(vec![ + &aml::IO::new(port, port, 1, REGISTER_LEN as u8), + &aml::Irq::new(true, false, false, self.irq), + ])), ], ) .to_aml_bytes(sink); From a72a71792de802e13d82e11fd9770260b8ef2b42 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Fri, 22 May 2026 01:19:25 +0000 Subject: [PATCH 31/47] acpi: update comments in DSDT table generation --- lib/propolis/src/firmware/acpi/dsdt.rs | 138 +++++++++++++++---------- 1 file changed, 86 insertions(+), 52 deletions(-) diff --git a/lib/propolis/src/firmware/acpi/dsdt.rs b/lib/propolis/src/firmware/acpi/dsdt.rs index 312892905..9b4d333af 100644 --- a/lib/propolis/src/firmware/acpi/dsdt.rs +++ b/lib/propolis/src/firmware/acpi/dsdt.rs @@ -14,14 +14,17 @@ //! Structs that implement the [`DsdtGenerator`] trait can be passed to //! [`DsdtConfig`] and their AML code will be added to the scope they selected. -// XXX(acpi): Most of the DSDT and SSDT tables are generated here to keep them -// consistent with the original EDK2 static tables. In the future -// they could be created by the devices they represent. For example, -// the _SB scope can be created by the I440FxHostBridge struct, the -// LPC by Piix3Lpc etc., but they currently lack all the information -// necessary to generate the tables. This pattern is already used -// for some devices when possible. +// 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::{devids, methods, names, paths}; use super::{ @@ -30,6 +33,21 @@ use super::{ }; use acpi_tables::{aml, sdt::Sdt, Aml, AmlSink}; +// The DSDT and SSDT OEM ID, OEM table ID, and OEM table revision are +// currently kept the same as the ones used by the original tables from EDK2. +// They could be updated to Propolis-specific values in the future. +// +// 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. +const DSDT_OEM_ID: [u8; 6] = *b"INTEL "; +const DSDT_OEM_TABLE_ID: [u8; 8] = *b"OVMF "; +const DSDT_OEM_TABLE_REV: u32 = 0x4; + +const SSDT_OEM_ID: [u8; 6] = *b"REDHAT"; +const SSDT_OEM_TABLE_ID: [u8; 8] = *b"OVMF "; +const SSDT_OEM_TABLE_REV: u32 = 0x1; + /// The ACPI scope in which DsdtGenerators are placed. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DsdtScope { @@ -98,9 +116,9 @@ impl<'a> Aml for Dsdt<'a> { fn to_aml_bytes(&self, sink: &mut dyn AmlSink) { let mut dsdt = Vec::new(); - // XXX(acpi): 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. + // 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( @@ -129,10 +147,14 @@ impl<'a> Aml for Dsdt<'a> { .to_aml_bytes(&mut dsdt); // DSDT table. - // XXX(acpi): OEM ID, table ID, and revision are kept the same as the - // original static EDK2 tables for consistency. They could - // be set to Propolis-specific values in the future. - let mut sdt = Sdt::new(*b"DSDT", 36, 1, *b"INTEL ", *b"OVMF ", 0x4); + let mut sdt = Sdt::new( + *b"DSDT", + 36, + 1, + DSDT_OEM_ID, + DSDT_OEM_TABLE_ID, + DSDT_OEM_TABLE_REV, + ); sdt.append_slice(dsdt.as_slice()); sdt.to_aml_bytes(sink); } @@ -222,10 +244,10 @@ const INTERRUPT_LIST_OFFSET: usize = 0x05; /// information. struct PciRootBridgeCrs {} -// XXX(acpi): This implementation currently follows the original static EDK2 -// tables. It can be simplified to return a single ResourceTemplate -// with all final values already populated instead of dynamically -// updating them based on the values from FWDT. +// 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 @@ -457,15 +479,18 @@ fn aml_len(vec: &[&dyn Aml]) -> usize { sink.len() } -/// Number of devices in the PCI0 root bridge. -// XXX(acpi): Value inherited from the original EDK2 static tables. It should -// be updated to match Propolis's expectations. +/// 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 { @@ -552,9 +577,8 @@ struct PciRootBridgeLpc<'a> { generators: &'a [&'a dyn DsdtGenerator], } -// XXX(acpi): 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 in a virtual machine environment. +// 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( @@ -795,12 +819,12 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { &DsdtGeneratorAml::new(self.generators, DsdtScope::Lpc), // QEMU panic device. // - // XXX(acpi): This code could be generated by the QemuPvpanic - // struct and passed as a DsdtGenerator, but it's - // only present if the enable_isa configuration is - // enabled for the instance. So it's always - // generated here for now to maintain consistency - // with the original EDK2 static tables. + // 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![ @@ -985,9 +1009,11 @@ impl Ssdt { } } -// XXX(acpi): This implementation follows the original static EDK2 tables. It -// can probably be further simplified or eliminated entirely. +// 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(); @@ -999,9 +1025,9 @@ impl Aml for Ssdt { // On boot, the \_SB.PCI0._CRS method reads FWDT and adjusts the PCI // bus configuration based on the data it holds. // - // XXX(acpi): This process can be removed if \_SB.PCI0._CRS returns a - // static ResourceTemplate that is generated with the right - // instance data. + // 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, @@ -1011,13 +1037,13 @@ impl Aml for Ssdt { .to_aml_bytes(&mut ssdt); // Sleep states. - // XXX(acpi): These sleep states are kept to keep the SSDT consistent - // 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 consistent with original EDK tables. + // 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![ @@ -1040,20 +1066,26 @@ impl Aml for Ssdt { ) .to_aml_bytes(&mut ssdt); - // XXX(acpi): OEM ID, table ID, and revision are kept the same as the - // original static EDK2 tables for consistency. They could - // be set to Propolis-specific values in the future. - let mut sdt = Sdt::new(*b"SSDT", 36, 1, *b"REDHAT", *b"OVMF ", 0x1); + let mut sdt = Sdt::new( + *b"SSDT", + 36, + 1, + SSDT_OEM_ID, + SSDT_OEM_TABLE_ID, + SSDT_OEM_TABLE_REV, + ); 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. +// 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. // -// XXX(acpi): Created just to keep tables consistent with the original EDK2 -// tables. +// 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, } @@ -1072,10 +1104,12 @@ impl Aml for DWord { } // Provides consistent Byte AML values. The acpi_tables crate minimizes -// integers to the smallest word size that the number fits. +// 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. // -// XXX(acpi): Created just to keep tables consistent with the original EDK2 -// tables. +// 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, } From 5ba9e220e56f45e726e6e7786fbb480a17b3a5f8 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Fri, 22 May 2026 18:22:34 +0000 Subject: [PATCH 32/47] acpi: document bhyve and unhandled IO ports --- lib/propolis/src/firmware/acpi/dsdt.rs | 34 +++++++++++++++++++++----- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/lib/propolis/src/firmware/acpi/dsdt.rs b/lib/propolis/src/firmware/acpi/dsdt.rs index 9b4d333af..d547b2553 100644 --- a/lib/propolis/src/firmware/acpi/dsdt.rs +++ b/lib/propolis/src/firmware/acpi/dsdt.rs @@ -567,6 +567,15 @@ impl<'a> Aml for PrtEntry<'a> { } } +// 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 @@ -682,14 +691,18 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { devids::AT_INT_CONTROLLER, )), &names::crs(&aml::ResourceTemplate::new(vec![ - &aml::IO::new(0x0020, 0x0020, 0x00, 0x02), - &aml::IO::new(0x00A0, 0x00A0, 0x00, 0x02), - &aml::IO::new(0x04d0, 0x04d0, 0x00, 0x02), + &aml::IO::new(IO_ICU1, IO_ICU1, 0x00, 0x02), + &aml::IO::new(IO_ICU2, IO_ICU2, 0x00, 0x02), + &aml::IO::new(IO_ELCR1, 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![ @@ -718,7 +731,7 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { vec![ &names::hid(&aml::EISAName::new(devids::AT_TIMER)), &names::crs(&aml::ResourceTemplate::new(vec![ - &aml::IO::new(0x0040, 0x0040, 0x00, 0x04), + &aml::IO::new(IO_TIMER1, IO_TIMER1, 0x00, 0x04), &aml::IrqNoFlags::new(0), ])), ], @@ -731,7 +744,7 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { devids::AT_REAL_TIME_CLOCK, )), &names::crs(&aml::ResourceTemplate::new(vec![ - &aml::IO::new(0x0070, 0x0070, 0x00, 0x02), + &aml::IO::new(IO_RTC, IO_RTC, 0x00, 0x02), &aml::IrqNoFlags::new(8), ])), ], @@ -744,11 +757,15 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { devids::AT_SPEAKER_SOUND, )), &names::crs(&aml::ResourceTemplate::new(vec![ - &aml::IO::new(0x0061, 0x0061, 0x01, 0x01), + &aml::IO::new(NMISC_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![ @@ -763,6 +780,11 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { ), // 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![ From 889b95a7b5ecf35b8f3bf22912aa660231dadb97 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Fri, 22 May 2026 19:53:26 +0000 Subject: [PATCH 33/47] acpi: expand documentation for sleep states --- lib/propolis/src/firmware/acpi/dsdt.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/propolis/src/firmware/acpi/dsdt.rs b/lib/propolis/src/firmware/acpi/dsdt.rs index d547b2553..cdf32b139 100644 --- a/lib/propolis/src/firmware/acpi/dsdt.rs +++ b/lib/propolis/src/firmware/acpi/dsdt.rs @@ -88,7 +88,14 @@ impl<'a> Aml for DsdtGeneratorAml<'a> { /// 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. +/// +/// 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; @@ -998,7 +1005,7 @@ impl<'a> Aml for Lnk<'a> { /// 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 From 2e3e0485a5652a1978c9d23e14910481ac9a917e Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Fri, 22 May 2026 21:05:10 +0000 Subject: [PATCH 34/47] acpi: wrapper for IO port declaration --- lib/propolis/src/firmware/acpi/aml.rs | 188 +++++++++++++++++++++++++ lib/propolis/src/firmware/acpi/dsdt.rs | 88 +++++------- lib/propolis/src/firmware/acpi/mod.rs | 171 +--------------------- lib/propolis/src/hw/ps2/ctrl.rs | 24 +--- lib/propolis/src/hw/uart/lpc.rs | 12 +- 5 files changed, 239 insertions(+), 244 deletions(-) create mode 100644 lib/propolis/src/firmware/acpi/aml.rs diff --git a/lib/propolis/src/firmware/acpi/aml.rs b/lib/propolis/src/firmware/acpi/aml.rs new file mode 100644 index 000000000..4805e4ffd --- /dev/null +++ b/lib/propolis/src/firmware/acpi/aml.rs @@ -0,0 +1,188 @@ +// 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. +/// +/// 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) +} + +/// 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 index cdf32b139..9cc08676d 100644 --- a/lib/propolis/src/firmware/acpi/dsdt.rs +++ b/lib/propolis/src/firmware/acpi/dsdt.rs @@ -26,7 +26,7 @@ // // This pattern is already used for some devices when possible. -use super::{devids, methods, names, paths}; +use super::aml::{devids, methods, names, paths, *}; use super::{ GPE0_BLK_ADDR, GPE0_BLK_LEN, IO_APIC_ADDR, LOCAL_APIC_ADDR, PCI_LINK_IRQS, PM1A_EVT_BLK_ADDR, SCI_IRQ, @@ -269,12 +269,8 @@ impl Aml for PciRootBridgeCrs { cres.push(&bus_number); // Legacy PCI configuration I/O ports. - let pci_config_io_ports = aml::IO::new( - PCI_CONFIG_IO_BASE, - PCI_CONFIG_IO_BASE, - 1, - PCI_CONFIG_IO_SIZE, - ); + let pci_config_io_ports = + io_port(PCI_CONFIG_IO_BASE, 1, PCI_CONFIG_IO_SIZE); cres.push(&pci_config_io_ports); // I/O ports below the PCI config ports (0x0000-0x0cf7). @@ -698,9 +694,9 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { devids::AT_INT_CONTROLLER, )), &names::crs(&aml::ResourceTemplate::new(vec![ - &aml::IO::new(IO_ICU1, IO_ICU1, 0x00, 0x02), - &aml::IO::new(IO_ICU2, IO_ICU2, 0x00, 0x02), - &aml::IO::new(IO_ELCR1, IO_ELCR1, 0x00, 0x02), + &io_port(IO_ICU1, 0x00, 0x02), + &io_port(IO_ICU2, 0x00, 0x02), + &io_port(IO_ELCR1, 0x00, 0x02), &aml::IrqNoFlags::new(2), ])), ], @@ -717,12 +713,12 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { devids::AT_DMA_CONTROLLER, )), &names::crs(&aml::ResourceTemplate::new(vec![ - &aml::IO::new(0x0000, 0x0000, 0x00, 0x10), - &aml::IO::new(0x0081, 0x0081, 0x00, 0x03), - &aml::IO::new(0x0087, 0x0087, 0x00, 0x01), - &aml::IO::new(0x0089, 0x0089, 0x00, 0x03), - &aml::IO::new(0x008f, 0x008f, 0x00, 0x01), - &aml::IO::new(0x00c0, 0x00c0, 0x00, 0x20), + &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, @@ -738,7 +734,7 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { vec![ &names::hid(&aml::EISAName::new(devids::AT_TIMER)), &names::crs(&aml::ResourceTemplate::new(vec![ - &aml::IO::new(IO_TIMER1, IO_TIMER1, 0x00, 0x04), + &io_port(IO_TIMER1, 0x00, 0x04), &aml::IrqNoFlags::new(0), ])), ], @@ -751,7 +747,7 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { devids::AT_REAL_TIME_CLOCK, )), &names::crs(&aml::ResourceTemplate::new(vec![ - &aml::IO::new(IO_RTC, IO_RTC, 0x00, 0x02), + &io_port(IO_RTC, 0x00, 0x02), &aml::IrqNoFlags::new(8), ])), ], @@ -764,7 +760,7 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { devids::AT_SPEAKER_SOUND, )), &names::crs(&aml::ResourceTemplate::new(vec![ - &aml::IO::new(NMISC_PORT, NMISC_PORT, 0x01, 0x01), + &io_port(NMISC_PORT, 0x01, 0x01), ])), ], ), @@ -780,7 +776,7 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { devids::MATH_COPROCESSOR, )), &names::crs(&aml::ResourceTemplate::new(vec![ - &aml::IO::new(0x00f0, 0x00f0, 0x00, 0x10), + &io_port(0x00f0, 0x00, 0x10), &aml::IrqNoFlags::new(13), ])), ], @@ -798,38 +794,28 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { &names::hid(&aml::EISAName::new(devids::GENERAL_ID)), &names::uid(&aml::ONE), &names::crs(&aml::ResourceTemplate::new(vec![ - &aml::IO::new(0x0010, 0x0010, 0x00, 0x10), - &aml::IO::new(0x0022, 0x0022, 0x00, 0x1e), - &aml::IO::new(0x0044, 0x0044, 0x00, 0x1c), - &aml::IO::new(0x0062, 0x0062, 0x00, 0x02), - &aml::IO::new(0x0065, 0x0065, 0x00, 0x0b), - &aml::IO::new(0x0072, 0x0072, 0x00, 0x0e), - &aml::IO::new(0x0080, 0x0080, 0x00, 0x01), - &aml::IO::new(0x0084, 0x0084, 0x00, 0x03), - &aml::IO::new(0x0088, 0x0088, 0x00, 0x01), - &aml::IO::new(0x008c, 0x008c, 0x00, 0x03), - &aml::IO::new(0x0090, 0x0090, 0x00, 0x10), - &aml::IO::new(0x00a2, 0x00a2, 0x00, 0x1e), - &aml::IO::new(0x00e0, 0x00e0, 0x00, 0x10), - &aml::IO::new(0x01e0, 0x01e0, 0x00, 0x10), - &aml::IO::new(0x0160, 0x0160, 0x00, 0x10), - &aml::IO::new(0x0370, 0x0370, 0x00, 0x02), - &aml::IO::new(0x0402, 0x0402, 0x00, 0x01), - &aml::IO::new(0x0440, 0x0440, 0x00, 0x10), + &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(0x0402, 0x00, 0x01), + &io_port(0x0440, 0x00, 0x10), // QEMU GPE0 BLK. - &aml::IO::new( - GPE0_BLK_ADDR, - GPE0_BLK_ADDR, - 0x00, - GPE0_BLK_LEN, - ), + &io_port(GPE0_BLK_ADDR, 0x00, GPE0_BLK_LEN), // PMBLK1. - &aml::IO::new( - PM1A_EVT_BLK_ADDR, - PM1A_EVT_BLK_ADDR, - 0x00, - 0x40, - ), + &io_port(PM1A_EVT_BLK_ADDR, 0x00, 0x40), // IO APIC. &aml::Memory32Fixed::new( false, @@ -859,7 +845,7 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { vec![ &names::hid(&devids::QEMU_PVPANIC), &names::crs(&aml::ResourceTemplate::new(vec![ - &aml::IO::new(0x0505, 0x0505, 0x01, 0x01), + &io_port(0x0505, 0x01, 0x01), ])), &aml::OpRegion::new( "PEOR".into(), diff --git a/lib/propolis/src/firmware/acpi/mod.rs b/lib/propolis/src/firmware/acpi/mod.rs index 0bae0cd29..e46f931a7 100644 --- a/lib/propolis/src/firmware/acpi/mod.rs +++ b/lib/propolis/src/firmware/acpi/mod.rs @@ -4,6 +4,7 @@ //! ACPI table and AML bytecode generation. +pub mod aml; pub mod dsdt; pub mod facs; pub mod fadt; @@ -53,173 +54,3 @@ const PM1A_EVT_BLK_ADDR: u16 = 0xb000; const GPE0_BLK_ADDR: u16 = 0xafe0; const GPE0_BLK_LEN: u8 = 4; - -/// 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/hw/ps2/ctrl.rs b/lib/propolis/src/hw/ps2/ctrl.rs index 4c7632567..a7ad9cbe8 100644 --- a/lib/propolis/src/hw/ps2/ctrl.rs +++ b/lib/propolis/src/hw/ps2/ctrl.rs @@ -1106,25 +1106,15 @@ impl Aml for PS2Ctrl { aml::Device::new( "PS2K".into(), vec![ - &acpi::names::hid(&aml::EISAName::new( - acpi::devids::IBM_ENHANCED_KEYBOARD, + &acpi::aml::names::hid(&aml::EISAName::new( + acpi::aml::devids::IBM_ENHANCED_KEYBOARD, )), - &acpi::names::cid(&aml::EISAName::new( - acpi::devids::MICROSOFT_RESERVED_KEYBOARD, + &acpi::aml::names::cid(&aml::EISAName::new( + acpi::aml::devids::MICROSOFT_RESERVED_KEYBOARD, )), - &acpi::names::crs(&aml::ResourceTemplate::new(vec![ - &aml::IO::new( - ibmpc::PORT_PS2_DATA, - ibmpc::PORT_PS2_DATA, - 0x00, - 0x01, - ), - &aml::IO::new( - ibmpc::PORT_PS2_CMD_STATUS, - ibmpc::PORT_PS2_CMD_STATUS, - 0x00, - 0x01, - ), + &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), ])), ], diff --git a/lib/propolis/src/hw/uart/lpc.rs b/lib/propolis/src/hw/uart/lpc.rs index 961c55ded..c99ed7aa5 100644 --- a/lib/propolis/src/hw/uart/lpc.rs +++ b/lib/propolis/src/hw/uart/lpc.rs @@ -242,13 +242,13 @@ impl Aml for LpcUart { aml::Device::new( aml::Path::new(&format!("UAR{}", uid)), vec![ - &acpi::names::hid(&aml::EISAName::new( - acpi::devids::COM_PORT_16550A, + &acpi::aml::names::hid(&aml::EISAName::new( + acpi::aml::devids::COM_PORT_16550A, )), - &acpi::names::ddn(&self.name), - &acpi::names::uid(&uid), - &acpi::names::crs(&aml::ResourceTemplate::new(vec![ - &aml::IO::new(port, port, 1, REGISTER_LEN as u8), + &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), ])), ], From 48a2dc1b54e8c1ab2b13f6ad3785440ddbeb07f7 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Fri, 22 May 2026 22:35:13 +0000 Subject: [PATCH 35/47] acpi: add historical note about ACPI tables --- lib/propolis/src/firmware/acpi/mod.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/propolis/src/firmware/acpi/mod.rs b/lib/propolis/src/firmware/acpi/mod.rs index e46f931a7..b7e57bf15 100644 --- a/lib/propolis/src/firmware/acpi/mod.rs +++ b/lib/propolis/src/firmware/acpi/mod.rs @@ -3,6 +3,20 @@ // 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 pub mod aml; pub mod dsdt; From a5d716fd474699c236bf221fa2b0821ea1895993 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Fri, 22 May 2026 23:44:45 +0000 Subject: [PATCH 36/47] acpi: reuse existing IO port values --- lib/propolis/src/firmware/acpi/dsdt.rs | 36 ++++++++++++++++---------- lib/propolis/src/hw/qemu/debug.rs | 2 +- lib/propolis/src/hw/qemu/pvpanic.rs | 8 +++--- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/lib/propolis/src/firmware/acpi/dsdt.rs b/lib/propolis/src/firmware/acpi/dsdt.rs index 9cc08676d..e5c94849c 100644 --- a/lib/propolis/src/firmware/acpi/dsdt.rs +++ b/lib/propolis/src/firmware/acpi/dsdt.rs @@ -31,6 +31,7 @@ use super::{ GPE0_BLK_ADDR, GPE0_BLK_LEN, IO_APIC_ADDR, LOCAL_APIC_ADDR, PCI_LINK_IRQS, PM1A_EVT_BLK_ADDR, SCI_IRQ, }; +use crate::hw::{pci, qemu}; use acpi_tables::{aml, sdt::Sdt, Aml, AmlSink}; // The DSDT and SSDT OEM ID, OEM table ID, and OEM table revision are @@ -218,12 +219,6 @@ impl<'a> Aml for PciRootBridge<'a> { } } -/// I/O port range for PCI configuration. -/// -/// -const PCI_CONFIG_IO_BASE: u16 = 0x0cf8; -const PCI_CONFIG_IO_SIZE: u8 = 8; - /// Bus number range for the PCI0 root bridge. const PCI_BUS_START: u16 = 0x00; const PCI_BUS_END: u16 = 0xff; @@ -269,18 +264,27 @@ impl Aml for PciRootBridgeCrs { cres.push(&bus_number); // Legacy PCI configuration I/O ports. - let pci_config_io_ports = - io_port(PCI_CONFIG_IO_BASE, 1, PCI_CONFIG_IO_SIZE); + 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_CONFIG_IO_BASE - 1, None); + 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_CONFIG_IO_BASE + PCI_CONFIG_IO_SIZE as u16, + pci::bits::PORT_PCI_CONFIG_ADDR + + pci::bits::LEN_PCI_CONFIG_ADDR + + pci::bits::LEN_PCI_CONFIG_DATA, 0xffff, None, ); @@ -810,7 +814,11 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { &io_port(0x01e0, 0x00, 0x10), &io_port(0x0160, 0x00, 0x10), &io_port(0x0370, 0x00, 0x02), - &io_port(0x0402, 0x00, 0x01), + &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), @@ -845,12 +853,12 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { vec![ &names::hid(&devids::QEMU_PVPANIC), &names::crs(&aml::ResourceTemplate::new(vec![ - &io_port(0x0505, 0x01, 0x01), + &io_port(qemu::pvpanic::IOPORT, 0x01, 0x01), ])), &aml::OpRegion::new( "PEOR".into(), aml::OpRegionSpace::SystemIO, - &0x0505_u64, + &qemu::pvpanic::IOPORT, &aml::ONE, ), &aml::Field::new( 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/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. From 71c8e6faef8ea702356d642a02de5a1c490e245f Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Fri, 22 May 2026 23:51:34 +0000 Subject: [PATCH 37/47] acpi: use SERIALIZED and NOT_SERIALIZED constants The boolean argument of `aml::Method::new()` is not descriptive enough to indicate what serialized and not serialized means in this context. --- lib/propolis/src/firmware/acpi/aml.rs | 13 +++++++++++ lib/propolis/src/firmware/acpi/dsdt.rs | 32 +++++++++++++++----------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/lib/propolis/src/firmware/acpi/aml.rs b/lib/propolis/src/firmware/acpi/aml.rs index 4805e4ffd..ae5ee6349 100644 --- a/lib/propolis/src/firmware/acpi/aml.rs +++ b/lib/propolis/src/firmware/acpi/aml.rs @@ -17,6 +17,19 @@ 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 diff --git a/lib/propolis/src/firmware/acpi/dsdt.rs b/lib/propolis/src/firmware/acpi/dsdt.rs index e5c94849c..9d3976e04 100644 --- a/lib/propolis/src/firmware/acpi/dsdt.rs +++ b/lib/propolis/src/firmware/acpi/dsdt.rs @@ -352,7 +352,7 @@ impl Aml for PciRootBridgeCrs { // devices. methods::crs( 0, - true, + SERIALIZED, vec![ // Create references to values in the FWDT OperationRegion. &aml::Field::new( @@ -541,7 +541,7 @@ impl Aml for PciRootBridgePrt { methods::prt( 0, - false, + NOT_SERIALIZED, vec![&aml::Return::new(&aml::Package::new(ptr))], ) .to_aml_bytes(sink); @@ -625,7 +625,7 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { &aml::Method::new( "PSTA".into(), 1, - false, + NOT_SERIALIZED, vec![ &aml::If::new( &aml::And::new(&aml::ZERO, &aml::Arg(0), &0x80_u64), @@ -638,7 +638,7 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { &aml::Method::new( "PCRS".into(), 1, - true, + SERIALIZED, vec![ &aml::Name::new( "BUF0".into(), @@ -872,7 +872,7 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { &aml::Method::new( "RDPT".into(), 0, - false, + NOT_SERIALIZED, vec![ &aml::Store::new( &aml::Local(0), @@ -884,7 +884,7 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { &aml::Method::new( "WRPT".into(), 1, - false, + NOT_SERIALIZED, vec![&aml::Store::new( &aml::Path::new("PEPT"), &aml::Arg(0), @@ -920,12 +920,16 @@ impl<'a> Lnk<'a> { &names::hid(&aml::EISAName::new(devids::PCI_INT_LINK)), &names::uid(&aml::ZERO), &names::sta(&0x0b_u64), - &methods::srs(1, false, vec![]), - &methods::dis(0, false, vec![]), + &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, false, vec![&aml::Return::new(&paths::prs())]), + &methods::crs( + 0, + NOT_SERIALIZED, + vec![&aml::Return::new(&paths::prs())], + ), ], ) .to_aml_bytes(sink); @@ -951,7 +955,7 @@ impl<'a> Aml for Lnk<'a> { &names::uid(&self.uid), &methods::sta( 0, - false, + NOT_SERIALIZED, vec![&aml::Return::new(&aml::MethodCall::new( "PSTA".into(), vec![&pir], @@ -959,12 +963,12 @@ impl<'a> Aml for Lnk<'a> { ), &methods::dis( 0, - false, + NOT_SERIALIZED, vec![&aml::Or::new(&pir, &pir, &0x80_u64)], ), &methods::crs( 0, - false, + NOT_SERIALIZED, vec![&aml::Return::new(&aml::MethodCall::new( "PCRS".into(), vec![&pir], @@ -972,7 +976,7 @@ impl<'a> Aml for Lnk<'a> { ), &methods::prs( 0, - false, + NOT_SERIALIZED, vec![&aml::Return::new(&aml::MethodCall::new( "PPRS".into(), vec![], @@ -980,7 +984,7 @@ impl<'a> Aml for Lnk<'a> { ), &methods::srs( 1, - false, + NOT_SERIALIZED, vec![ &aml::CreateDWordField::new( &aml::Path::new("IRQW"), From 9f29190f1c3d98378278917a54a6f0fd013208cf Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Sat, 23 May 2026 00:00:09 +0000 Subject: [PATCH 38/47] acpi: expand on MMIO32 and MMIO64 range values --- lib/propolis/src/firmware/acpi/dsdt.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/propolis/src/firmware/acpi/dsdt.rs b/lib/propolis/src/firmware/acpi/dsdt.rs index 9d3976e04..8980cb7a4 100644 --- a/lib/propolis/src/firmware/acpi/dsdt.rs +++ b/lib/propolis/src/firmware/acpi/dsdt.rs @@ -308,8 +308,11 @@ impl Aml for PciRootBridgeCrs { let mmio32 = aml::AddressSpace::new_memory( aml::AddressSpaceCacheable::NotCacheable, true, - 0xf800_0000_u32, // Value overwritten by _CRS. - 0xfffb_ffff_u32, // Value overwritten by _CRS. + // 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); @@ -332,8 +335,11 @@ impl Aml for PciRootBridgeCrs { let mmio64 = aml::AddressSpace::new_memory( aml::AddressSpaceCacheable::Cacheable, true, - 0x0080_0000_0000_u64, // Value overwritten by _CRS. - 0x0fff_ffff_ffff_u64, // Value overwritten by _CRS. + // 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); From 02bf8cd1fd23882abd438b152c7bff5ade84e783 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Mon, 25 May 2026 19:36:53 +0000 Subject: [PATCH 39/47] acpi: update lpc.rs --- lib/propolis/src/hw/uart/lpc.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/propolis/src/hw/uart/lpc.rs b/lib/propolis/src/hw/uart/lpc.rs index c99ed7aa5..b3701afc4 100644 --- a/lib/propolis/src/hw/uart/lpc.rs +++ b/lib/propolis/src/hw/uart/lpc.rs @@ -222,10 +222,14 @@ impl acpi::DsdtGenerator for LpcUart { } 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 => return, // Device is not attached to any port. + None => panic!("expected UART device to be connected"), }; #[allow(clippy::wildcard_in_or_patterns)] @@ -233,8 +237,6 @@ impl Aml for LpcUart { "COM1" => 1, "COM2" => 2, "COM3" | "COM4" | _ => { - // XXX(acpi): COM3 and COM4 are also attached to the instance - // but the original EDK2 static tables didn't include them. return; } }; From b82be28f65f12201ef69e14b47041a48e51c745c Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Tue, 26 May 2026 01:34:26 +0000 Subject: [PATCH 40/47] acpi: update fadt.rs --- lib/propolis/src/firmware/acpi/fadt.rs | 35 +++++++++++++++++--------- lib/propolis/src/hw/pci/bits.rs | 11 ++++++++ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/lib/propolis/src/firmware/acpi/fadt.rs b/lib/propolis/src/firmware/acpi/fadt.rs index 8bab1e17d..9273b652f 100644 --- a/lib/propolis/src/firmware/acpi/fadt.rs +++ b/lib/propolis/src/firmware/acpi/fadt.rs @@ -11,11 +11,11 @@ use super::{ GPE0_BLK_ADDR, GPE0_BLK_LEN, OEM_ID, OEM_REVISION, OEM_TABLE_ID, PM1A_EVT_BLK_ADDR, SCI_IRQ, }; +use crate::hw::pci; use acpi_tables::{ - // XXX(acpi): 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. + // 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}, @@ -71,12 +71,16 @@ impl Fadt { } } -// XXX(acpi): Values retained from the original EDK2 static tables. -// fwts reports 1 high failure for this table: -// - fadt: FADT X_GPE0_BLK Access width 0x00 but it should be 1 (byte access). -// -// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiTables/Platform.h#L25-L56 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) @@ -90,6 +94,9 @@ impl Aml for Fadt { .flag(Flags::ResetRegSup); fadt.sci_int = (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; @@ -104,8 +111,12 @@ impl Aml for Fadt { fadt.pm_tmr_len = PM_TMR_BLK_LEN; fadt.gpe0_blk_len = GPE0_BLK_LEN; - // Disable C2 and C3 state support. + // 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(); @@ -116,9 +127,9 @@ impl Aml for Fadt { u8::BITS as u8, 0, AccessSize::Undefined, - 0x0cf9, + pci::bits::PORT_ACPI_RESET_ADDR, ); - fadt.reset_value = 0x06; + fadt.reset_value = pci::bits::PORT_ACPI_RESET_VALUE; fadt.x_pm1a_evt_blk = GAS::new( AddressSpace::SystemIo, diff --git a/lib/propolis/src/hw/pci/bits.rs b/lib/propolis/src/hw/pci/bits.rs index e0b6f5c24..7b2b3a39e 100644 --- a/lib/propolis/src/hw/pci/bits.rs +++ b/lib/propolis/src/hw/pci/bits.rs @@ -78,6 +78,17 @@ 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 current value inherited from the original +/// EDK2 static ACPI tables fall within the PCI configuration address range. +/// +/// Refer to ACPI sections 5.2.9 "Fixed ACPI Description Table (FADT)" and +/// 4.8.4.6 "Reset Register", and the `native_machine_emergency_restart()` and +/// `acpi_reboot()` functions in the Linux source code for more information. +pub const PORT_ACPI_RESET_ADDR: u64 = 0xcf9; +pub const PORT_ACPI_RESET_VALUE: u8 = 0x06; + /// 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). From 1e97c1cba6a8f21a59eb7e9ebd80ee76ce831fef Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Tue, 26 May 2026 01:52:19 +0000 Subject: [PATCH 41/47] acpi: expand doc on ACPI reset register --- lib/propolis/src/hw/pci/bits.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/propolis/src/hw/pci/bits.rs b/lib/propolis/src/hw/pci/bits.rs index 7b2b3a39e..624caa5c2 100644 --- a/lib/propolis/src/hw/pci/bits.rs +++ b/lib/propolis/src/hw/pci/bits.rs @@ -80,14 +80,17 @@ pub const LEN_PCI_CONFIG_DATA: u16 = 4; /// I/O port used for the ACPI reset register. /// -/// Although not PCI-specific, the current value inherited from the original -/// EDK2 static ACPI tables fall within the PCI configuration address range. +/// 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 ACPI sections 5.2.9 "Fixed ACPI Description Table (FADT)" and -/// 4.8.4.6 "Reset Register", and the `native_machine_emergency_restart()` and -/// `acpi_reboot()` functions in the Linux source code for more information. +/// 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 = 0x06; +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 From b70a2c82e9e578375e751b02b65e098bdd92f80a Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Tue, 26 May 2026 14:07:04 +0000 Subject: [PATCH 42/47] acpi: add note about high vCPU count --- lib/propolis/src/firmware/acpi/madt.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/propolis/src/firmware/acpi/madt.rs b/lib/propolis/src/firmware/acpi/madt.rs index 860c4a709..541b12df1 100644 --- a/lib/propolis/src/firmware/acpi/madt.rs +++ b/lib/propolis/src/firmware/acpi/madt.rs @@ -91,6 +91,9 @@ impl<'a> Aml for Madt<'a> { // 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, From 99bf8c030b52d6cf65e0ea10df9bd8b9c0dff894 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Tue, 26 May 2026 18:29:31 +0000 Subject: [PATCH 43/47] acpi: reuse more existing values and improve docs --- lib/propolis/src/firmware/acpi/dsdt.rs | 32 ++++++++++++++++---------- lib/propolis/src/firmware/acpi/fadt.rs | 21 ++++++++--------- lib/propolis/src/firmware/acpi/mod.rs | 24 ++++++++++++++----- lib/propolis/src/hw/chipset/i440fx.rs | 14 +++++------ 4 files changed, 54 insertions(+), 37 deletions(-) diff --git a/lib/propolis/src/firmware/acpi/dsdt.rs b/lib/propolis/src/firmware/acpi/dsdt.rs index 8980cb7a4..79a40dd02 100644 --- a/lib/propolis/src/firmware/acpi/dsdt.rs +++ b/lib/propolis/src/firmware/acpi/dsdt.rs @@ -28,10 +28,10 @@ use super::aml::{devids, methods, names, paths, *}; use super::{ - GPE0_BLK_ADDR, GPE0_BLK_LEN, IO_APIC_ADDR, LOCAL_APIC_ADDR, PCI_LINK_IRQS, - PM1A_EVT_BLK_ADDR, SCI_IRQ, + GPE0_BLK_ADDR, GPE0_BLK_LEN, IO_APIC_ADDR, IO_APIC_LEN, LOCAL_APIC_ADDR, + LOCAL_APIC_LEN, PCI_LINK_IRQS, }; -use crate::hw::{pci, qemu}; +use crate::hw::{chipset::i440fx, pci, qemu}; use acpi_tables::{aml, sdt::Sdt, Aml, AmlSink}; // The DSDT and SSDT OEM ID, OEM table ID, and OEM table revision are @@ -612,8 +612,8 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { &aml::OpRegion::new( "PRR0".into(), aml::OpRegionSpace::PCIConfig, - &0x60_u64, - &0x04_u64, + &i440fx::PIR_OFFSET, + &i440fx::PIR_LEN, ), &aml::Field::new( "PRR0".into(), @@ -634,7 +634,11 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { NOT_SERIALIZED, vec![ &aml::If::new( - &aml::And::new(&aml::ZERO, &aml::Arg(0), &0x80_u64), + &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)]), @@ -667,7 +671,7 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { &aml::LogicalNot::new(&aml::And::new( &aml::ZERO, &aml::Arg(0), - &0x80_u64, + &i440fx::PIR_MASK_DISABLE, )), vec![&aml::Store::new( &aml::Path::new("IRQW"), @@ -687,7 +691,7 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { true, PCI_LINK_IRQS .iter() - .filter(|i| **i != SCI_IRQ) // The SCI has special handling LNKS. + .filter(|i| **i != i440fx::SCI_IRQ) // The SCI has special handling LNKS. .map(|i| *i as u32) .collect(), )]), @@ -829,18 +833,22 @@ impl<'a> Aml for PciRootBridgeLpc<'a> { // QEMU GPE0 BLK. &io_port(GPE0_BLK_ADDR, 0x00, GPE0_BLK_LEN), // PMBLK1. - &io_port(PM1A_EVT_BLK_ADDR, 0x00, 0x40), + &io_port( + i440fx::PMBASE_DEFAULT, + 0x00, + i440fx::PMBASE_LEN as u8, + ), // IO APIC. &aml::Memory32Fixed::new( false, IO_APIC_ADDR, - 0x0000_1000, + IO_APIC_LEN, ), // LAPIC. &aml::Memory32Fixed::new( false, LOCAL_APIC_ADDR, - 0x0010_0000, + LOCAL_APIC_LEN, ), ])), ], @@ -970,7 +978,7 @@ impl<'a> Aml for Lnk<'a> { &methods::dis( 0, NOT_SERIALIZED, - vec![&aml::Or::new(&pir, &pir, &0x80_u64)], + vec![&aml::Or::new(&pir, &pir, &i440fx::PIR_MASK_DISABLE)], ), &methods::crs( 0, diff --git a/lib/propolis/src/firmware/acpi/fadt.rs b/lib/propolis/src/firmware/acpi/fadt.rs index 9273b652f..8c2e63e00 100644 --- a/lib/propolis/src/firmware/acpi/fadt.rs +++ b/lib/propolis/src/firmware/acpi/fadt.rs @@ -7,11 +7,8 @@ //! 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, - PM1A_EVT_BLK_ADDR, SCI_IRQ, -}; -use crate::hw::pci; +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 @@ -36,8 +33,8 @@ 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: u32 = 0xb004; -const PM_TMR_BLK_ADDR: u32 = 0xb008; +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; @@ -93,7 +90,7 @@ impl Aml for Fadt { .flag(Flags::TmrValExt) .flag(Flags::ResetRegSup); - fadt.sci_int = (SCI_IRQ as u16).into(); + 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. @@ -101,9 +98,9 @@ impl Aml for Fadt { fadt.acpi_enable = 0xf1; fadt.acpi_disable = 0xf0; - fadt.pm1a_evt_blk = (PM1A_EVT_BLK_ADDR as u32).into(); - fadt.pm1a_cnt_blk = PM1A_CNT_BLK_ADDR.into(); - fadt.pm_tmr_blk = PM_TMR_BLK_ADDR.into(); + 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; @@ -136,7 +133,7 @@ impl Aml for Fadt { PM1A_EVT_BLK_LEN * 8, 0, AccessSize::Undefined, - PM1A_EVT_BLK_ADDR as u64, + i440fx::PMBASE_DEFAULT as u64, ); fadt.x_pm1a_cnt_blk = GAS::new( AddressSpace::SystemIo, diff --git a/lib/propolis/src/firmware/acpi/mod.rs b/lib/propolis/src/firmware/acpi/mod.rs index b7e57bf15..e3a41165b 100644 --- a/lib/propolis/src/firmware/acpi/mod.rs +++ b/lib/propolis/src/firmware/acpi/mod.rs @@ -18,6 +18,8 @@ //! //! 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; @@ -52,19 +54,29 @@ pub const TABLE_HEADER_CHECKSUM_LEN: usize = 1; // Internal values shared across tables. -// XXX(acpi): Values inherited from the original EDK2 static tables. They could -// be set to Propolis-specific values in the future. +// 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; -const SCI_IRQ: u8 = 0x09; -const PCI_LINK_IRQS: [u8; 4] = [0x05, SCI_IRQ, 0x0a, 0x0b]; +// 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; -const LOCAL_APIC_ADDR: u32 = 0xfee0_0000; +// VIOAPIC_SIZE in bhyve. +const IO_APIC_LEN: u32 = 0x1000; -const PM1A_EVT_BLK_ADDR: u16 = 0xb000; +// 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/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; From 75fd2621ed10620ad8b502af26ddd3485d238a28 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Tue, 26 May 2026 01:00:39 +0000 Subject: [PATCH 44/47] acpi: update fwcfg.rs Document that the `PciWindow` range has inclusive limits and fix `PciWindow::new()` validation to support 1 byte length windows. --- lib/propolis/src/hw/qemu/fwcfg.rs | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/propolis/src/hw/qemu/fwcfg.rs b/lib/propolis/src/hw/qemu/fwcfg.rs index e18777790..3109a8cd3 100644 --- a/lib/propolis/src/hw/qemu/fwcfg.rs +++ b/lib/propolis/src/hw/qemu/fwcfg.rs @@ -1320,7 +1320,7 @@ pub mod formats { pub dsdt_generators: &'a [&'a dyn acpi::DsdtGenerator], } - /// A range of address to be used for PCI MMIO. + /// An inclusive range of address to be used for PCI MMIO. #[derive(PartialEq)] pub struct PciWindow { base: u64, @@ -1328,7 +1328,7 @@ pub mod formats { } impl PciWindow { pub fn new(base: u64, end: u64) -> Result { - if base >= end { + if base > end { return Err(AcpiTablesError::InvalidPCIWindowRange(base, end)); } Ok(Self { base, end }) @@ -1348,6 +1348,26 @@ pub mod formats { } } + #[cfg(test)] + mod test_pci_window { + use super::*; + + #[test] + fn basic() { + let w = PciWindow::new(0, 0).unwrap(); + assert_eq!(w.len(), 0); + + let w = PciWindow::empty(); + assert_eq!(w.len(), 0); + + let w = PciWindow::new(0, 100).unwrap(); + assert_eq!(w.len(), 101); + + let w = PciWindow::new(100, 100).unwrap(); + assert_eq!(w.len(), 1); + } + } + /// The resulting values to be loaded into QEMU fw_cfg data. pub struct AcpiTables { pub tables: Entry, @@ -1631,7 +1651,7 @@ pub mod formats { checksum_offset + acpi::TABLE_HEADER_CHECKSUM_LEN; // Zero existing checksum so it doesn't affect the new value. - self.rsdp[checksum_offset..checksum_end].copy_from_slice(&[0_u8]); + self.rsdp[checksum_offset..checksum_end].fill(0); self.loader.add_checksum( FW_CFG_ACPI_RSDP_PATH, checksum_offset as u32, @@ -1654,7 +1674,7 @@ pub mod formats { + acpi::TABLE_HEADER_CHECKSUM_LEN; // Zero existing checksum so it doesn't affect the new value. - self.tables[checksum_start..checksum_end].copy_from_slice(&[0u8]); + self.tables[checksum_start..checksum_end].fill(0); self.loader.add_checksum( FW_CFG_ACPI_TABLES_PATH, checksum_start as u32, From 580ba5a6753fca5703a64aea9df261c9d90f95a1 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Tue, 26 May 2026 20:08:14 +0000 Subject: [PATCH 45/47] deps: use tag for acpi_tables crate --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f72ef7fea..c1dbcbfe6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5,7 +5,7 @@ version = 3 [[package]] name = "acpi_tables" version = "0.2.1" -source = "git+https://github.com/oxidecomputer/acpi_tables.git#a4e63474d63ec81dbaacebebf41aefac743dc045" +source = "git+https://github.com/oxidecomputer/acpi_tables.git?tag=v0.2.1-oxide.1#f20592bc0f2bfe9b71493de67e6b917fcea50c2e" dependencies = [ "zerocopy 0.8.27", ] diff --git a/Cargo.toml b/Cargo.toml index 12889bc66..8536926bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -194,7 +194,7 @@ uuid = "1.3.2" zerocopy = "0.8.25" [patch.crates-io] -acpi_tables = { git = 'https://github.com/oxidecomputer/acpi_tables.git' } +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 From f27192a589be849c105cc6b17a425cff519f7a24 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Tue, 26 May 2026 20:39:02 +0000 Subject: [PATCH 46/47] test: document `acpi_tables_parse` guest image --- phd-tests/tests/src/firmware.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/phd-tests/tests/src/firmware.rs b/phd-tests/tests/src/firmware.rs index bece55645..5dd44c34f 100644 --- a/phd-tests/tests/src/firmware.rs +++ b/phd-tests/tests/src/firmware.rs @@ -158,6 +158,22 @@ async fn acpi_tables_generation(ctx: &TestCtx) { 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 @@ -213,8 +229,7 @@ async fn acpi_tables_parse(ctx: &TestCtx) { .map(|s| s.replace(" ", "")) .collect(); - // XXX(acpi): The current ACPI tables generate (num_cpus + 2) errors and 1 - // warning. + // The current ACPI tables generate (num_cpus + 2) errors and 1 warning. // // Test Failure Summary // ================================================================================ From c37f332750309a92dedf1cd149d24ab16422726e Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Wed, 27 May 2026 02:20:55 +0000 Subject: [PATCH 47/47] acpi: minor updates and expanded docs --- bin/propolis-server/src/lib/initializer.rs | 20 ++++++--- bin/propolis-standalone/src/main.rs | 20 ++++++--- lib/propolis/src/firmware/acpi/aml.rs | 5 +++ lib/propolis/src/firmware/acpi/dsdt.rs | 50 +++++++++------------- lib/propolis/src/firmware/acpi/facs.rs | 8 ++-- lib/propolis/src/firmware/acpi/madt.rs | 11 ++--- lib/propolis/src/hw/qemu/fwcfg.rs | 16 ++++--- 7 files changed, 71 insertions(+), 59 deletions(-) diff --git a/bin/propolis-server/src/lib/initializer.rs b/bin/propolis-server/src/lib/initializer.rs index ef1c181a7..40d809ac3 100644 --- a/bin/propolis-server/src/lib/initializer.rs +++ b/bin/propolis-server/src/lib/initializer.rs @@ -128,9 +128,12 @@ pub enum MachineInitError { const MAX_ROM_SIZE: usize = 0x20_0000; /// End address of the 32-bit PCI MMIO window. -// XXX(acpi): Value inherited from the original EDK2 static tables. It should -// match the actual memory regions registered in the instance. +/// +// 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) { @@ -1449,6 +1452,14 @@ impl MachineInitializer<'_> { .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, @@ -1457,11 +1468,6 @@ impl MachineInitializer<'_> { let config = &fwcfg::formats::AcpiConfig { num_cpus: cpus, pci_window_32, - // XXX(acpi): Value inherited from the original EDK2 static tables, - // where the 64-bit PCI MMIO region was never set. It - // should match the actual memory regions registered in - // the instance. - // https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiPlatformDxe/Qemu.c#L284-L286 pci_window_64: fwcfg::formats::PciWindow::empty(), dsdt_generators: &generators, }; diff --git a/bin/propolis-standalone/src/main.rs b/bin/propolis-standalone/src/main.rs index d3a2731df..7b1d038a2 100644 --- a/bin/propolis-standalone/src/main.rs +++ b/bin/propolis-standalone/src/main.rs @@ -45,9 +45,12 @@ const PAGE_OFFSET: u64 = 0xfff; const MAX_ROM_SIZE: usize = 0x20_0000; /// End address of the 32-bit PCI MMIO window. -// XXX(acpi): Value inherited from the original EDK2 static tables. It should -// match the actual memory regions registered in the instance. +/// +// 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; @@ -1088,6 +1091,14 @@ fn generate_acpi_tables( .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")?; @@ -1095,11 +1106,6 @@ fn generate_acpi_tables( let config = &fwcfg::formats::AcpiConfig { num_cpus: cpus, pci_window_32, - // XXX(acpi): Value inherited from the original EDK2 static tables, - // where the 64-bit PCI MMIO region was never set. It - // should match the actual memory regions registered in - // the instance. - // https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/AcpiPlatformDxe/Qemu.c#L284-L286 pci_window_64: fwcfg::formats::PciWindow::empty(), dsdt_generators: &generators, }; diff --git a/lib/propolis/src/firmware/acpi/aml.rs b/lib/propolis/src/firmware/acpi/aml.rs index ae5ee6349..3a9633304 100644 --- a/lib/propolis/src/firmware/acpi/aml.rs +++ b/lib/propolis/src/firmware/acpi/aml.rs @@ -12,6 +12,11 @@ use acpi_tables::aml; /// 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) diff --git a/lib/propolis/src/firmware/acpi/dsdt.rs b/lib/propolis/src/firmware/acpi/dsdt.rs index 79a40dd02..6da20ed0c 100644 --- a/lib/propolis/src/firmware/acpi/dsdt.rs +++ b/lib/propolis/src/firmware/acpi/dsdt.rs @@ -34,20 +34,23 @@ use super::{ use crate::hw::{chipset::i440fx, pci, qemu}; use acpi_tables::{aml, sdt::Sdt, Aml, AmlSink}; -// The DSDT and SSDT OEM ID, OEM table ID, and OEM table revision are -// currently kept the same as the ones used by the original tables from EDK2. -// They could be updated to Propolis-specific values in the future. -// -// 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. -const DSDT_OEM_ID: [u8; 6] = *b"INTEL "; -const DSDT_OEM_TABLE_ID: [u8; 8] = *b"OVMF "; -const DSDT_OEM_TABLE_REV: u32 = 0x4; +// 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) +} -const SSDT_OEM_ID: [u8; 6] = *b"REDHAT"; -const SSDT_OEM_TABLE_ID: [u8; 8] = *b"OVMF "; -const SSDT_OEM_TABLE_REV: u32 = 0x1; +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)] @@ -93,7 +96,8 @@ impl<'a> Aml for DsdtGeneratorAml<'a> { /// /// 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. +/// 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. @@ -155,14 +159,7 @@ impl<'a> Aml for Dsdt<'a> { .to_aml_bytes(&mut dsdt); // DSDT table. - let mut sdt = Sdt::new( - *b"DSDT", - 36, - 1, - DSDT_OEM_ID, - DSDT_OEM_TABLE_ID, - DSDT_OEM_TABLE_REV, - ); + let mut sdt = dsdt_sdt_edk2_style(); sdt.append_slice(dsdt.as_slice()); sdt.to_aml_bytes(sink); } @@ -1107,14 +1104,7 @@ impl Aml for Ssdt { ) .to_aml_bytes(&mut ssdt); - let mut sdt = Sdt::new( - *b"SSDT", - 36, - 1, - SSDT_OEM_ID, - SSDT_OEM_TABLE_ID, - SSDT_OEM_TABLE_REV, - ); + let mut sdt = ssdt_sdt_edk2_style(); sdt.append_slice(ssdt.as_slice()); sdt.to_aml_bytes(sink); } diff --git a/lib/propolis/src/firmware/acpi/facs.rs b/lib/propolis/src/firmware/acpi/facs.rs index 5bf804121..6a6bb6a36 100644 --- a/lib/propolis/src/firmware/acpi/facs.rs +++ b/lib/propolis/src/firmware/acpi/facs.rs @@ -20,10 +20,10 @@ impl Facs { } } -// XXX(acpi): 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. +// 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/madt.rs b/lib/propolis/src/firmware/acpi/madt.rs index 541b12df1..41114fbd8 100644 --- a/lib/propolis/src/firmware/acpi/madt.rs +++ b/lib/propolis/src/firmware/acpi/madt.rs @@ -38,14 +38,15 @@ impl<'a> Madt<'a> { } } -// XXX(acpi): Values retained from the original EDK2 static tables. -// 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 +// 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( diff --git a/lib/propolis/src/hw/qemu/fwcfg.rs b/lib/propolis/src/hw/qemu/fwcfg.rs index 3109a8cd3..5faf87f8f 100644 --- a/lib/propolis/src/hw/qemu/fwcfg.rs +++ b/lib/propolis/src/hw/qemu/fwcfg.rs @@ -1328,7 +1328,11 @@ pub mod formats { } impl PciWindow { pub fn new(base: u64, end: u64) -> Result { - if base > end { + // 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 }) @@ -1354,17 +1358,17 @@ pub mod formats { #[test] fn basic() { - let w = PciWindow::new(0, 0).unwrap(); - assert_eq!(w.len(), 0); + 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); - - let w = PciWindow::new(100, 100).unwrap(); - assert_eq!(w.len(), 1); } }