Skip to content

Commit 7330aa2

Browse files
committed
acpi: add ACPI support in Firecracker
Initial ACPI setup for x86. We have FADT in Hardware Reduced mode. This means that we declare to the guest that we don't implement in "hardware" some features, such as control and event registers. We also have MADT, which essentially replaces the functionality of MPTable; we define on IO-APIC and one LocalAPIC structure per vCPU. All vCPUs are marked as online by default. Finally, we have an empty DSDT. This will be populated with info about VirtIO, legacy and ACPI devices in future commits. Signed-off-by: Babis Chalios <[email protected]>
1 parent 6a5c41d commit 7330aa2

File tree

11 files changed

+519
-44
lines changed

11 files changed

+519
-44
lines changed

Cargo.lock

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

src/vmm/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,12 @@ micro_http = { git = "https://github.com/firecracker-microvm/micro-http" }
3838
log-instrument = { path = "../log-instrument", optional = true }
3939
crc64 = "2.0.0"
4040
slab = "0.4.7"
41+
zerocopy = { version = "0.7.32" }
4142

4243
seccompiler = { path = "../seccompiler" }
4344
utils = { path = "../utils" }
4445
smallvec = "1.11.2"
46+
acpi_tables = { path = "../acpi-tables" }
4547

4648
[target.'cfg(target_arch = "aarch64")'.dependencies]
4749
vm-fdt = "0.2.0"

src/vmm/src/acpi/mod.rs

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use acpi_tables::fadt::{FADT_F_HW_REDUCED_ACPI, FADT_F_PWR_BUTTON, FADT_F_SLP_BUTTON};
5+
use acpi_tables::{Dsdt, Fadt, Madt, Rsdp, Sdt, Xsdt};
6+
use log::{debug, error};
7+
use vm_allocator::AllocPolicy;
8+
9+
use crate::acpi::x86_64::{
10+
apic_addr, rsdp_addr, setup_arch_dsdt, setup_arch_fadt, setup_interrupt_controllers,
11+
};
12+
use crate::device_manager::resources::ResourceAllocator;
13+
use crate::vstate::memory::{GuestAddress, GuestMemoryMmap};
14+
use crate::Vcpu;
15+
16+
mod x86_64;
17+
18+
// Our (Original Equipment Manufacturer" (OEM) name. OEM is how ACPI names the manufacturer of the
19+
// hardware that is exposed to the OS, through ACPI tables. The OEM name is passed in every ACPI
20+
// table, to let the OS know that we are the owner of the table.
21+
const OEM_ID: [u8; 6] = *b"FIRECK";
22+
23+
// In reality the OEM revision is per table and it defines the revision of the OEM's implementation
24+
// of the particular ACPI table. For our purpose, we can set it to a fixed value for all the tables
25+
const OEM_REVISION: u32 = 0;
26+
27+
// This is needed for an entry in the FADT table. Populating this entry in FADT is a way to let the
28+
// guest know that it runs within a Firecracker microVM.
29+
const HYPERVISOR_VENDOR_ID: [u8; 8] = *b"FIRECKVM";
30+
31+
#[derive(Debug, thiserror::Error, displaydoc::Display)]
32+
/// Error type for ACPI related operations
33+
pub enum AcpiError {
34+
/// Could not allocate resources: {0}
35+
VmAllocator(#[from] vm_allocator::Error),
36+
/// ACPI tables error: {0}
37+
AcpiTables(#[from] acpi_tables::AcpiError),
38+
}
39+
40+
/// Helper type that holds the guest memory in which we write the tables in and a resource
41+
/// allocator for allocating space for the tables
42+
struct AcpiTableWriter<'a> {
43+
mem: &'a GuestMemoryMmap,
44+
resource_allocator: &'a mut ResourceAllocator,
45+
}
46+
47+
impl<'a> AcpiTableWriter<'a> {
48+
/// Write a table in guest memory
49+
///
50+
/// This will allocate enough space inside guest memory and write the table in the allocated
51+
/// buffer. It returns the address in which it wrote the table.
52+
fn write_acpi_table<S>(&mut self, table: &mut S) -> Result<u64, AcpiError>
53+
where
54+
S: Sdt,
55+
{
56+
let addr = self.resource_allocator.allocate_system_memory(
57+
table.len().try_into().unwrap(),
58+
1,
59+
AllocPolicy::FirstMatch,
60+
)?;
61+
62+
table
63+
.write_to_guest(self.mem, GuestAddress(addr))
64+
.inspect_err(|err| error!("acpi: Could not write table in guest memory: {err}"))?;
65+
66+
debug!(
67+
"acpi: Wrote table ({} bytes) at address: {:#010x}",
68+
table.len(),
69+
addr
70+
);
71+
72+
Ok(addr)
73+
}
74+
75+
/// Build the DSDT table for the guest
76+
fn build_dsdt(&mut self) -> Result<u64, AcpiError> {
77+
let mut dsdt_data = Vec::new();
78+
79+
// Architecture specific DSDT data
80+
setup_arch_dsdt(&mut dsdt_data);
81+
82+
let mut dsdt = Dsdt::new(OEM_ID, *b"FCVMDSDT", OEM_REVISION, dsdt_data);
83+
self.write_acpi_table(&mut dsdt)
84+
}
85+
86+
/// Build the FADT table for the guest
87+
///
88+
/// This includes a pointer with the location of the DSDT in guest memory
89+
fn build_fadt(&mut self, dsdt_addr: u64) -> Result<u64, AcpiError> {
90+
let mut fadt = Fadt::new(OEM_ID, *b"FCVMFADT", OEM_REVISION);
91+
fadt.set_hypervisor_vendor_id(HYPERVISOR_VENDOR_ID);
92+
fadt.set_x_dsdt(dsdt_addr);
93+
fadt.set_flags(
94+
1 << FADT_F_HW_REDUCED_ACPI | 1 << FADT_F_PWR_BUTTON | 1 << FADT_F_SLP_BUTTON,
95+
);
96+
setup_arch_fadt(&mut fadt);
97+
self.write_acpi_table(&mut fadt)
98+
}
99+
100+
/// Build the MADT table for the guest
101+
///
102+
/// This includes information about the interrupt controllers supported in the platform
103+
fn build_madt(&mut self, nr_vcpus: u8) -> Result<u64, AcpiError> {
104+
let mut madt = Madt::new(
105+
OEM_ID,
106+
*b"FCVMMADT",
107+
OEM_REVISION,
108+
apic_addr(),
109+
setup_interrupt_controllers(nr_vcpus),
110+
);
111+
self.write_acpi_table(&mut madt)
112+
}
113+
114+
/// Build the XSDT table for the guest
115+
///
116+
/// Currently, we pass to the guest just FADT and MADT tables.
117+
fn build_xsdt(&mut self, fadt_addr: u64, madt_addr: u64) -> Result<u64, AcpiError> {
118+
let mut xsdt = Xsdt::new(
119+
OEM_ID,
120+
*b"FCMVXSDT",
121+
OEM_REVISION,
122+
vec![fadt_addr, madt_addr],
123+
);
124+
self.write_acpi_table(&mut xsdt)
125+
}
126+
127+
/// Build the RSDP pointer for the guest.
128+
///
129+
/// This will build the RSDP pointer which points to the XSDT table and write it in guest
130+
/// memory. The address in which we write RSDP is pre-determined for every architecture.
131+
/// We will not allocate arbitrary memory for it
132+
fn build_rsdp(&mut self, xsdt_addr: u64) -> Result<(), AcpiError> {
133+
let mut rsdp = Rsdp::new(OEM_ID, xsdt_addr);
134+
rsdp.write_to_guest(self.mem, rsdp_addr())
135+
.inspect_err(|err| error!("acpi: Could not write RSDP in guest memory: {err}"))?;
136+
137+
debug!(
138+
"acpi: Wrote RSDP ({} bytes) at address: {:#010x}",
139+
rsdp.len(),
140+
rsdp_addr().0
141+
);
142+
Ok(())
143+
}
144+
}
145+
146+
/// Create ACPI tables for the guest
147+
///
148+
/// This will create the ACPI tables needed to describe to the guest OS the available hardware,
149+
/// such as interrupt controllers, vCPUs and VirtIO devices.
150+
pub(crate) fn create_acpi_tables(
151+
mem: &GuestMemoryMmap,
152+
resource_allocator: &mut ResourceAllocator,
153+
vcpus: &[Vcpu],
154+
) -> Result<(), AcpiError> {
155+
let mut writer = AcpiTableWriter {
156+
mem,
157+
resource_allocator,
158+
};
159+
160+
let dsdt_addr = writer.build_dsdt()?;
161+
let fadt_addr = writer.build_fadt(dsdt_addr)?;
162+
let madt_addr = writer.build_madt(vcpus.len().try_into().unwrap())?;
163+
let xsdt_addr = writer.build_xsdt(fadt_addr, madt_addr)?;
164+
writer.build_rsdp(xsdt_addr)
165+
}
166+
167+
#[cfg(test)]
168+
pub mod tests {
169+
use acpi_tables::Sdt;
170+
use vm_memory::Bytes;
171+
172+
use crate::acpi::{AcpiError, AcpiTableWriter};
173+
use crate::arch::x86_64::layout::{SYSTEM_MEM_SIZE, SYSTEM_MEM_START};
174+
use crate::builder::tests::default_vmm;
175+
use crate::utilities::test_utils::arch_mem;
176+
177+
struct MockSdt(Vec<u8>);
178+
179+
impl Sdt for MockSdt {
180+
fn len(&self) -> usize {
181+
self.0.len()
182+
}
183+
184+
fn write_to_guest<M: vm_memory::GuestMemory>(
185+
&mut self,
186+
mem: &M,
187+
address: vm_memory::GuestAddress,
188+
) -> acpi_tables::Result<()> {
189+
mem.write_slice(&self.0, address)?;
190+
Ok(())
191+
}
192+
}
193+
194+
// Currently we are allocating up to SYSTEM_MEM_SIZE memory for ACPI tables. We are allocating
195+
// using the FirstMatch policy, with an 1 byte alignment. This test checks that we are able to
196+
// allocate up to this size, and get back the expected addresses.
197+
#[test]
198+
fn test_write_acpi_table_memory_allocation() {
199+
// A mocke Vmm object with 128MBs of memory
200+
let mut vmm = default_vmm();
201+
let mut writer = AcpiTableWriter {
202+
mem: &vmm.guest_memory,
203+
resource_allocator: &mut vmm.resource_allocator,
204+
};
205+
206+
// This should succeed
207+
let mut sdt = MockSdt(vec![0; 4096]);
208+
let addr = writer.write_acpi_table(&mut sdt).unwrap();
209+
assert_eq!(addr, SYSTEM_MEM_START);
210+
211+
// Let's try to write two 4K pages plus one byte
212+
let mut sdt = MockSdt(vec![0; usize::try_from(SYSTEM_MEM_SIZE + 1).unwrap()]);
213+
let err = writer.write_acpi_table(&mut sdt).unwrap_err();
214+
assert!(
215+
matches!(
216+
err,
217+
AcpiError::VmAllocator(vm_allocator::Error::ResourceNotAvailable)
218+
),
219+
"{:?}",
220+
err
221+
);
222+
223+
// We are allocating memory for tables with alignment of 1 byte. All of these should
224+
// succeed.
225+
let mut sdt = MockSdt(vec![0; 5]);
226+
let addr = writer.write_acpi_table(&mut sdt).unwrap();
227+
assert_eq!(addr, SYSTEM_MEM_START + 4096);
228+
let mut sdt = MockSdt(vec![0; 2]);
229+
let addr = writer.write_acpi_table(&mut sdt).unwrap();
230+
assert_eq!(addr, SYSTEM_MEM_START + 4101);
231+
let mut sdt = MockSdt(vec![0; 4]);
232+
let addr = writer.write_acpi_table(&mut sdt).unwrap();
233+
assert_eq!(addr, SYSTEM_MEM_START + 4103);
234+
let mut sdt = MockSdt(vec![0; 8]);
235+
let addr = writer.write_acpi_table(&mut sdt).unwrap();
236+
assert_eq!(addr, SYSTEM_MEM_START + 4107);
237+
let mut sdt = MockSdt(vec![0; 16]);
238+
let addr = writer.write_acpi_table(&mut sdt).unwrap();
239+
assert_eq!(addr, SYSTEM_MEM_START + 4115);
240+
}
241+
242+
// If, for whatever weird reason, we end up with microVM that has less memory than the maximum
243+
// address we allocate for ACPI tables, we would be able to allocate the tables but we would
244+
// not be able to write them. This is practically impossible in our case. If we get such a
245+
// guest memory, we won't be able to load the guest kernel, but the function does
246+
// return an error on this case, so let's just check that in case any of these assumptions
247+
// change in the future.
248+
#[test]
249+
fn test_write_acpi_table_small_memory() {
250+
let mut vmm = default_vmm();
251+
vmm.guest_memory = arch_mem(
252+
(SYSTEM_MEM_START + SYSTEM_MEM_SIZE - 4096)
253+
.try_into()
254+
.unwrap(),
255+
);
256+
let mut writer = AcpiTableWriter {
257+
mem: &vmm.guest_memory,
258+
resource_allocator: &mut vmm.resource_allocator,
259+
};
260+
261+
let mut sdt = MockSdt(vec![0; usize::try_from(SYSTEM_MEM_SIZE).unwrap()]);
262+
let err = writer.write_acpi_table(&mut sdt).unwrap_err();
263+
assert!(
264+
matches!(
265+
err,
266+
AcpiError::AcpiTables(acpi_tables::AcpiError::GuestMemory(
267+
vm_memory::GuestMemoryError::PartialBuffer {
268+
expected: 263168, // SYSTEM_MEM_SIZE
269+
completed: 259072 // SYSTEM_MEM_SIZE - 4096
270+
},
271+
))
272+
),
273+
"{:?}",
274+
err
275+
);
276+
}
277+
}

src/vmm/src/acpi/x86_64.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use std::mem::size_of;
5+
6+
use acpi_tables::fadt::{
7+
IAPC_BOOT_ARG_FLAGS_MSI_NOT_PRESENT, IAPC_BOOT_ARG_FLAGS_PCI_ASPM,
8+
IAPC_BOOT_ARG_FLAGS_VGA_NOT_PRESENT,
9+
};
10+
use acpi_tables::madt::{IoAPIC, LocalAPIC};
11+
use acpi_tables::Fadt;
12+
use vm_memory::GuestAddress;
13+
use zerocopy::AsBytes;
14+
15+
use crate::arch::x86_64::layout;
16+
17+
#[inline(always)]
18+
pub(crate) fn setup_interrupt_controllers(nr_vcpus: u8) -> Vec<u8> {
19+
let mut ic =
20+
Vec::with_capacity(size_of::<IoAPIC>() + (nr_vcpus as usize) * size_of::<LocalAPIC>());
21+
22+
ic.extend_from_slice(IoAPIC::new(0, layout::IOAPIC_ADDR).as_bytes());
23+
for i in 0..nr_vcpus {
24+
ic.extend_from_slice(LocalAPIC::new(i).as_bytes());
25+
}
26+
ic
27+
}
28+
29+
#[inline(always)]
30+
pub(crate) fn setup_arch_fadt(fadt: &mut Fadt) {
31+
// Let the guest kernel know that there is not VGA hardware present
32+
// neither do we support ASPM, or MSI type of interrupts.
33+
// More info here:
34+
// https://uefi.org/specs/ACPI/6.5/05_ACPI_Software_Programming_Model.html?highlight=0a06#ia-pc-boot-architecture-flags
35+
fadt.setup_iapc_flags(
36+
1 << IAPC_BOOT_ARG_FLAGS_VGA_NOT_PRESENT
37+
| 1 << IAPC_BOOT_ARG_FLAGS_PCI_ASPM
38+
| 1 << IAPC_BOOT_ARG_FLAGS_MSI_NOT_PRESENT,
39+
);
40+
}
41+
42+
#[allow(clippy::ptr_arg)]
43+
#[inline(always)]
44+
pub(crate) fn setup_arch_dsdt(_dsdt_data: &mut Vec<u8>) {}
45+
46+
pub(crate) const fn apic_addr() -> u32 {
47+
layout::APIC_ADDR
48+
}
49+
50+
pub(crate) const fn rsdp_addr() -> GuestAddress {
51+
GuestAddress(layout::RSDP_ADDR)
52+
}

src/vmm/src/arch/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ pub mod x86_64;
2222

2323
#[cfg(target_arch = "x86_64")]
2424
pub use crate::arch::x86_64::{
25-
arch_memory_regions, configure_system, get_kernel_start, initrd_load_addr,
26-
layout::CMDLINE_MAX_SIZE, layout::IRQ_BASE, layout::IRQ_MAX, ConfigurationError, MMIO_MEM_SIZE,
25+
arch_memory_regions, configure_system, get_kernel_start, initrd_load_addr, layout::APIC_ADDR,
26+
layout::CMDLINE_MAX_SIZE, layout::IOAPIC_ADDR, layout::IRQ_BASE, layout::IRQ_MAX,
27+
layout::SYSTEM_MEM_SIZE, layout::SYSTEM_MEM_START, ConfigurationError, MMIO_MEM_SIZE,
2728
MMIO_MEM_START,
2829
};
2930

0 commit comments

Comments
 (0)