Skip to content

Commit fa39721

Browse files
glitzflitzlgfa29iximeow
authored
Enable propolis to generate ACPI tables (#999)
Introduce ACPI table generation to Propolis and load them into guests using QEMU's Firmware Configuration (`fw_cfg`) protocol. This initial implementation of the tables produce the exact same AML bytecode as the one provided by EDKII OVMF firmware, that is why it still contains some elements that do not make sense in Propolis. Future work will modernize and make these tables more specific to Propolis. To avoid unexpected changes on reboot, the ACPI tables that are generated and loaded into guests is versioned using the `AcpiVariant` enum. The `acpi` module contains the table generation logic and uses the `acpi_tables` crate to generate the AML bytecode for the tables. Devices can generate their own AML code by implementing the `DsdtGenerator` trait and returning themselves in the method `as_dsdt_generator` of the `Lifecyle` trait. ``` impl Lifecycle for ... { fn as_dsdt_generator(&self) -> Option<&dyn acpi::DsdtGenerator> { Some(self) } // ... } ``` The `to_aml_bytes()` method of `DsdtGenerator` provides a metadata storage that can be populated during VM initialization with information needed for the AML code. Devices also need to implement the `DeviceMetadata` trait to store and retrieve metadata from this storage. Currently only the PS2 controller and UART serial ports generate their own AML because they are the only ones that can be generated to be identical to the original EDKII tables. Future ACPI table variants may be able to generate AML code directly from other devices. Co-authored-by: Luiz Aoqui <luiz@oxidecomputer.com> Co-authored-by: iximeow <git@iximeow.net>
1 parent f0e52fe commit fa39721

44 files changed

Lines changed: 3786 additions & 35 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,8 @@
44
debug.out
55
core
66
out/
7+
8+
# Ignore all binary and decompiled ACPI files except the ones used for tests.
9+
*.dat
10+
*.dsl
11+
!phd-tests/tests/testdata/acpi/**/*.dat

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ dice-verifier = { git = "https://github.com/oxidecomputer/dice-util", rev = "d74
100100
vm-attest = { git = "https://github.com/oxidecomputer/vm-attest", rev = "deaf0203ecfa8cbe8c0c28c9bd16645d5d7be4e8", default-features = false }
101101

102102
# External dependencies
103+
acpi_tables = { git = 'https://github.com/oxidecomputer/acpi_tables.git', tag = "v0.2.1-oxide.1" }
103104
anyhow = "1.0"
104105
async-trait = "0.1.88"
105106
atty = "0.2.14"

bin/propolis-server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ proptest.workspace = true
8484

8585
[features]
8686
default = []
87+
acpi-debug = ["propolis/acpi-debug"]
8788

8889
# When building to be packaged for inclusion in the production ramdisk
8990
# (nominally an Omicron package), certain code is compiled in or out.

bin/propolis-server/src/lib/initializer.rs

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ use propolis::attestation::server::AttestationServerConfig;
2929
use propolis::attestation::server::AttestationSock;
3030
use propolis::block;
3131
use propolis::chardev::{self, BlockingSource, Source};
32-
use propolis::common::{Lifecycle, GB, MB, PAGE_SIZE};
32+
use propolis::common::{DeviceMetadataMap, Lifecycle, GB, MB, PAGE_SIZE};
3333
use propolis::cpuid::TopoKind;
3434
use propolis::enlightenment::Enlightenment;
35-
use propolis::firmware::smbios;
35+
use propolis::firmware::{acpi, smbios};
3636
use propolis::hw::bhyve::BhyveHpet;
3737
use propolis::hw::chipset::{i440fx, Chipset};
3838
use propolis::hw::ibmpc;
@@ -45,7 +45,7 @@ use propolis::hw::qemu::{
4545
fwcfg::{self, Entry},
4646
ramfb,
4747
};
48-
use propolis::hw::uart::LpcUart;
48+
use propolis::hw::uart::{LpcUart, LpcUartMetadata};
4949
use propolis::hw::{nvme, virtio};
5050
use propolis::intr_pins;
5151
use propolis::vmm::{self, Builder, Machine};
@@ -107,6 +107,9 @@ pub enum MachineInitError {
107107
#[error("boot entry {0:?} refers to a device on non-zero PCI bus {1}")]
108108
BootDeviceOnDownstreamPciBus(SpecKey, u8),
109109

110+
#[error("failed to generate ACPI tables: {0}")]
111+
AcpiTableError(#[from] fwcfg::formats::AcpiTablesError),
112+
110113
#[error("failed to insert {0} fwcfg entry")]
111114
FwcfgInsertFailed(&'static str, #[source] fwcfg::InsertError),
112115

@@ -124,6 +127,15 @@ pub enum MachineInitError {
124127
/// Arbitrary ROM limit for now
125128
const MAX_ROM_SIZE: usize = 0x20_0000;
126129

130+
/// End address of the 32-bit PCI MMIO window.
131+
///
132+
// Value inherited from the original EDK2 static tables.
133+
// https://github.com/oxidecomputer/edk2/blob/f33871f488bfbbc080e0f7e3881e04d0db0b6367/OvmfPkg/PlatformPei/Platform.c#L180-L192
134+
//
135+
// It should be updated to match the actual memory regions registered in the
136+
// instance.
137+
const PCI_MMIO32_END: usize = 0xfeef_ffff;
138+
127139
fn get_spec_guest_ram_limits(spec: &Spec) -> (usize, usize) {
128140
let memsize = spec.board.memory_mb as usize * MB;
129141
let lowmem = memsize.min(3 * GB);
@@ -204,6 +216,7 @@ pub struct MachineInitializer<'a> {
204216
pub(crate) log: slog::Logger,
205217
pub(crate) machine: &'a Machine,
206218
pub(crate) devices: DeviceMap,
219+
pub(crate) device_metadata: DeviceMetadataMap,
207220
pub(crate) block_backends: BlockBackendMap,
208221
pub(crate) crucible_backends: CrucibleBackendMap,
209222
pub(crate) spec: &'a Spec,
@@ -406,17 +419,28 @@ impl MachineInitializer<'_> {
406419
continue;
407420
}
408421

409-
let (irq, port) = match desc.num {
410-
SerialPortNumber::Com1 => (ibmpc::IRQ_COM1, ibmpc::PORT_COM1),
411-
SerialPortNumber::Com2 => (ibmpc::IRQ_COM2, ibmpc::PORT_COM2),
412-
SerialPortNumber::Com3 => (ibmpc::IRQ_COM3, ibmpc::PORT_COM3),
413-
SerialPortNumber::Com4 => (ibmpc::IRQ_COM4, ibmpc::PORT_COM4),
422+
let (num, irq, port) = match desc.num {
423+
SerialPortNumber::Com1 => {
424+
(1, ibmpc::IRQ_COM1, ibmpc::PORT_COM1)
425+
}
426+
SerialPortNumber::Com2 => {
427+
(2, ibmpc::IRQ_COM2, ibmpc::PORT_COM2)
428+
}
429+
SerialPortNumber::Com3 => {
430+
(3, ibmpc::IRQ_COM3, ibmpc::PORT_COM3)
431+
}
432+
SerialPortNumber::Com4 => {
433+
(4, ibmpc::IRQ_COM4, ibmpc::PORT_COM4)
434+
}
414435
};
415436

416437
let dev = LpcUart::new(chipset.irq_pin(irq).unwrap());
417438
dev.set_autodiscard(true);
418439
LpcUart::attach(&dev, &self.machine.bus_pio, port);
419440
self.devices.insert(name.to_owned(), dev.clone());
441+
self.device_metadata
442+
.insert(&dev, Box::new(LpcUartMetadata::new(num, port, irq)));
443+
420444
if desc.num == SerialPortNumber::Com1 {
421445
assert!(com1.is_none());
422446
com1 = Some(dev);
@@ -1110,9 +1134,17 @@ impl MachineInitializer<'_> {
11101134
// NOTE: SoftNpu squats on com4.
11111135
let uart = LpcUart::new(chipset.irq_pin(ibmpc::IRQ_COM4).unwrap());
11121136
uart.set_autodiscard(true);
1113-
LpcUart::attach(&uart, &self.machine.bus_pio, ibmpc::PORT_COM4);
1137+
uart.attach(&self.machine.bus_pio, ibmpc::PORT_COM4);
11141138
self.devices
11151139
.insert(SpecKey::Name("softnpu-uart".to_string()), uart.clone());
1140+
self.device_metadata.insert(
1141+
&uart,
1142+
Box::new(LpcUartMetadata::new(
1143+
4,
1144+
ibmpc::PORT_COM4,
1145+
ibmpc::IRQ_COM4,
1146+
)),
1147+
);
11161148

11171149
// Start with no pipeline. The guest must load the initial P4 program.
11181150
let pipeline = Arc::new(std::sync::Mutex::new(None));
@@ -1433,14 +1465,52 @@ impl MachineInitializer<'_> {
14331465
Ok(Some(order.finish()))
14341466
}
14351467

1468+
fn generate_acpi_tables(
1469+
&self,
1470+
acpi_variant: acpi::AcpiVariant,
1471+
cpus: u8,
1472+
) -> Result<fwcfg::formats::AcpiTables, MachineInitError> {
1473+
let (lowmem, _) = get_spec_guest_ram_limits(self.spec);
1474+
let generators: Vec<_> = self
1475+
.devices
1476+
.values()
1477+
.filter_map(|dev| dev.as_dsdt_generator())
1478+
.collect();
1479+
1480+
// The values for pci_window_32 and pci_window_64 are set based on the
1481+
// original EDK2 ACPI tables, and currently don't exactly match the
1482+
// ranges defined in build_instance().
1483+
//
1484+
// Propolis doesn't verify if an MMIO operation happens in an address
1485+
// reserved for MMIO, so this doesn't cause problems for now, but the
1486+
// PCI windows should be updated to match what's reserved in
1487+
// build_instance().
1488+
let pci_window_32 = fwcfg::formats::PciWindow::new(
1489+
lowmem as u64,
1490+
PCI_MMIO32_END as u64,
1491+
)?;
1492+
1493+
let config = &fwcfg::formats::AcpiConfig {
1494+
acpi_variant,
1495+
num_cpus: cpus,
1496+
pci_window_32,
1497+
pci_window_64: fwcfg::formats::PciWindow::empty(),
1498+
dsdt_generators: &generators,
1499+
device_metadata: &self.device_metadata,
1500+
};
1501+
let acpi_tables = fwcfg::formats::AcpiTablesBuilder::new(config);
1502+
Ok(acpi_tables.build())
1503+
}
1504+
14361505
/// Initialize qemu `fw_cfg` device, and populate it with data including CPU
1437-
/// count, SMBIOS tables, and attached RAM-FB device.
1506+
/// count, SMBIOS and ACPI tables, and attached RAM-FB device.
14381507
///
14391508
/// Should not be called before [`Self::initialize_rom()`].
14401509
pub fn initialize_fwcfg(
14411510
&mut self,
14421511
cpus: u8,
14431512
bootrom_version: &Option<String>,
1513+
acpi_variant: acpi::AcpiVariant,
14441514
) -> Result<Arc<ramfb::RamFb>, MachineInitError> {
14451515
let fwcfg = fwcfg::FwCfg::new();
14461516
fwcfg
@@ -1479,6 +1549,19 @@ impl MachineInitializer<'_> {
14791549
.insert_named("etc/e820", e820_entry)
14801550
.map_err(|e| MachineInitError::FwcfgInsertFailed("e820", e))?;
14811551

1552+
let acpi_entries = self.generate_acpi_tables(acpi_variant, cpus)?;
1553+
fwcfg.insert_named("etc/acpi/tables", acpi_entries.tables).map_err(
1554+
|e| MachineInitError::FwcfgInsertFailed("acpi/tables", e),
1555+
)?;
1556+
fwcfg
1557+
.insert_named("etc/acpi/rsdp", acpi_entries.rsdp)
1558+
.map_err(|e| MachineInitError::FwcfgInsertFailed("acpi/rsdp", e))?;
1559+
fwcfg
1560+
.insert_named("etc/table-loader", acpi_entries.table_loader)
1561+
.map_err(|e| {
1562+
MachineInitError::FwcfgInsertFailed("table-loader", e)
1563+
})?;
1564+
14821565
let ramfb = ramfb::RamFb::create(
14831566
self.log.new(slog::o!("component" => "ramfb")),
14841567
);

bin/propolis-server/src/lib/spec/builder.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ impl SpecBuilder {
100100
memory_mb: board.memory_mb,
101101
chipset: board.chipset,
102102
guest_hv_interface: board.guest_hv_interface,
103+
acpi_variant: propolis::firmware::acpi::AcpiVariant::V0,
103104
},
104105
cpuid,
105106
..Default::default()
@@ -403,6 +404,7 @@ mod test {
403404
memory_mb: 512,
404405
chipset: Chipset::I440Fx(I440Fx { enable_pcie: false }),
405406
guest_hv_interface: GuestHypervisorInterface::Bhyve,
407+
acpi_variant: propolis::firmware::acpi::AcpiVariant::V0,
406408
};
407409

408410
SpecBuilder {

bin/propolis-server/src/lib/spec/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ use thiserror::Error;
4242
#[cfg(feature = "failure-injection")]
4343
use propolis_api_types::instance_spec::components::devices::MigrationFailureInjector;
4444

45+
use propolis::firmware::acpi::AcpiVariant;
4546
#[cfg(feature = "falcon")]
4647
use propolis_api_types::instance_spec::components::{
4748
backends::DlpiNetworkBackend,
@@ -160,6 +161,9 @@ pub(crate) struct Board {
160161
pub memory_mb: u64,
161162
pub chipset: Chipset,
162163
pub guest_hv_interface: GuestHypervisorInterface,
164+
165+
// XXX: expose via the API once more variants are implemented.
166+
pub acpi_variant: AcpiVariant,
163167
}
164168

165169
impl Default for Board {
@@ -169,6 +173,7 @@ impl Default for Board {
169173
memory_mb: 0,
170174
chipset: Chipset::I440Fx(I440Fx { enable_pcie: false }),
171175
guest_hv_interface: GuestHypervisorInterface::Bhyve,
176+
acpi_variant: AcpiVariant::V0,
172177
}
173178
}
174179
}

bin/propolis-server/src/lib/vm/ensure.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ use std::sync::Arc;
9393

9494
use oximeter::types::ProducerRegistry;
9595
use oximeter_instruments::kstat::KstatSampler;
96+
use propolis::common::DeviceMetadataMap;
9697
use propolis::enlightenment::{
9798
bhyve::BhyveGuestInterface,
9899
hyperv::{Features as HyperVFeatures, HyperV},
@@ -532,6 +533,7 @@ async fn initialize_vm_objects(
532533
log: log.clone(),
533534
machine: &machine,
534535
devices: Default::default(),
536+
device_metadata: DeviceMetadataMap::new(),
535537
block_backends: Default::default(),
536538
crucible_backends: Default::default(),
537539
spec: &spec,
@@ -579,8 +581,11 @@ async fn initialize_vm_objects(
579581
.initialize_storage_devices(&chipset, options.nexus_client.clone())
580582
.await?;
581583

582-
let ramfb =
583-
init.initialize_fwcfg(spec.board.cpus, &options.bootrom_version)?;
584+
let ramfb = init.initialize_fwcfg(
585+
spec.board.cpus,
586+
&options.bootrom_version,
587+
spec.board.acpi_variant,
588+
)?;
584589

585590
// If we have a VM RoT, that RoT needs to be able to collect some
586591
// information about the guest before it can be actually usable. It will do

bin/propolis-standalone/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,4 @@ pbind.workspace = true
4444
[features]
4545
default = []
4646
crucible = ["propolis/crucible-full", "propolis/oximeter", "crucible-client-types"]
47+
acpi-debug = ["propolis/acpi-debug"]

bin/propolis-standalone/src/config.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use serde::{Deserialize, Serialize};
1818

1919
use cpuid_profile_config::*;
2020
use propolis::block;
21+
use propolis::firmware::acpi::AcpiVariant;
2122
use propolis::hw::pci::Bdf;
2223

2324
use crate::cidata::build_cidata_be;
@@ -71,6 +72,12 @@ pub struct Main {
7172

7273
/// Request bootrom override boot order using the devices specified
7374
pub boot_order: Option<Vec<String>>,
75+
76+
/// ACPI table variant to use for the VM
77+
///
78+
/// Default: V0
79+
#[serde(default)]
80+
pub acpi_variant: AcpiVariant,
7481
}
7582

7683
#[derive(Copy, Clone, Debug, Deserialize, Serialize)]

0 commit comments

Comments
 (0)