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 ee7ebdac6..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")?; @@ -393,16 +402,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 +917,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()); @@ -1156,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); + } + } } } @@ -1270,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 507284faf..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. + } } } @@ -1139,10 +1152,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 +1189,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"); @@ -1375,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/lib/propolis/src/firmware/acpi/aml.rs b/lib/propolis/src/firmware/acpi/aml.rs new file mode 100644 index 000000000..e021c4078 --- /dev/null +++ b/lib/propolis/src/firmware/acpi/aml.rs @@ -0,0 +1,674 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use super::names::{encode_name_string, EisaId, UUID_SIZE}; +use super::opcodes::*; + +pub trait AmlWriter { + fn write_aml(&self, buf: &mut Vec); +} + +impl AmlWriter for u8 { + fn write_aml(&self, buf: &mut Vec) { + match *self { + 0 => buf.push(ZERO_OP), + 1 => buf.push(ONE_OP), + v => { + buf.push(BYTE_PREFIX); + buf.push(v); + } + } + } +} + +impl AmlWriter for u16 { + fn write_aml(&self, buf: &mut Vec) { + if *self <= u8::MAX as u16 { + (*self as u8).write_aml(buf); + } else { + buf.push(WORD_PREFIX); + buf.extend_from_slice(&self.to_le_bytes()); + } + } +} + +impl AmlWriter for u32 { + fn write_aml(&self, buf: &mut Vec) { + if *self <= u16::MAX as u32 { + (*self as u16).write_aml(buf); + } else { + buf.push(DWORD_PREFIX); + buf.extend_from_slice(&self.to_le_bytes()); + } + } +} + +impl AmlWriter for u64 { + fn write_aml(&self, buf: &mut Vec) { + if *self <= u32::MAX as u64 { + (*self as u32).write_aml(buf); + } else { + buf.push(QWORD_PREFIX); + buf.extend_from_slice(&self.to_le_bytes()); + } + } +} + +impl AmlWriter for &str { + fn write_aml(&self, buf: &mut Vec) { + buf.push(STRING_PREFIX); + buf.extend_from_slice(self.as_bytes()); + buf.push(0); + } +} + +impl AmlWriter for String { + fn write_aml(&self, buf: &mut Vec) { + self.as_str().write_aml(buf); + } +} + +impl AmlWriter for EisaId { + fn write_aml(&self, buf: &mut Vec) { + buf.push(DWORD_PREFIX); + buf.extend_from_slice(&self.0.to_le_bytes()); + } +} + +impl AmlWriter for Vec { + fn write_aml(&self, buf: &mut Vec) { + write_buffer(buf, self); + } +} + +impl AmlWriter for &[u8] { + fn write_aml(&self, buf: &mut Vec) { + write_buffer(buf, self); + } +} + +fn write_buffer(buf: &mut Vec, data: &[u8]) { + buf.push(BUFFER_OP); + + let mut size_buf = Vec::new(); + (data.len() as u64).write_aml(&mut size_buf); + + write_pkg_length(buf, size_buf.len() + data.len()); + buf.extend_from_slice(&size_buf); + buf.extend_from_slice(data); +} + +fn encode_pkg_length(total_len: usize) -> ([u8; MAX_PKG_LENGTH_BYTES], usize) { + let mut bytes = [0u8; MAX_PKG_LENGTH_BYTES]; + let size = pkg_length_size(total_len.saturating_sub(1)); + match size { + 1 => bytes[0] = total_len as u8, + 2 => { + bytes[0] = 0x40 | ((total_len & 0x0F) as u8); + bytes[1] = ((total_len >> 4) & 0xFF) as u8; + } + 3 => { + bytes[0] = 0x80 | ((total_len & 0x0F) as u8); + bytes[1] = ((total_len >> 4) & 0xFF) as u8; + bytes[2] = ((total_len >> 12) & 0xFF) as u8; + } + 4 => { + bytes[0] = 0xC0 | ((total_len & 0x0F) as u8); + bytes[1] = ((total_len >> 4) & 0xFF) as u8; + bytes[2] = ((total_len >> 12) & 0xFF) as u8; + bytes[3] = ((total_len >> 20) & 0xFF) as u8; + } + _ => unreachable!(), + } + (bytes, size) +} + +fn write_pkg_length(buf: &mut Vec, content_len: usize) { + let pkg_size = pkg_length_size(content_len); + let (bytes, size) = encode_pkg_length(pkg_size + content_len); + buf.extend_from_slice(&bytes[..size]); +} + +fn pkg_length_size(content_len: usize) -> usize { + if content_len < 0x3F - 1 { + 1 + } else if content_len < 0xFFF - 2 { + 2 + } else if content_len < 0xFFFFF - 3 { + 3 + } else { + 4 + } +} + +const MAX_PKG_LENGTH_BYTES: usize = 4; + +#[must_use = "call .finish() to get the AML bytes"] +pub struct AmlBuilder { + buf: Vec, +} + +impl AmlBuilder { + pub fn new() -> Self { + Self { buf: Vec::new() } + } + + pub fn scope(&mut self, name: &str) -> ScopeGuard<'_> { + ScopeGuard::new(self, name) + } + + pub fn device(&mut self, name: &str) -> DeviceGuard<'_> { + DeviceGuard::new(self, name) + } + + pub fn method( + &mut self, + name: &str, + arg_count: u8, + serialized: bool, + ) -> MethodGuard<'_> { + MethodGuard::new(self, name, arg_count, serialized) + } + + pub fn name(&mut self, name: &str, value: &T) { + self.buf.push(NAME_OP); + encode_name_string(name, &mut self.buf); + value.write_aml(&mut self.buf); + } + + pub fn name_package(&mut self, name: &str, elements: &[T]) { + self.buf.push(NAME_OP); + encode_name_string(name, &mut self.buf); + write_package(&mut self.buf, elements); + } + + pub fn raw(&mut self, bytes: &[u8]) { + self.buf.extend_from_slice(bytes); + } + + pub fn return_value(&mut self, value: &T) { + self.buf.push(RETURN_OP); + value.write_aml(&mut self.buf); + } + + pub fn finish(self) -> Vec { + self.buf + } + + pub fn len(&self) -> usize { + self.buf.len() + } + + pub fn is_empty(&self) -> bool { + self.buf.is_empty() + } + + pub fn as_bytes(&self) -> &[u8] { + &self.buf + } +} + +impl Default for AmlBuilder { + fn default() -> Self { + Self::new() + } +} + +fn write_package(buf: &mut Vec, elements: &[T]) { + buf.push(PACKAGE_OP); + + let mut content = Vec::new(); + content.push(elements.len() as u8); + for elem in elements { + elem.write_aml(&mut content); + } + + write_pkg_length(buf, content.len()); + buf.extend_from_slice(&content); +} + +pub fn write_package_raw(buf: &mut Vec, num_elements: u8, content: &[u8]) { + buf.push(PACKAGE_OP); + let len = 1 + content.len(); + write_pkg_length(buf, len); + buf.push(num_elements); + buf.extend_from_slice(content); +} + +/// ```compile_fail +/// use propolis::firmware::acpi::AmlBuilder; +/// let mut builder = AmlBuilder::new(); +/// { +/// let mut sb = builder.scope("\\_SB_"); +/// { +/// let mut pci = sb.device("PCI0"); +/// { +/// let dev = pci.device("DEV0"); +/// } +/// sb.device("DEV1"); // error: `sb` still borrowed by `pci` +/// } +/// } +/// ``` +pub struct ScopeGuard<'a> { + builder: &'a mut AmlBuilder, + start_pos: usize, + content_start: usize, +} + +impl<'a> ScopeGuard<'a> { + fn new(builder: &'a mut AmlBuilder, name: &str) -> Self { + builder.buf.push(SCOPE_OP); + let start_pos = builder.buf.len(); + builder.buf.extend_from_slice(&[0; MAX_PKG_LENGTH_BYTES]); + encode_name_string(name, &mut builder.buf); + let content_start = builder.buf.len(); + Self { builder, start_pos, content_start } + } + + pub fn scope(&mut self, name: &str) -> ScopeGuard<'_> { + ScopeGuard::new(self.builder, name) + } + + pub fn device(&mut self, name: &str) -> DeviceGuard<'_> { + DeviceGuard::new(self.builder, name) + } + + pub fn method( + &mut self, + name: &str, + arg_count: u8, + serialized: bool, + ) -> MethodGuard<'_> { + MethodGuard::new(self.builder, name, arg_count, serialized) + } + + pub fn name(&mut self, name: &str, value: &T) { + self.builder.name(name, value); + } + + pub fn name_package(&mut self, name: &str, elements: &[T]) { + self.builder.name_package(name, elements); + } + + pub fn processor(&mut self, name: &str, proc_id: u8) { + self.builder.buf.push(EXT_OP_PREFIX); + self.builder.buf.push(PROCESSOR_OP); + + let mut name_buf = Vec::new(); + encode_name_string(name, &mut name_buf); + write_pkg_length(&mut self.builder.buf, name_buf.len() + 6); + + self.builder.buf.extend_from_slice(&name_buf); + self.builder.buf.push(proc_id); + self.builder.buf.extend_from_slice(&[0u8; 4]); + self.builder.buf.push(0); + } + + pub fn raw(&mut self, bytes: &[u8]) { + self.builder.raw(bytes); + } +} + +impl Drop for ScopeGuard<'_> { + fn drop(&mut self) { + finalize_pkg_length( + &mut self.builder.buf, + self.start_pos, + self.content_start, + ); + } +} + +/// ```compile_fail +/// use propolis::firmware::acpi::AmlBuilder; +/// let mut builder = AmlBuilder::new(); +/// { +/// let mut sb = builder.scope("\\_SB_"); +/// { +/// let mut dev = sb.device("DEV0"); +/// { +/// let m = dev.method("_STA", 0, false); +/// } +/// dev.name("_UID", &0u32); +/// sb.device("DEV1"); // error: `sb` still borrowed by `dev` +/// } +/// } +/// ``` +pub struct DeviceGuard<'a> { + builder: &'a mut AmlBuilder, + start_pos: usize, + content_start: usize, +} + +impl<'a> DeviceGuard<'a> { + fn new(builder: &'a mut AmlBuilder, name: &str) -> Self { + builder.buf.push(EXT_OP_PREFIX); + builder.buf.push(DEVICE_OP); + let start_pos = builder.buf.len(); + builder.buf.extend_from_slice(&[0; MAX_PKG_LENGTH_BYTES]); + encode_name_string(name, &mut builder.buf); + let content_start = builder.buf.len(); + Self { builder, start_pos, content_start } + } + + pub fn device(&mut self, name: &str) -> DeviceGuard<'_> { + DeviceGuard::new(self.builder, name) + } + + pub fn method( + &mut self, + name: &str, + arg_count: u8, + serialized: bool, + ) -> MethodGuard<'_> { + MethodGuard::new(self.builder, name, arg_count, serialized) + } + + pub fn name(&mut self, name: &str, value: &T) { + self.builder.name(name, value); + } + + pub fn name_package(&mut self, name: &str, elements: &[T]) { + self.builder.name_package(name, elements); + } + + pub fn raw(&mut self, bytes: &[u8]) { + self.builder.raw(bytes); + } +} + +impl Drop for DeviceGuard<'_> { + fn drop(&mut self) { + finalize_pkg_length( + &mut self.builder.buf, + self.start_pos, + self.content_start, + ); + } +} + +/// ```compile_fail +/// use propolis::firmware::acpi::AmlBuilder; +/// let mut builder = AmlBuilder::new(); +/// { +/// let mut sb = builder.scope("\\_SB_"); +/// { +/// let mut dev = sb.device("DEV0"); +/// { +/// let m = dev.method("_STA", 0, false); +/// dev.method("_ON_", 0, false); // error: `dev` still borrowed by `m` +/// } +/// } +/// } +/// ``` +pub struct MethodGuard<'a> { + builder: &'a mut AmlBuilder, + start_pos: usize, + content_start: usize, +} + +impl<'a> MethodGuard<'a> { + fn new( + builder: &'a mut AmlBuilder, + name: &str, + arg_count: u8, + serialized: bool, + ) -> Self { + assert!(arg_count <= 7, "method can have at most 7 arguments"); + builder.buf.push(METHOD_OP); + let start_pos = builder.buf.len(); + builder.buf.extend_from_slice(&[0; MAX_PKG_LENGTH_BYTES]); + encode_name_string(name, &mut builder.buf); + let flags = arg_count | if serialized { 0x08 } else { 0 }; + builder.buf.push(flags); + let content_start = builder.buf.len(); + Self { builder, start_pos, content_start } + } + + pub fn store_arg_to_name(&mut self, arg: u8, name: &str) { + self.builder.buf.push(STORE_OP); + self.builder.buf.push(ARG0_OP + arg); + encode_name_string(name, &mut self.builder.buf); + } + + pub fn return_value(&mut self, value: &T) { + self.builder.return_value(value); + } + + pub fn return_name(&mut self, name: &str) { + self.builder.buf.push(RETURN_OP); + encode_name_string(name, &mut self.builder.buf); + } + + pub fn return_arg(&mut self, n: u8) { + assert!(n <= 6); + self.builder.buf.push(RETURN_OP); + self.builder.buf.push(ARG0_OP + n); + } + + pub fn raw(&mut self, bytes: &[u8]) { + self.builder.raw(bytes); + } + + pub fn create_dword_field(&mut self, source: u8, offset: u8, name: &str) { + self.builder.buf.push(CREATE_DWORD_FIELD_OP); + self.builder.buf.push(source); + if offset == 0 { + self.builder.buf.push(ZERO_OP); + } else { + self.builder.buf.push(BYTE_PREFIX); + self.builder.buf.push(offset); + } + encode_name_string(name, &mut self.builder.buf); + } + + pub fn if_uuid_equal( + &mut self, + uuid: &[u8; 16], + body: impl FnOnce(&mut Self), + ) { + self.builder.buf.push(IF_OP); + let start = self.builder.buf.len(); + self.builder.buf.extend_from_slice(&[0; MAX_PKG_LENGTH_BYTES]); + + self.builder.buf.push(LEQUAL_OP); + self.builder.buf.push(ARG0_OP); + + self.builder.buf.push(BUFFER_OP); + let buffer_start = self.builder.buf.len(); + self.builder.buf.extend_from_slice(&[0; MAX_PKG_LENGTH_BYTES]); + self.builder.buf.push(BYTE_PREFIX); + self.builder.buf.push(UUID_SIZE as u8); + let buffer_content_start = self.builder.buf.len(); + self.builder.buf.extend_from_slice(uuid); + finalize_pkg_length( + &mut self.builder.buf, + buffer_start, + buffer_content_start, + ); + + let content_start = self.builder.buf.len(); + body(self); + finalize_pkg_length(&mut self.builder.buf, start, content_start); + } + + pub fn else_block(&mut self, body: impl FnOnce(&mut Self)) { + self.builder.buf.push(ELSE_OP); + let start = self.builder.buf.len(); + self.builder.buf.extend_from_slice(&[0; MAX_PKG_LENGTH_BYTES]); + let content_start = self.builder.buf.len(); + body(self); + finalize_pkg_length(&mut self.builder.buf, start, content_start); + } + + pub fn and_to(&mut self, name: &str, mask: u32) { + self.builder.buf.push(AND_OP); + encode_name_string(name, &mut self.builder.buf); + mask.write_aml(&mut self.builder.buf); + encode_name_string(name, &mut self.builder.buf); + } + + pub fn or_to(&mut self, name: &str, value: u32) { + self.builder.buf.push(OR_OP); + encode_name_string(name, &mut self.builder.buf); + value.write_aml(&mut self.builder.buf); + encode_name_string(name, &mut self.builder.buf); + } + + pub fn store(&mut self, source: &str, dest: &str) { + self.builder.buf.push(STORE_OP); + encode_name_string(source, &mut self.builder.buf); + encode_name_string(dest, &mut self.builder.buf); + } +} + +impl Drop for MethodGuard<'_> { + fn drop(&mut self) { + finalize_pkg_length( + &mut self.builder.buf, + self.start_pos, + self.content_start, + ); + } +} + +fn finalize_pkg_length( + buf: &mut Vec, + start_pos: usize, + content_start: usize, +) { + let name_len = content_start - start_pos - MAX_PKG_LENGTH_BYTES; + let body_len = buf.len() - content_start; + let content_len = name_len + body_len; + let pkg_size = pkg_length_size(content_len); + let (pkg_bytes, size) = encode_pkg_length(pkg_size + content_len); + + buf.splice( + start_pos..start_pos + MAX_PKG_LENGTH_BYTES, + pkg_bytes[..size].iter().copied(), + ); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn integer_encoding() { + let mut buf = Vec::new(); + 0u8.write_aml(&mut buf); + assert_eq!(buf, vec![ZERO_OP]); + + buf.clear(); + 1u8.write_aml(&mut buf); + assert_eq!(buf, vec![ONE_OP]); + + buf.clear(); + 42u8.write_aml(&mut buf); + assert_eq!(buf, vec![BYTE_PREFIX, 42]); + + buf.clear(); + 0x1234u16.write_aml(&mut buf); + assert_eq!(buf, vec![WORD_PREFIX, 0x34, 0x12]); + + buf.clear(); + 0xDEADBEEFu32.write_aml(&mut buf); + assert_eq!(buf, vec![DWORD_PREFIX, 0xEF, 0xBE, 0xAD, 0xDE]); + + buf.clear(); + 0x123456789ABCDEF0u64.write_aml(&mut buf); + assert_eq!(buf[0], QWORD_PREFIX); + } + + #[test] + fn string_encoding() { + let mut buf = Vec::new(); + "Hello".write_aml(&mut buf); + assert_eq!(buf, vec![STRING_PREFIX, b'H', b'e', b'l', b'l', b'o', 0]); + } + + #[test] + fn scope_with_named_object() { + let mut builder = AmlBuilder::new(); + { + let mut scope = builder.scope("_SB_"); + scope.name("TEST", &42u8); + } + let aml = builder.finish(); + + assert_eq!(aml[0], SCOPE_OP); + assert!(aml.windows(4).any(|w| w == b"TEST")); + } + + #[test] + fn device_in_scope() { + use super::super::names::EisaId; + + let mut builder = AmlBuilder::new(); + { + let mut sb = builder.scope("\\_SB_"); + { + let mut dev = sb.device("PCI0"); + dev.name("_HID", &EisaId::from_str("PNP0A08")); + } + } + let aml = builder.finish(); + + assert_eq!(aml[0], SCOPE_OP); + assert!(aml.windows(2).any(|w| w == [EXT_OP_PREFIX, DEVICE_OP])); + } + + #[test] + fn method_with_return() { + let mut builder = AmlBuilder::new(); + { + let mut scope = builder.scope("_SB_"); + { + let mut method = scope.method("_STA", 0, false); + method.return_value(&0x0Fu8); + } + } + let aml = builder.finish(); + + assert!(aml.windows(1).any(|w| w == [METHOD_OP])); + assert!(aml.windows(4).any(|w| w == b"_STA")); + } + + #[test] + fn nested_scopes() { + let mut builder = AmlBuilder::new(); + { + let mut sb = builder.scope("\\_SB_"); + { + let mut pci = sb.scope("PCI0"); + pci.name("_ADR", &0u32); + } + } + let aml = builder.finish(); + + let scope_count = aml.iter().filter(|&&b| b == SCOPE_OP).count(); + assert_eq!(scope_count, 2); + } + + #[test] + fn pkg_length_single_byte() { + let mut builder = AmlBuilder::new(); + { + let scope = builder.scope("TEST"); + drop(scope); + } + let aml = builder.finish(); + + assert_eq!(aml.len(), 6); + assert_eq!(aml[0], SCOPE_OP); + assert_eq!(aml[1], 5); + } + + #[test] + fn pkg_length_zero() { + let (bytes, size) = encode_pkg_length(0); + assert_eq!(size, 1); + assert_eq!(bytes[0], 0); + } +} diff --git a/lib/propolis/src/firmware/acpi/dsdt.rs b/lib/propolis/src/firmware/acpi/dsdt.rs new file mode 100644 index 000000000..d54dba9d0 --- /dev/null +++ b/lib/propolis/src/firmware/acpi/dsdt.rs @@ -0,0 +1,293 @@ +// 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); + 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. +/// +/// 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 new file mode 100644 index 000000000..0117a7149 --- /dev/null +++ b/lib/propolis/src/firmware/acpi/mod.rs @@ -0,0 +1,18 @@ +// 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 aml; +pub mod dsdt; +pub mod names; +pub mod opcodes; +pub mod resources; +pub mod tables; + +pub use aml::{AmlBuilder, AmlWriter, DeviceGuard, MethodGuard, ScopeGuard}; +pub use dsdt::{build_dsdt_aml, DsdtConfig, DsdtGenerator, DsdtScope, PcieConfig}; +pub use names::{encode_uuid, EisaId}; +pub use resources::ResourceTemplateBuilder; +pub use tables::{Dsdt, Facs, Fadt, Hpet, Madt, Mcfg, Rsdt, Rsdp, Xsdt}; diff --git a/lib/propolis/src/firmware/acpi/names.rs b/lib/propolis/src/firmware/acpi/names.rs new file mode 100644 index 000000000..b1abd3805 --- /dev/null +++ b/lib/propolis/src/firmware/acpi/names.rs @@ -0,0 +1,256 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! ACPI name encoding utilities. +//! +//! See ACPI Specification 6.4, Section 20.2.2 for Name Objects Encoding. + +use super::opcodes::{DUAL_NAME_PREFIX, MULTI_NAME_PREFIX, ROOT_PREFIX}; + +pub const MAX_NAME_SEGS: usize = 255; + +pub const NAME_SEG_SIZE: usize = 4; + +/// Encode a 4-character ACPI name segment, padding shorter names with '_'. +pub fn encode_name_seg(name: &str) -> [u8; NAME_SEG_SIZE] { + assert!(name.len() <= NAME_SEG_SIZE, "name segment too long: {}", name); + assert!(!name.is_empty(), "name segment cannot be empty"); + + let bytes = name.as_bytes(); + + let first = bytes[0]; + assert!( + first.is_ascii_uppercase() || first == b'_', + "invalid first character in name segment: {}", + name + ); + + for &c in &bytes[1..] { + assert!( + c.is_ascii_uppercase() || c.is_ascii_digit() || c == b'_', + "invalid character in name segment: {}", + name + ); + } + + let mut seg = [b'_'; NAME_SEG_SIZE]; + seg[..bytes.len()].copy_from_slice(bytes); + seg +} + +/// Encode an ACPI name path (e.g. "\\_SB_.PCI0") into the buffer. +pub fn encode_name_string(path: &str, buf: &mut Vec) { + let mut chars = path.chars().peekable(); + + if chars.peek() == Some(&'\\') { + buf.push(ROOT_PREFIX); + chars.next(); + } + + while chars.peek() == Some(&'^') { + buf.push(super::opcodes::PARENT_PREFIX); + chars.next(); + } + + let remaining: String = chars.collect(); + if remaining.is_empty() { + return; + } + + let segments: Vec<&str> = remaining.split('.').collect(); + assert!( + segments.len() <= MAX_NAME_SEGS, + "too many name segments: {}", + segments.len() + ); + + match segments.len() { + 0 => {} + 1 => { + let seg = encode_name_seg(segments[0]); + buf.extend_from_slice(&seg); + } + 2 => { + buf.push(DUAL_NAME_PREFIX); + for s in &segments { + let seg = encode_name_seg(s); + buf.extend_from_slice(&seg); + } + } + n => { + buf.push(MULTI_NAME_PREFIX); + buf.push(n as u8); + for s in &segments { + let seg = encode_name_seg(s); + buf.extend_from_slice(&seg); + } + } + } +} + +/// Encode an EISA ID string (e.g. "PNP0A08") into a 32-bit value. +pub fn encode_eisaid(id: &str) -> u32 { + assert_eq!(id.len(), 7, "EISA ID must be exactly 7 characters: {}", id); + + let bytes = id.as_bytes(); + + for (i, &c) in bytes[0..3].iter().enumerate() { + assert!( + c.is_ascii_uppercase(), + "EISA ID manufacturer code must be A-Z at position {}: {}", + i, + id + ); + } + + let c1 = bytes[0] - b'A' + 1; + let c2 = bytes[1] - b'A' + 1; + let c3 = bytes[2] - b'A' + 1; + let mfg = ((c1 as u16) << 10) | ((c2 as u16) << 5) | (c3 as u16); + + let product = + u16::from_str_radix(&id[3..7], 16).expect("invalid hex in EISA ID"); + + let mfg_bytes = mfg.to_be_bytes(); + let product_bytes = product.to_be_bytes(); + + u32::from_le_bytes([ + mfg_bytes[0], + mfg_bytes[1], + product_bytes[0], + product_bytes[1], + ]) +} + +/// EISA ID wrapper that implements AmlWriter. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EisaId(pub u32); + +impl EisaId { + pub fn from_str(id: &str) -> Self { + Self(encode_eisaid(id)) + } +} + +/// UUID byte size. +pub const UUID_SIZE: usize = 16; + +/// Encode a UUID string into ACPI ToUUID format at compile time. +/// +/// UUID format: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" +/// +/// ACPI ToUUID uses mixed-endian encoding: +/// - First 3 groups (8-4-4 hex digits): little-endian +/// - Last 2 groups (4-12 hex digits): big-endian +pub const fn encode_uuid(uuid: &str) -> [u8; UUID_SIZE] { + let b = uuid.as_bytes(); + assert!(b.len() == 36, "UUID must be 36 characters"); + assert!( + b[8] == b'-' && b[13] == b'-' && b[18] == b'-' && b[23] == b'-', + "UUID must have dashes at positions 8, 13, 18, 23" + ); + + const fn hex(c: u8) -> u8 { + match c { + b'0'..=b'9' => c - b'0', + b'A'..=b'F' => c - b'A' + 10, + b'a'..=b'f' => c - b'a' + 10, + _ => panic!("invalid hex"), + } + } + + const fn byte(b: &[u8], i: usize) -> u8 { + (hex(b[i]) << 4) | hex(b[i + 1]) + } + + [ + byte(b, 6), + byte(b, 4), + byte(b, 2), + byte(b, 0), + byte(b, 11), + byte(b, 9), + byte(b, 16), + byte(b, 14), + byte(b, 19), + byte(b, 21), + byte(b, 24), + byte(b, 26), + byte(b, 28), + byte(b, 30), + byte(b, 32), + byte(b, 34), + ] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn name_seg_encoding() { + assert_eq!(encode_name_seg("_SB_"), *b"_SB_"); + assert_eq!(encode_name_seg("PCI0"), *b"PCI0"); + assert_eq!(encode_name_seg("A"), *b"A___"); + assert_eq!(encode_name_seg("AB"), *b"AB__"); + } + + #[test] + #[should_panic(expected = "name segment too long")] + fn name_seg_rejects_long() { + encode_name_seg("TOOLONG"); + } + + #[test] + #[should_panic(expected = "invalid first character")] + fn name_seg_rejects_leading_digit() { + encode_name_seg("1BAD"); + } + + #[test] + fn name_string_encoding() { + let mut buf = Vec::new(); + encode_name_string("_SB_", &mut buf); + assert_eq!(buf, b"_SB_"); + + buf.clear(); + encode_name_string("\\_SB_", &mut buf); + assert_eq!(buf, vec![ROOT_PREFIX, b'_', b'S', b'B', b'_']); + + buf.clear(); + encode_name_string("_SB_.PCI0", &mut buf); + assert_eq!(buf[0], DUAL_NAME_PREFIX); + + buf.clear(); + encode_name_string("_SB_.PCI0.ISA_", &mut buf); + assert_eq!(buf[0], MULTI_NAME_PREFIX); + assert_eq!(buf[1], 3); + } + + #[test] + fn eisaid_encoding() { + assert_eq!(encode_eisaid("PNP0A08"), 0x080AD041); + assert_eq!(encode_eisaid("PNP0A03"), 0x030AD041); + assert_eq!(encode_eisaid("PNP0501"), 0x0105D041); + assert_eq!(EisaId::from_str("PNP0A08").0, 0x080AD041); + } + + #[test] + #[should_panic(expected = "EISA ID manufacturer code must be A-Z")] + fn eisaid_rejects_lowercase() { + encode_eisaid("pnp0A08"); + } + + #[test] + fn uuid_encoding() { + let uuid = encode_uuid("33DB4D5B-1FF7-401C-9657-7441C03DD766"); + assert_eq!( + uuid, + [ + 0x5B, 0x4D, 0xDB, 0x33, 0xF7, 0x1F, 0x1C, 0x40, 0x96, 0x57, + 0x74, 0x41, 0xC0, 0x3D, 0xD7, 0x66, + ] + ); + } +} diff --git a/lib/propolis/src/firmware/acpi/opcodes.rs b/lib/propolis/src/firmware/acpi/opcodes.rs new file mode 100644 index 000000000..2c791c2c4 --- /dev/null +++ b/lib/propolis/src/firmware/acpi/opcodes.rs @@ -0,0 +1,117 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! AML opcode constants. +//! +//! See ACPI spec section 20: + +// Namespace modifier objects +pub const SCOPE_OP: u8 = 0x10; +pub const NAME_OP: u8 = 0x08; + +// Named objects (require EXT_OP_PREFIX) +pub const EXT_OP_PREFIX: u8 = 0x5B; +pub const DEVICE_OP: u8 = 0x82; // ExtOp +pub const PROCESSOR_OP: u8 = 0x83; // ExtOp +pub const POWER_RES_OP: u8 = 0x84; // ExtOp +pub const THERMAL_ZONE_OP: u8 = 0x85; // ExtOp +pub const FIELD_OP: u8 = 0x81; // ExtOp +pub const OP_REGION_OP: u8 = 0x80; // ExtOp + +// Method +pub const METHOD_OP: u8 = 0x14; + +// Data objects +pub const ZERO_OP: u8 = 0x00; +pub const ONE_OP: u8 = 0x01; +pub const ONES_OP: u8 = 0xFF; +pub const BYTE_PREFIX: u8 = 0x0A; +pub const WORD_PREFIX: u8 = 0x0B; +pub const DWORD_PREFIX: u8 = 0x0C; +pub const QWORD_PREFIX: u8 = 0x0E; +pub const STRING_PREFIX: u8 = 0x0D; +pub const BUFFER_OP: u8 = 0x11; +pub const PACKAGE_OP: u8 = 0x12; +pub const VAR_PACKAGE_OP: u8 = 0x13; + +// Name prefixes +pub const DUAL_NAME_PREFIX: u8 = 0x2E; +pub const MULTI_NAME_PREFIX: u8 = 0x2F; +pub const ROOT_PREFIX: u8 = 0x5C; // '\' +pub const PARENT_PREFIX: u8 = 0x5E; // '^' +pub const NULL_NAME: u8 = 0x00; + +// Local and argument references +pub const LOCAL0_OP: u8 = 0x60; +pub const LOCAL1_OP: u8 = 0x61; +pub const LOCAL2_OP: u8 = 0x62; +pub const LOCAL3_OP: u8 = 0x63; +pub const LOCAL4_OP: u8 = 0x64; +pub const LOCAL5_OP: u8 = 0x65; +pub const LOCAL6_OP: u8 = 0x66; +pub const LOCAL7_OP: u8 = 0x67; +pub const ARG0_OP: u8 = 0x68; +pub const ARG1_OP: u8 = 0x69; +pub const ARG2_OP: u8 = 0x6A; +pub const ARG3_OP: u8 = 0x6B; +pub const ARG4_OP: u8 = 0x6C; +pub const ARG5_OP: u8 = 0x6D; +pub const ARG6_OP: u8 = 0x6E; + +// Control flow +pub const IF_OP: u8 = 0xA0; +pub const ELSE_OP: u8 = 0xA1; +pub const WHILE_OP: u8 = 0xA2; +pub const RETURN_OP: u8 = 0xA4; +pub const BREAK_OP: u8 = 0xA5; +pub const CONTINUE_OP: u8 = 0x9F; + +// Logical operators +pub const LAND_OP: u8 = 0x90; +pub const LOR_OP: u8 = 0x91; +pub const LNOT_OP: u8 = 0x92; +pub const LEQUAL_OP: u8 = 0x93; +pub const LGREATER_OP: u8 = 0x94; +pub const LLESS_OP: u8 = 0x95; + +// Arithmetic operators +pub const ADD_OP: u8 = 0x72; +pub const SUBTRACT_OP: u8 = 0x74; +pub const MULTIPLY_OP: u8 = 0x77; +pub const DIVIDE_OP: u8 = 0x78; +pub const AND_OP: u8 = 0x7B; +pub const OR_OP: u8 = 0x7D; +pub const XOR_OP: u8 = 0x7F; +pub const NOT_OP: u8 = 0x80; +pub const SHIFT_LEFT_OP: u8 = 0x79; +pub const SHIFT_RIGHT_OP: u8 = 0x7A; + +// Miscellaneous +pub const STORE_OP: u8 = 0x70; +pub const NOTIFY_OP: u8 = 0x86; +pub const SIZEOF_OP: u8 = 0x87; +pub const INDEX_OP: u8 = 0x88; +pub const DEREF_OF_OP: u8 = 0x83; +pub const REF_OF_OP: u8 = 0x71; +pub const CREATE_DWORD_FIELD_OP: u8 = 0x8A; + +// Resource template end tag +pub const END_TAG: u8 = 0x79; + +// Operation region address space types +pub const SYSTEM_MEMORY: u8 = 0x00; +pub const SYSTEM_IO: u8 = 0x01; +pub const PCI_CONFIG: u8 = 0x02; +pub const EMBEDDED_CONTROL: u8 = 0x03; +pub const SMBUS: u8 = 0x04; +pub const CMOS: u8 = 0x05; +pub const PCI_BAR_TARGET: u8 = 0x06; + +// Field access types +pub const ACCESS_ANY: u8 = 0x00; +pub const ACCESS_BYTE: u8 = 0x01; +pub const ACCESS_WORD: u8 = 0x02; +pub const ACCESS_DWORD: u8 = 0x03; +pub const ACCESS_QWORD: u8 = 0x04; +pub const ACCESS_BUFFER: u8 = 0x05; diff --git a/lib/propolis/src/firmware/acpi/resources.rs b/lib/propolis/src/firmware/acpi/resources.rs new file mode 100644 index 000000000..86004663b --- /dev/null +++ b/lib/propolis/src/firmware/acpi/resources.rs @@ -0,0 +1,359 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! ACPI resource template encoding. +//! +//! See ACPI Specification 6.4, Section 6.4 "Resource Data Types for ACPI": +//! + +use super::aml::AmlWriter; + +// Table 6.27 "Small Resource Items" +// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#small-resource-data-type +const SMALL_IRQ_TAG: u8 = 0x04; +const SMALL_IO_TAG: u8 = 0x08; +const SMALL_END_TAG: u8 = 0x0F; + +// Table 6.40 "Large Resource Items" +// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#large-resource-data-type +const LARGE_RESOURCE_BIT: u8 = 0x80; +const LARGE_MEMORY32_FIXED: u8 = 0x06; +const LARGE_DWORD_ADDR_SPACE: u8 = 0x07; +const LARGE_WORD_ADDR_SPACE: u8 = 0x08; +const LARGE_EXT_IRQ: u8 = 0x09; +const LARGE_QWORD_ADDR_SPACE: u8 = 0x0A; + +// Table 6.48 "QWord Address Space Descriptor" +// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#qword-address-space-descriptor +const ADDR_SPACE_TYPE_MEMORY: u8 = 0x00; +const ADDR_SPACE_TYPE_IO: u8 = 0x01; +const ADDR_SPACE_TYPE_BUS: u8 = 0x02; + +// Table 6.49 "Memory Resource Flag (Resource Type = 0) Definitions" +// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#qword-address-space-descriptor +const MEM_FLAG_READ_WRITE: u8 = 1 << 0; +const MEM_FLAG_CACHEABLE: u8 = 1 << 1; +const MEM_FLAG_WRITE_COMBINING: u8 = 1 << 2; + +// Table 6.56 "Extended Interrupt Descriptor Definition" +// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#extended-interrupt-descriptor +const EXT_IRQ_FLAG_CONSUMER: u8 = 1 << 0; +const EXT_IRQ_FLAG_EDGE: u8 = 1 << 1; +const EXT_IRQ_FLAG_ACTIVE_LOW: u8 = 1 << 2; +const EXT_IRQ_FLAG_SHARED: u8 = 1 << 3; + +// Table 6.33 "I/O Port Descriptor Definition" +// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#i-o-port-descriptor +const IO_DECODE_16BIT: u8 = 1 << 0; + +fn mem_type_flags(cacheable: bool, read_write: bool) -> u8 { + let mut f = 0u8; + if cacheable { + f |= MEM_FLAG_CACHEABLE | MEM_FLAG_WRITE_COMBINING; + } + if read_write { + f |= MEM_FLAG_READ_WRITE; + } + f +} + +// Table 6.47 "General Flags" +// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#qword-address-space-descriptor +const ADDR_SPACE_FLAG_MIF: u8 = 1 << 2; +const ADDR_SPACE_FLAG_MAF: u8 = 1 << 3; + +// Table 6.50 "I/O Resource Flag (Resource Type = 1) Definitions" +// https://uefi.org/htmlspecs/ACPI_Spec_6_4_html/06_Device_Configuration/Device_Configuration.html#qword-address-space-descriptor +const IO_RANGE_ENTIRE: u8 = 0x03; + +/// Builder for ACPI resource templates used in _CRS, _PRS and _SRS methods. +#[must_use = "call .finish() to get the resource template bytes"] +pub struct ResourceTemplateBuilder { + buf: Vec, +} + +impl ResourceTemplateBuilder { + pub fn new() -> Self { + Self { buf: Vec::new() } + } + + pub fn qword_memory( + &mut self, + cacheable: bool, + read_write: bool, + min: u64, + max: u64, + translation: u64, + len: u64, + ) -> &mut Self { + self.qword_address_space( + ADDR_SPACE_TYPE_MEMORY, + cacheable, + read_write, + min, + max, + translation, + len, + ) + } + + #[allow(clippy::too_many_arguments)] + fn qword_address_space( + &mut self, + resource_type: u8, + cacheable: bool, + read_write: bool, + min: u64, + max: u64, + translation: u64, + len: u64, + ) -> &mut Self { + // 3 bytes of header + 5 u64 fields + let data_len = (3 + 5 * std::mem::size_of::()) as u16; + + self.buf.push(LARGE_RESOURCE_BIT | LARGE_QWORD_ADDR_SPACE); + self.buf.extend_from_slice(&data_len.to_le_bytes()); + + self.buf.push(resource_type); + self.buf.push(0x00); // General flags + + let type_flags = if resource_type == ADDR_SPACE_TYPE_MEMORY { + mem_type_flags(cacheable, read_write) + } else { + 0x00 + }; + self.buf.push(type_flags); + + self.buf.extend_from_slice(&0u64.to_le_bytes()); // Granularity + self.buf.extend_from_slice(&min.to_le_bytes()); + self.buf.extend_from_slice(&max.to_le_bytes()); + self.buf.extend_from_slice(&translation.to_le_bytes()); + self.buf.extend_from_slice(&len.to_le_bytes()); + + self + } + + pub fn word_bus_number( + &mut self, + min: u16, + max: u16, + translation: u16, + len: u16, + ) -> &mut Self { + // 3 bytes of header + 5 u16 fields + let data_len = (3 + 5 * std::mem::size_of::()) as u16; + + self.buf.push(LARGE_RESOURCE_BIT | LARGE_WORD_ADDR_SPACE); + self.buf.extend_from_slice(&data_len.to_le_bytes()); + + self.buf.push(ADDR_SPACE_TYPE_BUS); + self.buf.push(0x00); // General flags + self.buf.push(0x00); // Type specific flags + + self.buf.extend_from_slice(&0u16.to_le_bytes()); // Granularity + self.buf.extend_from_slice(&min.to_le_bytes()); + self.buf.extend_from_slice(&max.to_le_bytes()); + self.buf.extend_from_slice(&translation.to_le_bytes()); + self.buf.extend_from_slice(&len.to_le_bytes()); + + self + } + + pub fn dword_memory( + &mut self, + cacheable: bool, + read_write: bool, + min: u32, + max: u32, + translation: u32, + len: u32, + ) -> &mut Self { + // 3 bytes of header + 5 u32 fields + let data_len = (3 + 5 * std::mem::size_of::()) as u16; + + self.buf.push(LARGE_RESOURCE_BIT | LARGE_DWORD_ADDR_SPACE); + self.buf.extend_from_slice(&data_len.to_le_bytes()); + + self.buf.push(ADDR_SPACE_TYPE_MEMORY); + self.buf.push(0x00); // General flags + self.buf.push(mem_type_flags(cacheable, read_write)); + + self.buf.extend_from_slice(&0u32.to_le_bytes()); // Granularity + self.buf.extend_from_slice(&min.to_le_bytes()); + self.buf.extend_from_slice(&max.to_le_bytes()); + self.buf.extend_from_slice(&translation.to_le_bytes()); + self.buf.extend_from_slice(&len.to_le_bytes()); + + self + } + + pub fn io(&mut self, min: u16, max: u16, align: u8, len: u8) -> &mut Self { + // info(1) + min(2) + max(2) + align(1) + len(1) + let data_len = 1 + 2 * std::mem::size_of::() + 2; + + self.buf.push((SMALL_IO_TAG << 3) | data_len as u8); + self.buf.push(IO_DECODE_16BIT); + self.buf.extend_from_slice(&min.to_le_bytes()); + self.buf.extend_from_slice(&max.to_le_bytes()); + self.buf.push(align); + self.buf.push(len); + self + } + + pub fn io_range(&mut self, min: u16, max: u16, len: u16) -> &mut Self { + // 3 bytes of header + 5 u16 fields + let data_len = (3 + 5 * std::mem::size_of::()) as u16; + + self.buf.push(LARGE_RESOURCE_BIT | LARGE_WORD_ADDR_SPACE); + self.buf.extend_from_slice(&data_len.to_le_bytes()); + + self.buf.push(ADDR_SPACE_TYPE_IO); + self.buf.push(ADDR_SPACE_FLAG_MIF | ADDR_SPACE_FLAG_MAF); + self.buf.push(IO_RANGE_ENTIRE); + + self.buf.extend_from_slice(&0u16.to_le_bytes()); + self.buf.extend_from_slice(&min.to_le_bytes()); + self.buf.extend_from_slice(&max.to_le_bytes()); + self.buf.extend_from_slice(&0u16.to_le_bytes()); + self.buf.extend_from_slice(&len.to_le_bytes()); + + self + } + + pub fn fixed_memory(&mut self, base: u32, len: u32) -> &mut Self { + // info(1) + base(4) + len(4) + let data_len = (1 + 2 * std::mem::size_of::()) as u16; + + self.buf.push(LARGE_RESOURCE_BIT | LARGE_MEMORY32_FIXED); + self.buf.extend_from_slice(&data_len.to_le_bytes()); + self.buf.push(MEM_FLAG_READ_WRITE); + self.buf.extend_from_slice(&base.to_le_bytes()); + self.buf.extend_from_slice(&len.to_le_bytes()); + self + } + + pub fn irq(&mut self, irq_mask: u16) -> &mut Self { + let data_len = std::mem::size_of::(); + + self.buf.push((SMALL_IRQ_TAG << 3) | data_len as u8); + self.buf.extend_from_slice(&irq_mask.to_le_bytes()); + self + } + + pub fn extended_irq( + &mut self, + consumer: bool, + edge_triggered: bool, + active_low: bool, + shared: bool, + irqs: &[u32], + ) -> &mut Self { + let data_len = 2 + (irqs.len() * 4); + + self.buf.push(LARGE_RESOURCE_BIT | LARGE_EXT_IRQ); + self.buf.extend_from_slice(&(data_len as u16).to_le_bytes()); + + let mut flags = 0u8; + if consumer { + flags |= EXT_IRQ_FLAG_CONSUMER; + } + if edge_triggered { + flags |= EXT_IRQ_FLAG_EDGE; + } + if active_low { + flags |= EXT_IRQ_FLAG_ACTIVE_LOW; + } + if shared { + flags |= EXT_IRQ_FLAG_SHARED; + } + self.buf.push(flags); + self.buf.push(irqs.len() as u8); + + for &irq in irqs { + self.buf.extend_from_slice(&irq.to_le_bytes()); + } + + self + } + + pub fn finish(mut self) -> Vec { + self.buf.push((SMALL_END_TAG << 3) | 1); // 1 byte checksum + self.buf.push(0x00); + self.buf + } +} + +impl AmlWriter for ResourceTemplateBuilder { + fn write_aml(&self, buf: &mut Vec) { + let mut data = Vec::with_capacity(self.buf.len() + 2); + data.extend_from_slice(&self.buf); + data.push((SMALL_END_TAG << 3) | 1); + data.push(0x00); + data.as_slice().write_aml(buf); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn small_descriptors() { + let io_data_len = 1 + 2 * std::mem::size_of::() + 2; + let irq_data_len = std::mem::size_of::(); + + let mut builder = ResourceTemplateBuilder::new(); + builder.io(0x3F8, 0x3F8, 1, 8); + let data = builder.finish(); + assert_eq!(data[0], (SMALL_IO_TAG << 3) | io_data_len as u8); + assert_eq!(data[1], IO_DECODE_16BIT); + + let mut builder = ResourceTemplateBuilder::new(); + builder.irq(0x0010); + let data = builder.finish(); + assert_eq!(data[0], (SMALL_IRQ_TAG << 3) | irq_data_len as u8); + } + + #[test] + fn large_descriptors() { + let mut builder = ResourceTemplateBuilder::new(); + builder.word_bus_number(0, 255, 0, 256); + let data = builder.finish(); + assert_eq!(data[0], LARGE_RESOURCE_BIT | LARGE_WORD_ADDR_SPACE); + assert_eq!(data[3], ADDR_SPACE_TYPE_BUS); + + let mut builder = ResourceTemplateBuilder::new(); + builder.qword_memory( + false, + true, + 0xE000_0000, + 0xEFFF_FFFF, + 0, + 0x1000_0000, + ); + let data = builder.finish(); + assert_eq!(data[0], LARGE_RESOURCE_BIT | LARGE_QWORD_ADDR_SPACE); + assert_eq!(data[3], ADDR_SPACE_TYPE_MEMORY); + } + + #[test] + fn chained_resources() { + let mut builder = ResourceTemplateBuilder::new(); + builder + .word_bus_number(0, 255, 0, 256) + .io(0xCF8, 0xCFF, 1, 8) + .qword_memory( + false, + true, + 0xE000_0000, + 0xEFFF_FFFF, + 0, + 0x1000_0000, + ); + let data = builder.finish(); + + let len = data.len(); + assert_eq!(data[len - 2], (SMALL_END_TAG << 3) | 1); + } +} diff --git a/lib/propolis/src/firmware/acpi/tables.rs b/lib/propolis/src/firmware/acpi/tables.rs new file mode 100644 index 000000000..d845967cd --- /dev/null +++ b/lib/propolis/src/firmware/acpi/tables.rs @@ -0,0 +1,563 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! ACPI table builders. +//! +//! This module provides builders for generating ACPI tables that are used +//! to describe the system configuration to guest firmware. + +use std::mem::size_of; + +use zerocopy::{Immutable, IntoBytes}; + +pub const ACPI_TABLE_HEADER_SIZE: usize = 36; +const ACPI_TABLE_LENGTH_OFFSET: usize = 4; +pub const ACPI_TABLE_CHECKSUM_OFF: usize = 9; + +#[derive(Copy, Clone, IntoBytes, Immutable)] +#[repr(C, packed)] +struct AcpiTableHeader { + signature: [u8; 4], + length: u32, + revision: u8, + checksum: u8, + oem_id: [u8; 6], + oem_table_id: [u8; 8], + oem_revision: u32, + creator_id: [u8; 4], + creator_revision: u32, +} + +impl AcpiTableHeader { + fn new(signature: [u8; 4], revision: u8) -> Self { + Self { + signature, + length: 0, + revision, + checksum: 0, + oem_id: *b"OXIDE\0", + oem_table_id: *b"PROPOLIS", + oem_revision: 1, + creator_id: *b"OXDE", + creator_revision: 1, + } + } +} + +#[must_use = "call .finish() to get the table bytes"] +pub struct Rsdt { + data: Vec, +} + +impl Rsdt { + pub fn new() -> Self { + let header = AcpiTableHeader::new(*b"RSDT", 1); + let mut data = vec![0u8; ACPI_TABLE_HEADER_SIZE]; + data[..ACPI_TABLE_HEADER_SIZE].copy_from_slice(header.as_bytes()); + Self { data } + } + + pub fn add_entry(&mut self) -> u32 { + let offset = self.data.len() as u32; + self.data.extend_from_slice(&[0u8; size_of::()]); + offset + } + + pub fn finish(mut self) -> Vec { + let length = self.data.len() as u32; + self.data[ACPI_TABLE_LENGTH_OFFSET + ..ACPI_TABLE_LENGTH_OFFSET + size_of::()] + .copy_from_slice(&length.to_le_bytes()); + self.data + } +} + +#[must_use = "call .finish() to get the table bytes"] +pub struct Xsdt { + data: Vec, +} + +impl Xsdt { + pub fn new() -> Self { + let header = AcpiTableHeader::new(*b"XSDT", 1); + let mut data = vec![0u8; ACPI_TABLE_HEADER_SIZE]; + data[..ACPI_TABLE_HEADER_SIZE].copy_from_slice(header.as_bytes()); + Self { data } + } + + pub fn add_entry(&mut self) -> u32 { + let offset = self.data.len() as u32; + self.data.extend_from_slice(&[0u8; size_of::()]); + offset + } + + pub fn finish(mut self) -> Vec { + let length = self.data.len() as u32; + self.data[ACPI_TABLE_LENGTH_OFFSET + ..ACPI_TABLE_LENGTH_OFFSET + size_of::()] + .copy_from_slice(&length.to_le_bytes()); + self.data + } +} + +pub const RSDP_SIZE: usize = 36; +pub const RSDP_V1_SIZE: usize = 20; + +const RSDP_SIGNATURE_OFFSET: usize = 0; +const RSDP_SIGNATURE_LEN: usize = 8; +pub const RSDP_CHECKSUM_OFFSET: usize = 8; +const RSDP_OEMID_OFFSET: usize = 9; +const RSDP_OEMID_LEN: usize = 6; +const RSDP_REVISION_OFFSET: usize = 15; +const RSDP_LENGTH_OFFSET: usize = 20; +pub const RSDP_XSDT_ADDR_OFFSET: usize = 24; +pub const RSDP_EXT_CHECKSUM_OFFSET: usize = 32; + +#[must_use = "call .finish() to get the RSDP bytes"] +pub struct Rsdp { + data: Vec, +} + +impl Rsdp { + pub fn new() -> Self { + let mut data = vec![0u8; RSDP_SIZE]; + data[RSDP_SIGNATURE_OFFSET..RSDP_SIGNATURE_OFFSET + RSDP_SIGNATURE_LEN] + .copy_from_slice(b"RSD PTR "); + data[RSDP_OEMID_OFFSET..RSDP_OEMID_OFFSET + RSDP_OEMID_LEN] + .copy_from_slice(b"OXIDE\0"); + data[RSDP_REVISION_OFFSET] = 2; + data[RSDP_LENGTH_OFFSET..RSDP_LENGTH_OFFSET + size_of::()] + .copy_from_slice(&(RSDP_SIZE as u32).to_le_bytes()); + Self { data } + } + + pub fn finish(self) -> Vec { + self.data + } +} + +pub struct Dsdt { + data: Vec, +} + +impl Dsdt { + pub fn new() -> Self { + let header = AcpiTableHeader::new(*b"DSDT", 2); + let mut data = vec![0u8; ACPI_TABLE_HEADER_SIZE]; + data[..ACPI_TABLE_HEADER_SIZE].copy_from_slice(header.as_bytes()); + Self { data } + } + + pub fn append_aml(&mut self, aml: &[u8]) { + self.data.extend_from_slice(aml); + } + + pub fn finish(mut self) -> Vec { + let length = self.data.len() as u32; + self.data[ACPI_TABLE_LENGTH_OFFSET + ..ACPI_TABLE_LENGTH_OFFSET + size_of::()] + .copy_from_slice(&length.to_le_bytes()); + self.data + } +} + +pub const FADT_SIZE: usize = 276; +pub const FADT_REVISION: u8 = 6; +pub const FADT_MINOR_REVISION: u8 = 5; + +const FADT_FLAG_WBINVD: u32 = 1 << 0; +const FADT_FLAG_C1_SUPPORTED: u32 = 1 << 2; +const FADT_FLAG_SLP_BUTTON: u32 = 1 << 5; +const FADT_FLAG_TMR_VAL_EXT: u32 = 1 << 8; +const FADT_FLAG_RESET_REG_SUP: u32 = 1 << 10; +const FADT_FLAG_APIC_PHYSICAL: u32 = 1 << 19; +pub const FADT_FLAG_HW_REDUCED_ACPI: u32 = 1 << 20; + +pub const FADT_OFF_FACS32: usize = 36; +pub const FADT_OFF_DSDT32: usize = 40; +pub const FADT_OFF_DSDT64: usize = 140; +const FADT_OFF_SCI_INT: usize = 46; +const FADT_OFF_PM1A_EVT_BLK: usize = 56; +const FADT_OFF_PM1A_CNT_BLK: usize = 64; +const FADT_OFF_PM_TMR_BLK: usize = 76; +const FADT_OFF_PM1_EVT_LEN: usize = 88; +const FADT_OFF_PM1_CNT_LEN: usize = 89; +const FADT_OFF_PM_TMR_LEN: usize = 91; +const FADT_OFF_IAPC_BOOT_ARCH: usize = 109; +const FADT_OFF_FLAGS: usize = 112; +const FADT_OFF_RESET_REG: usize = 116; +const FADT_OFF_RESET_VALUE: usize = 128; +const FADT_OFF_MINOR_REV: usize = 131; +const FADT_OFF_X_PM1A_EVT_BLK: usize = 148; +const FADT_OFF_X_PM1A_CNT_BLK: usize = 172; +const FADT_OFF_X_PM_TMR_BLK: usize = 208; +const FADT_OFF_HYPERVISOR_ID: usize = 268; + +const GAS_OFF_SPACE_ID: usize = 0; +const GAS_OFF_BIT_WIDTH: usize = 1; +const GAS_OFF_ACCESS_WIDTH: usize = 3; +const GAS_OFF_ADDRESS: usize = 4; +const GAS_ADDRESS_LEN: usize = 8; +const GAS_SPACE_SYSTEM_IO: u8 = 1; +const GAS_ACCESS_BYTE: u8 = 1; +const GAS_ACCESS_WORD: u8 = 2; +const GAS_ACCESS_DWORD: u8 = 3; + +const ACPI_RESET_REG_PORT: u64 = 0xcf9; +const ACPI_RESET_VALUE: u8 = 0x06; + +const IAPC_BOOT_ARCH_LEGACY_DEVICES: u16 = 1 << 0; +const IAPC_BOOT_ARCH_8042: u16 = 1 << 1; + +const PIIX4_PM_BASE: u32 = 0xb000; +const PIIX4_PM1A_CNT_OFF: u32 = 4; +const PIIX4_PM_TMR_OFF: u32 = 8; +const PIIX4_PM1_EVT_LEN: u8 = 4; +const PIIX4_PM1_CNT_LEN: u8 = 2; +const PIIX4_PM_TMR_LEN: u8 = 4; +const PIIX4_SCI_IRQ: u16 = 9; + +const HYPERVISOR_ID: &[u8] = b"OXIDE"; + +pub struct Fadt { + data: Vec, +} + +impl Fadt { + pub fn new() -> Self { + let header = AcpiTableHeader::new(*b"FACP", FADT_REVISION); + let mut data = vec![0u8; FADT_SIZE]; + data[..ACPI_TABLE_HEADER_SIZE].copy_from_slice(header.as_bytes()); + data[ACPI_TABLE_LENGTH_OFFSET + ..ACPI_TABLE_LENGTH_OFFSET + size_of::()] + .copy_from_slice(&(FADT_SIZE as u32).to_le_bytes()); + + data[FADT_OFF_SCI_INT..FADT_OFF_SCI_INT + size_of::()] + .copy_from_slice(&PIIX4_SCI_IRQ.to_le_bytes()); + + data[FADT_OFF_PM1A_EVT_BLK..FADT_OFF_PM1A_EVT_BLK + size_of::()] + .copy_from_slice(&PIIX4_PM_BASE.to_le_bytes()); + data[FADT_OFF_PM1A_CNT_BLK..FADT_OFF_PM1A_CNT_BLK + size_of::()] + .copy_from_slice(&(PIIX4_PM_BASE + PIIX4_PM1A_CNT_OFF).to_le_bytes()); + data[FADT_OFF_PM_TMR_BLK..FADT_OFF_PM_TMR_BLK + size_of::()] + .copy_from_slice(&(PIIX4_PM_BASE + PIIX4_PM_TMR_OFF).to_le_bytes()); + + data[FADT_OFF_PM1_EVT_LEN] = PIIX4_PM1_EVT_LEN; + data[FADT_OFF_PM1_CNT_LEN] = PIIX4_PM1_CNT_LEN; + data[FADT_OFF_PM_TMR_LEN] = PIIX4_PM_TMR_LEN; + + let boot_arch = IAPC_BOOT_ARCH_LEGACY_DEVICES | IAPC_BOOT_ARCH_8042; + data[FADT_OFF_IAPC_BOOT_ARCH + ..FADT_OFF_IAPC_BOOT_ARCH + size_of::()] + .copy_from_slice(&boot_arch.to_le_bytes()); + + let flags = FADT_FLAG_WBINVD + | FADT_FLAG_C1_SUPPORTED + | FADT_FLAG_SLP_BUTTON + | FADT_FLAG_TMR_VAL_EXT + | FADT_FLAG_RESET_REG_SUP + | FADT_FLAG_APIC_PHYSICAL; + data[FADT_OFF_FLAGS..FADT_OFF_FLAGS + size_of::()] + .copy_from_slice(&flags.to_le_bytes()); + + data[FADT_OFF_RESET_REG + GAS_OFF_SPACE_ID] = GAS_SPACE_SYSTEM_IO; + data[FADT_OFF_RESET_REG + GAS_OFF_BIT_WIDTH] = u8::BITS as u8; + data[FADT_OFF_RESET_REG + GAS_OFF_ACCESS_WIDTH] = GAS_ACCESS_BYTE; + data[FADT_OFF_RESET_REG + GAS_OFF_ADDRESS + ..FADT_OFF_RESET_REG + GAS_OFF_ADDRESS + GAS_ADDRESS_LEN] + .copy_from_slice(&ACPI_RESET_REG_PORT.to_le_bytes()); + data[FADT_OFF_RESET_VALUE] = ACPI_RESET_VALUE; + + data[FADT_OFF_MINOR_REV] = FADT_MINOR_REVISION; + + data[FADT_OFF_X_PM1A_EVT_BLK + GAS_OFF_SPACE_ID] = GAS_SPACE_SYSTEM_IO; + data[FADT_OFF_X_PM1A_EVT_BLK + GAS_OFF_BIT_WIDTH] = PIIX4_PM1_EVT_LEN * 8; + data[FADT_OFF_X_PM1A_EVT_BLK + GAS_OFF_ACCESS_WIDTH] = GAS_ACCESS_DWORD; + data[FADT_OFF_X_PM1A_EVT_BLK + GAS_OFF_ADDRESS + ..FADT_OFF_X_PM1A_EVT_BLK + GAS_OFF_ADDRESS + GAS_ADDRESS_LEN] + .copy_from_slice(&(PIIX4_PM_BASE as u64).to_le_bytes()); + + data[FADT_OFF_X_PM1A_CNT_BLK + GAS_OFF_SPACE_ID] = GAS_SPACE_SYSTEM_IO; + data[FADT_OFF_X_PM1A_CNT_BLK + GAS_OFF_BIT_WIDTH] = PIIX4_PM1_CNT_LEN * 8; + data[FADT_OFF_X_PM1A_CNT_BLK + GAS_OFF_ACCESS_WIDTH] = GAS_ACCESS_WORD; + data[FADT_OFF_X_PM1A_CNT_BLK + GAS_OFF_ADDRESS + ..FADT_OFF_X_PM1A_CNT_BLK + GAS_OFF_ADDRESS + GAS_ADDRESS_LEN] + .copy_from_slice( + &((PIIX4_PM_BASE + PIIX4_PM1A_CNT_OFF) as u64).to_le_bytes(), + ); + + data[FADT_OFF_X_PM_TMR_BLK + GAS_OFF_SPACE_ID] = GAS_SPACE_SYSTEM_IO; + data[FADT_OFF_X_PM_TMR_BLK + GAS_OFF_BIT_WIDTH] = PIIX4_PM_TMR_LEN * 8; + data[FADT_OFF_X_PM_TMR_BLK + GAS_OFF_ACCESS_WIDTH] = GAS_ACCESS_DWORD; + data[FADT_OFF_X_PM_TMR_BLK + GAS_OFF_ADDRESS + ..FADT_OFF_X_PM_TMR_BLK + GAS_OFF_ADDRESS + GAS_ADDRESS_LEN] + .copy_from_slice( + &((PIIX4_PM_BASE + PIIX4_PM_TMR_OFF) as u64).to_le_bytes(), + ); + + data[FADT_OFF_HYPERVISOR_ID..FADT_OFF_HYPERVISOR_ID + HYPERVISOR_ID.len()] + .copy_from_slice(HYPERVISOR_ID); + Self { data } + } + + pub fn new_reduced() -> Self { + let mut fadt = Self::new(); + fadt.data[FADT_OFF_FLAGS..FADT_OFF_FLAGS + size_of::()] + .copy_from_slice(&FADT_FLAG_HW_REDUCED_ACPI.to_le_bytes()); + fadt + } + + pub fn finish(self) -> Vec { + self.data + } +} + +const MADT_LOCAL_APIC_ADDR_OFF: usize = ACPI_TABLE_HEADER_SIZE; +const MADT_FLAGS_OFF: usize = ACPI_TABLE_HEADER_SIZE + size_of::(); +const MADT_ENTRIES_OFF: usize = ACPI_TABLE_HEADER_SIZE + 2 * size_of::(); + +const MADT_TYPE_LOCAL_APIC: u8 = 0; +const MADT_TYPE_IO_APIC: u8 = 1; +const MADT_TYPE_INT_SRC_OVERRIDE: u8 = 2; +const MADT_TYPE_LAPIC_NMI: u8 = 4; + +const MADT_LOCAL_APIC_LEN: u8 = 8; +const MADT_IO_APIC_LEN: u8 = 12; +const MADT_INT_SRC_OVERRIDE_LEN: u8 = 10; +const MADT_LAPIC_NMI_LEN: u8 = 6; + +pub const MADT_FLAG_PCAT_COMPAT: u32 = 1; +pub const MADT_LAPIC_ENABLED: u32 = 1; + +const MADT_INT_POLARITY_ACTIVE_HIGH: u16 = 0x01; +const MADT_INT_POLARITY_ACTIVE_LOW: u16 = 0x03; +const MADT_INT_TRIGGER_EDGE: u16 = 0x04; +const MADT_INT_TRIGGER_LEVEL: u16 = 0x0c; +pub const MADT_INT_LEVEL_ACTIVE_LOW: u16 = + MADT_INT_POLARITY_ACTIVE_LOW | MADT_INT_TRIGGER_LEVEL; +pub const MADT_INT_EDGE_ACTIVE_HIGH: u16 = + MADT_INT_POLARITY_ACTIVE_HIGH | MADT_INT_TRIGGER_EDGE; +pub const MADT_INT_LEVEL_ACTIVE_HIGH: u16 = + MADT_INT_POLARITY_ACTIVE_HIGH | MADT_INT_TRIGGER_LEVEL; + +pub const ISA_BUS: u8 = 0; +pub const ISA_IRQ_TIMER: u8 = 0; +pub const ISA_IRQ_SCI: u8 = 9; +pub const GSI_TIMER: u32 = 2; +pub const GSI_SCI: u32 = 9; + +pub const ACPI_PROCESSOR_ALL: u8 = 0xff; +pub const MADT_INT_FLAGS_DEFAULT: u16 = 0; +pub const LINT1: u8 = 1; + +pub struct Madt { + data: Vec, +} + +impl Madt { + pub fn new(local_apic_addr: u32) -> Self { + let header = AcpiTableHeader::new(*b"APIC", 5); + let mut data = vec![0u8; MADT_ENTRIES_OFF]; + data[..ACPI_TABLE_HEADER_SIZE].copy_from_slice(header.as_bytes()); + data[MADT_LOCAL_APIC_ADDR_OFF + ..MADT_LOCAL_APIC_ADDR_OFF + size_of::()] + .copy_from_slice(&local_apic_addr.to_le_bytes()); + data[MADT_FLAGS_OFF..MADT_FLAGS_OFF + size_of::()] + .copy_from_slice(&MADT_FLAG_PCAT_COMPAT.to_le_bytes()); + Self { data } + } + + pub fn add_local_apic(&mut self, processor_id: u8, apic_id: u8, flags: u32) { + self.data.push(MADT_TYPE_LOCAL_APIC); + self.data.push(MADT_LOCAL_APIC_LEN); + self.data.push(processor_id); + self.data.push(apic_id); + self.data.extend_from_slice(&flags.to_le_bytes()); + } + + pub fn add_io_apic(&mut self, id: u8, addr: u32, gsi_base: u32) { + self.data.push(MADT_TYPE_IO_APIC); + self.data.push(MADT_IO_APIC_LEN); + self.data.push(id); + self.data.push(0); + self.data.extend_from_slice(&addr.to_le_bytes()); + self.data.extend_from_slice(&gsi_base.to_le_bytes()); + } + + pub fn add_int_src_override( + &mut self, + bus: u8, + source: u8, + gsi: u32, + flags: u16, + ) { + self.data.push(MADT_TYPE_INT_SRC_OVERRIDE); + self.data.push(MADT_INT_SRC_OVERRIDE_LEN); + self.data.push(bus); + self.data.push(source); + self.data.extend_from_slice(&gsi.to_le_bytes()); + self.data.extend_from_slice(&flags.to_le_bytes()); + } + + pub fn add_lapic_nmi(&mut self, processor_uid: u8, flags: u16, lint: u8) { + self.data.push(MADT_TYPE_LAPIC_NMI); + self.data.push(MADT_LAPIC_NMI_LEN); + self.data.push(processor_uid); + self.data.extend_from_slice(&flags.to_le_bytes()); + self.data.push(lint); + } + + pub fn finish(mut self) -> Vec { + let length = self.data.len() as u32; + self.data[ACPI_TABLE_LENGTH_OFFSET + ..ACPI_TABLE_LENGTH_OFFSET + size_of::()] + .copy_from_slice(&length.to_le_bytes()); + self.data + } +} + +const MCFG_ENTRIES_OFF: usize = ACPI_TABLE_HEADER_SIZE + 8; + +pub struct Mcfg { + data: Vec, +} + +impl Mcfg { + pub fn new() -> Self { + let header = AcpiTableHeader::new(*b"MCFG", 1); + let mut data = vec![0u8; MCFG_ENTRIES_OFF]; + data[..ACPI_TABLE_HEADER_SIZE].copy_from_slice(header.as_bytes()); + Self { data } + } + + pub fn add_allocation( + &mut self, + base_addr: u64, + segment_group: u16, + start_bus: u8, + end_bus: u8, + ) { + assert!(start_bus <= end_bus); + self.data.extend_from_slice(&base_addr.to_le_bytes()); + self.data.extend_from_slice(&segment_group.to_le_bytes()); + self.data.push(start_bus); + self.data.push(end_bus); + self.data.extend_from_slice(&[0u8; 4]); + } + + pub fn finish(mut self) -> Vec { + let length = self.data.len() as u32; + self.data[ACPI_TABLE_LENGTH_OFFSET + ..ACPI_TABLE_LENGTH_OFFSET + size_of::()] + .copy_from_slice(&length.to_le_bytes()); + self.data + } +} + +const HPET_HW_ID: u32 = 0x8086_0701; +const HPET_BASE_ADDR: u64 = 0xfed0_0000; +const HPET_DATA_SIZE: usize = 20; +const HPET_PAGE_PROTECT4: u8 = 1; + +const HPET_OFF_HW_ID: usize = ACPI_TABLE_HEADER_SIZE; +const HPET_OFF_BASE_ADDR: usize = ACPI_TABLE_HEADER_SIZE + 8; +const HPET_OFF_FLAGS: usize = ACPI_TABLE_HEADER_SIZE + 19; + +#[must_use = "call .finish() to get the HPET table bytes"] +pub struct Hpet { + data: Vec, +} + +impl Hpet { + pub fn new() -> Self { + let header = AcpiTableHeader::new(*b"HPET", 1); + let mut data = vec![0u8; ACPI_TABLE_HEADER_SIZE + HPET_DATA_SIZE]; + data[..ACPI_TABLE_HEADER_SIZE].copy_from_slice(header.as_bytes()); + + data[HPET_OFF_HW_ID..HPET_OFF_HW_ID + size_of::()] + .copy_from_slice(&HPET_HW_ID.to_le_bytes()); + data[HPET_OFF_BASE_ADDR..HPET_OFF_BASE_ADDR + size_of::()] + .copy_from_slice(&HPET_BASE_ADDR.to_le_bytes()); + data[HPET_OFF_FLAGS] = HPET_PAGE_PROTECT4; + + Self { data } + } + + pub fn finish(mut self) -> Vec { + let length = self.data.len() as u32; + self.data[ACPI_TABLE_LENGTH_OFFSET + ..ACPI_TABLE_LENGTH_OFFSET + size_of::()] + .copy_from_slice(&length.to_le_bytes()); + self.data + } +} + +pub const FACS_SIZE: usize = 64; +const FACS_SIGNATURE_OFF: usize = 0; +const FACS_LENGTH_OFF: usize = size_of::(); +const FACS_HW_SIGNATURE_OFF: usize = 2 * size_of::(); +const FACS_VERSION_OFF: usize = 32; + +pub struct Facs { + data: Vec, +} + +impl Facs { + pub fn new() -> Self { + let mut data = vec![0u8; FACS_SIZE]; + data[FACS_SIGNATURE_OFF..FACS_SIGNATURE_OFF + size_of::()] + .copy_from_slice(b"FACS"); + data[FACS_LENGTH_OFF..FACS_LENGTH_OFF + size_of::()] + .copy_from_slice(&(FACS_SIZE as u32).to_le_bytes()); + data[FACS_HW_SIGNATURE_OFF..FACS_HW_SIGNATURE_OFF + size_of::()] + .copy_from_slice(&0u32.to_le_bytes()); + data[FACS_VERSION_OFF] = 2; + Self { data } + } + + pub fn finish(self) -> Vec { + self.data + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn basic() { + let mut xsdt = Xsdt::new(); + xsdt.add_entry(); + let xsdt_data = xsdt.finish(); + assert_eq!(&xsdt_data[0..4], b"XSDT"); + + let rsdp = Rsdp::new(); + let rsdp_data = rsdp.finish(); + assert_eq!(&rsdp_data[0..8], b"RSD PTR "); + + let dsdt = Dsdt::new(); + let dsdt_data = dsdt.finish(); + assert_eq!(&dsdt_data[0..4], b"DSDT"); + + let fadt = Fadt::new(); + let fadt_data = fadt.finish(); + assert_eq!(&fadt_data[0..4], b"FACP"); + + let madt = Madt::new(0xFEE0_0000); + let madt_data = madt.finish(); + assert_eq!(&madt_data[0..4], b"APIC"); + + let mcfg = Mcfg::new(); + let mcfg_data = mcfg.finish(); + assert_eq!(&mcfg_data[0..4], b"MCFG"); + + let hpet = Hpet::new(); + let hpet_data = hpet.finish(); + assert_eq!(&hpet_data[0..4], b"HPET"); + + let facs = Facs::new(); + let facs_data = facs.finish(); + assert_eq!(&facs_data[0..4], b"FACS"); + } +} diff --git a/lib/propolis/src/firmware/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/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::*; diff --git a/lib/propolis/src/hw/qemu/fwcfg.rs b/lib/propolis/src/hw/qemu/fwcfg.rs index 5bad160e0..ac3c1fc52 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,495 @@ 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"; + + 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 { + 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)) + )); + } + } + + #[cfg(test)] + mod test_acpi_tables { + use super::*; + + #[test] + fn basic() { + let mut xsdt = Xsdt::new(); + xsdt.add_entry(); + let xsdt_data = xsdt.finish(); + assert_eq!(&xsdt_data[0..4], b"XSDT"); + + let rsdp = Rsdp::new(); + let rsdp_data = rsdp.finish(); + assert_eq!(&rsdp_data[0..8], b"RSD PTR "); + } + } } diff --git a/lib/propolis/src/hw/qemu/pvpanic.rs b/lib/propolis/src/hw/qemu/pvpanic.rs index 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); + } } 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); + } +} 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. 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 } }