From 1b0f52470692f25c3b8445db356ac07cea4d3932 Mon Sep 17 00:00:00 2001 From: Amey Narkhede Date: Mon, 29 Dec 2025 00:41:38 +0000 Subject: [PATCH 01/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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 } }