ACPI and the MPTable
Before a guest kernel can use the vCPUs and the interrupt controller the VMM created, it has to be told they exist — how many CPUs there are, where the IOAPIC lives, which interrupts are routed where. On a real PC this information comes from firmware tables the BIOS leaves in memory. Firecracker has no BIOS, so the VMM writes those tables itself, into guest memory, before the kernel starts. Historically it wrote a legacy MPTable (the Intel MultiProcessor Specification format); more recently it builds proper ACPI tables (RSDP/MADT and friends) and is deprecating MPTable.
This chapter covers the legacy MPTable vs the newer ACPI tables, the acpi-tables crate, why ACPI
was added and MPTable is going away, how vCPUs and the IOAPIC are described to the guest, and how this
all relates to the PCI/PVH future of Firecracker. This is x86_64 territory — aarch64 uses an FDT/DTB
instead (see the-boot-sequence.md).
Note: This is a topology-description problem, not a device-emulation one. The tables are static data structures the VMM lays into guest RAM at boot; the guest kernel parses them once, early, to discover its own hardware. Get a checksum or a field wrong and the guest either ignores a CPU, mis-routes interrupts, or refuses to boot — failures that look mysterious until you remember the table is the contract.
The two formats
rg -n "mptable|MpTable|MP_SPEC|mpf_intel|mpc_table" src/vmm/src/arch/x86_64/
find src/vmm/src/arch/x86_64 -name "mptable*"
find src/acpi-tables -name "*.rs"
| Format | What it is | Status in Firecracker |
|---|---|---|
| MPTable | Intel MultiProcessor Specification — a pre-ACPI table describing CPUs, the local/IO APIC, and interrupt routing | legacy; being deprecated (verify default on your branch) |
| ACPI | Advanced Configuration and Power Interface — the modern standard; Firecracker builds RSDP → RSDT/XSDT → MADT (+ others) | the direction; built by the in-tree acpi-tables crate |
MPTable lives in src/vmm/src/arch/x86_64/mptable.rs (find it with the find above). The ACPI table
builders live in the dedicated acpi-tables crate (src/acpi-tables/), consumed by the x86_64
arch code. Both produce the same information — CPU count, APIC layout, interrupt overrides — in two
different on-memory encodings the kernel knows how to read.
What the tables describe
rg -n "MADT|Madt|Rsdp|Xsdt|local_apic|io_apic|IOAPIC|LocalApic|InterruptSourceOverride" src/acpi-tables/src/ src/vmm/src/arch/x86_64/
The essential content, regardless of format:
| Item | Described by | Why the guest needs it |
|---|---|---|
| vCPU count + their Local APIC IDs | MADT Local APIC entries / MPTable CPU entries | the kernel must know how many CPUs to bring online and their APIC IDs |
| The IOAPIC | MADT I/O APIC entry / MPTable IOAPIC entry | where external-interrupt routing is programmed |
| Interrupt source overrides | MADT Interrupt Source Override entries | corrects the default ISA IRQ→GSI mapping |
| The Local APIC address | MADT header / MPTable | where each CPU's local APIC MMIO lives |
For ACPI the kernel finds everything by first locating the RSDP (Root System Description Pointer), which points to the RSDT/XSDT, which lists the other tables, of which the MADT (Multiple APIC Description Table) is the one that enumerates CPUs and APICs.
flowchart TD
RSDP["RSDP (Root System Description Pointer)"] --> XSDT["XSDT / RSDT (table directory)"]
XSDT --> MADT["MADT — Local APIC × N, IO APIC, Interrupt Source Overrides"]
XSDT --> Other["(FADT/DSDT/... as needed)"]
MADT --> CPUs["guest brings N CPUs online"]
MADT --> IOAPIC["guest programs interrupt routing"]
Every ACPI table carries a checksum field; the sum of all its bytes must be zero. A wrong
checksum makes the kernel reject the table. This is the single most common ACPI-table bug, and a good
reason the building is centralized in acpi-tables where the checksum logic is written once.
Why ACPI was added and MPTable is deprecated
rg -n "acpi|enable_pci|pvh|ACPI" docs/ CHANGELOG.md
sed -n '1,40p' CHANGELOG.md
MPTable is genuinely legacy — the MultiProcessor Specification predates ACPI by years, modern Linux
treats it as a fallback, and it cannot express things ACPI can (power management, richer device
description, PCI host bridges, memory hotplug). As Firecracker grows features that need richer
firmware-style description — notably the optional PCI transport (--enable-pci) and PVH boot,
plus memory hotplug — MPTable simply isn't enough. ACPI is the standard the rest of the ecosystem
(Cloud Hypervisor, QEMU) already speaks, and it's what these newer features assume. So Firecracker
added ACPI (the acpi-tables crate) and is steering MPTable toward removal.
| Capability | MPTable | ACPI |
|---|---|---|
| Enumerate CPUs + APICs | yes | yes |
| Power management (sleep/shutdown semantics) | no | yes |
| PCI host bridge description | no | yes |
| Memory/device hotplug | no | yes |
| What the modern ecosystem uses | rarely | yes |
Tip: Check your branch's behavior empirically.
rg/CHANGELOGfor the MPTable deprecation and whether ACPI is the default for a plain (non-PCI) boot, because this is exactly the kind of fact that changes release to release. Treat any claim about "the default" as "(verify on your branch)."
The acpi-tables crate
find src/acpi-tables -name "*.rs" | sort
rg -n "struct Rsdp|struct Madt|struct .*Header|fn checksum|trait Aml|fn write|SdtHeader" src/acpi-tables/src/
acpi-tables is a focused crate: it knows how to construct well-formed ACPI tables — the shared
SdtHeader (signature, length, revision, checksum, OEM id), the RSDP, the MADT with its entry types,
and the AML/structure helpers — and to compute the mandatory checksums. The x86_64 arch code calls
into it during build_microvm_for_boot to lay the tables into guest memory at the right addresses,
then the boot configuration tells the kernel where to find the RSDP. Read the crate's table types and
note how every table flows through one checksum routine — that centralization is the whole point of a
dedicated crate.
# Where the arch code invokes the table builders during boot.
rg -n "acpi|Madt|Rsdp|create_acpi_tables|write.*acpi" src/vmm/src/arch/x86_64/ src/vmm/src/builder.rs
Relation to PCI and the PVH future
rg -n "enable_pci|pci|pvh|Pvh|PVH" src/vmm/src/ docs/
Firecracker's default transport is virtio-MMIO, which needs no PCI enumeration — the guest is told
each device's MMIO location on the kernel command line, so a plain microVM can boot with very little
firmware description. But the optional virtio-PCI transport (--enable-pci, verify) and PVH
boot change that: PCI requires the guest to enumerate a PCI host bridge, which is described through
ACPI; PVH is an ACPI-aware boot path. So ACPI is not just a nicer MPTable — it is the enabling
substrate for the PCI/hotplug direction. (The PCI transport is also where CVE-2026-5747 was found
and fixed in 1.14.4/1.15.1 — a reminder that new surface is new risk; see
virtio-transport-mmio.md and the
minimal-device-model philosophy.)
plain microVM (default): virtio-MMIO → device location via cmdline → minimal firmware tables
(CPUs/APIC via MPTable or ACPI MADT)
--enable-pci / PVH: virtio-PCI → guest enumerates PCI host bridge
→ REQUIRES ACPI (MADT + more) → MPTable insufficient
This is why the MPTable→ACPI migration and the MMIO→PCI option are entangled: you cannot have the richer transport without the richer firmware description, and ACPI is that description.
Reading exercise
# 1. The legacy MPTable builder.
find src/vmm/src/arch/x86_64 -name "mptable*"
rg -n "MpTable|mpc_table|mpf_intel|checksum" src/vmm/src/arch/x86_64/
# 2. The acpi-tables crate: RSDP, MADT, the shared header + checksum.
find src/acpi-tables -name "*.rs"
rg -n "Rsdp|Madt|SdtHeader|fn checksum|local_apic|io_apic" src/acpi-tables/src/
# 3. Where the arch/boot code writes the tables into guest memory.
rg -n "acpi|Madt|Rsdp|mptable|create_.*tables" src/vmm/src/arch/x86_64/ src/vmm/src/builder.rs
# 4. The deprecation + PCI/PVH context.
rg -n -i "mptable|acpi|enable_pci|pvh" CHANGELOG.md docs/
# 5. Boot a microVM and from the guest inspect what it parsed:
# cat /proc/cpuinfo | grep -c processor (CPU count the tables advertised)
# dmesg | grep -i "ACPI\|APIC\|MP-table" (what the kernel found at boot)
# 6. Contrast with aarch64, which uses an FDT instead.
rg -n "FdtWriter|fdt|dtb|DRAM_MEM_START" src/vmm/src/arch/aarch64/
Answer:
- Why does Firecracker have to write these tables at all? What provides them on a real PC?
- Name the two formats and their status. Which crate builds the ACPI tables?
- Walk the ACPI table chain RSDP → XSDT → MADT and say what the MADT enumerates.
- What does an ACPI table checksum guarantee, and why is centralizing it in one crate valuable?
- Give three capabilities ACPI has that MPTable lacks, and tie each to a Firecracker feature direction.
- Explain why the MMIO→PCI transport change requires the MPTable→ACPI change.
Common bugs and symptoms
| Symptom | Root cause | Where to look |
|---|---|---|
| Guest sees fewer CPUs than configured | MADT/MPTable Local APIC entries wrong or missing | the per-CPU entry generation; vCPU count plumbing |
| Guest boots but interrupts misbehave | IOAPIC entry or interrupt source override wrong | MADT IO APIC / override entries |
| Kernel rejects a table / ignores ACPI | bad checksum or malformed header | SdtHeader/checksum in acpi-tables |
--enable-pci guest can't find devices | PCI host bridge not described (ACPI incomplete) | ACPI/PCI description; transport code |
| Works on x86_64, breaks on aarch64 | aarch64 uses FDT, not ACPI/MPTable | src/vmm/src/arch/aarch64/ FDT code |
| "Default changed" between branches | MPTable deprecation / ACPI-default flip | CHANGELOG; verify on your branch |
Validation: prove you understand this
- Explain why a BIOS-less VMM must construct firmware tables, and what information they carry.
- Compare MPTable and ACPI: encoding, capability, and Firecracker's deprecation direction.
- Draw the RSDP→XSDT→MADT chain and label what the guest derives from the MADT.
- Explain the ACPI checksum rule and why the
acpi-tablescrate centralizes it. - Justify why ACPI was added by tying three of its capabilities to PCI/PVH/hotplug.
- State how the topology-description story differs on aarch64 and why.
This is the final deep dive in the security/lifecycle/topology arc. Return to the deep-dives index to pick your next thread, or step back to the-boot-sequence.md to see where these tables are written into guest memory during boot.