The Minimal Device Model Philosophy
If you internalize one idea from this entire curriculum, make it this one, because it explains more Firecracker design decisions — and more rejected pull requests — than any other. Every device Firecracker emulates is host code that a hostile guest can attack. A microVM's whole reason to exist is to be a hard isolation boundary for untrusted, multi-tenant code; the VMM is the privileged host process sitting on the inside of that boundary; and the device model is the largest attacker-reachable surface in the VMM. So Firecracker's answer to "should we add this device?" starts from a default of no, and the burden is on the feature to earn its way in.
This is not minimalism for aesthetics. It is a security argument with a number attached: fewer devices means fewer lines of attacker-reachable host code, which means a smaller attack surface, which is the entire value proposition that lets AWS run thousands of mutually-untrusting tenants per host. QEMU emulates hundreds of devices, a BIOS, PCI, USB, sound cards, and a dozen architectures — and every one of those is a place a guest exploit has historically lived. Firecracker bets that you can keep most of QEMU's isolation with a tiny fraction of its exploitable code. This chapter is about that bet and, more importantly, about the culture it creates — because understanding this culture is the difference between a PR that gets merged and one that gets a polite "this does not fit Firecracker's threat model."
Note: This is the most important cultural essay in the section. It builds directly on the threat model, the seccomp deep dive, and the jailer deep dive. Read those for the mechanism; read this for the judgment.
What Firecracker chose not to have
The clearest way to see the philosophy is to list the things a normal PC has that a Firecracker microVM does not:
| Absent | Why it's absent | What replaces it |
|---|---|---|
| BIOS / UEFI firmware | Huge, complex, historically exploit-ridden host code run before the kernel | Direct kernel load — Firecracker copies an uncompressed vmlinux into guest RAM and jumps to e_entry |
| PCI bus enumeration (by default) | Enumeration logic + config space is attack surface; not needed when device locations are fixed | virtio-MMIO: each device at a fixed MMIO address told to the guest via cmdline / FDT |
| USB, sound, GPU, legacy ISA devices | Enormous emulation surface for capabilities serverless doesn't need | Nothing — they simply don't exist |
| A full keyboard controller | Not needed; full i8042 is surface | A partial i8042 that handles only reset/reboot |
| Most of ACPI | Complex tables = surface | A minimal set (RSDP/MADT) only where required; historically MPTable on x86 |
# The entire device catalog. Note how short this list is — that is the point.
ls src/vmm/src/devices/virtio/ # block net vsock balloon rng (entropy) ...
ls src/vmm/src/devices/ # virtio/ + legacy/ + pseudo/
rg -rn "i8042|I8042|reset|reboot" src/vmm/src/devices/legacy/ | head
What is present is a deliberately short list: virtio block, net (over a host TAP), vsock, rng/entropy, balloon, a serial console (16550 UART), and the partial i8042. Newer additions (virtio-pmem, virtio-mem, the opt-in virtio-PCI transport) each had to clear the bar this chapter is about.
Why virtio-MMIO over PCI
The choice of virtio-MMIO as the default transport instead of virtio-PCI is the philosophy in miniature, so it's worth dwelling on. PCI is the universal transport in real hardware and in QEMU; virtio-PCI is what most cloud guests expect. Yet Firecracker defaults to MMIO. Why?
virtio-PCI virtio-MMIO
────────── ───────────
guest probes PCI bus, VMM tells the guest each device's
reads config space, allocates fixed MMIO address + IRQ via the
BARs, sets up interrupts kernel cmdline (x86) or FDT (aarch64)
│ │
▼ ▼
host emulates: PCI host bridge, host emulates: a fixed register block
config space, BAR allocation, per device. That's it.
MSI-X, capability lists ...
= MORE host code = MORE surface = LESS host code = LESS surface
PCI enumeration requires the host to emulate a host bridge, config space, BAR
allocation, and MSI-X — a meaningful chunk of code that exists only so the guest
can discover devices it could just as easily be told about. MMIO skips all of
it: there is no bus to probe, the guest is simply informed where each device sits.
Less discovery machinery, less host code, smaller surface. The cost is
compatibility — some guests and some features assume PCI — which is exactly why the
PCI transport was added later, behind --enable-pci, as an opt-in. And it is no
coincidence that the recent CVE in the transport layer (CVE-2026-5747, fixed
1.14.4 / 1.15.1 — verify) was in the PCI path: more surface, more risk, exactly
as the philosophy predicts.
rg -rn "enable.pci|virtio.pci|MmioTransport|virtio_mmio" src/vmm/src/devices/virtio/ | head
rg -rn "device=|virtio_mmio.device|MMIO_MEM_START|MMIO_LEN" src/vmm/src/ | head
Block-only I/O and the host kernel surface
The device model also protects a surface that is easy to forget: the host kernel. A guest cannot attack only Firecracker; through Firecracker's syscalls it indirectly reaches the host kernel. Every device that drives a complex host subsystem widens that surface too.
This is part of why Firecracker exposes storage as a file-backed block device
and nothing richer. A block device's host side is, at bottom, pread/pwrite (or
io_uring submissions) against a single file descriptor — a narrow, well-understood
host kernel path. Firecracker does not expose filesystem passthrough
(virtio-fs / 9p), which would drag a large, stateful host-kernel filesystem
interaction surface into reach of the guest. Want to share files? Put them in a
block image. The constraint pushes complexity out of the host's trusted path.
# Block I/O bottoms out in a narrow host file path. See the engines.
ls src/vmm/src/devices/virtio/block/virtio/io/ # sync_io.rs async_io.rs mod.rs
rg -n "pread|pwrite|read_exact_at|write_all_at|preadv|pwritev" \
src/vmm/src/devices/virtio/block/virtio/io/sync_io.rs | head
The same logic shows up in the seccomp filter: the set of syscalls Firecracker is allowed to make is tiny and whitelisted per thread category. The device model and the seccomp filter are the same argument from two directions — minimize what the guest can reach, and minimize what the VMM can do if reached.
The bar a new device must clear
When someone proposes a new device (or a richer capability on an existing one), the maintainers evaluate it against the constraints table from the section overview. Concretely, a new device must answer:
- Is it necessary? Not "useful" — necessary for the serverless use case. "QEMU has it" and "it would be convenient" are explicitly not arguments.
- What is the added attack surface? How much new host code becomes guest-reachable? Can it be made smaller? Is it in Rust, memory-safe, fuzzed?
- What does it cost in overhead? Every device adds resident memory and boot- time work, multiplied across thousands of microVMs — see density and boot time.
- What does it do to snapshot compatibility? A new device must serialize via
Persist, tolerate restoring snapshots that predate it, and join the versioned format — see snapshotting. - Does it create a cross-tenant observable? Anything that lets one guest infer something about another (à la KSM) fails.
- Is it covered? Integration tests, and ideally fuzzing
(
docs/fuzzing.md) / Kani harnesses for the parsing paths.
flowchart TD
Prop["proposed device / capability"] --> Nec{"necessary for\nserverless?"}
Nec -- no --> Reject["rejected: 'does not fit'"]
Nec -- yes --> Surf{"surface minimized?\nmemory-safe? fuzzed?"}
Surf -- no --> Rework["rework or reject"]
Surf -- yes --> Snap{"snapshot-compatible?\nlow overhead?\nno cross-tenant leak?"}
Snap -- no --> Rework
Snap -- yes --> Tests{"integration tests +\nfuzz/Kani coverage?"}
Tests -- no --> Rework
Tests -- yes --> Accept["accepted"]
This is a high bar, and it is supposed to be. Most "add device X" proposals die at step 1, and that is the system working as designed.
The tension: features vs. minimalism
The philosophy is not zealotry — Firecracker does add capabilities (the PCI transport, virtio-pmem, virtio-mem, huge pages). The discipline is that each addition is weighed against the surface it costs, often shipped opt-in first, and frequently lands with a design doc that argues the case in public. The honest tension is real:
| Pull toward features | Pull toward minimalism |
|---|---|
| Users want compatibility (PCI, more devices) | Every device is attack surface |
| Workloads want richer I/O (passthrough, accel) | Richer I/O widens the host-kernel surface |
| Ecosystems (Kata, containerd) want parity with QEMU | Parity with QEMU defeats the whole point |
| New hardware wants new device support | Each addition multiplies overhead across the fleet |
Where the line gets drawn is a judgment the maintainers make case by case, and watching them make it — on real issues, in real review threads — is how you learn to make it yourself.
How this shapes what PRs get accepted
This is the practical payoff, and it changes how you contribute:
- A PR that removes surface or narrows a device is welcomed. Tightening a parser, removing dead device code, hardening a virtqueue bound — these align with the grain of the project and tend to merge.
- A PR that adds a device "because it's useful" faces the bar above and usually loses. If you want a new device, open an issue first, argue necessity and surface, get maintainer buy-in, and then write code. Code-first is wasted code.
- A PR that adds surface but pairs it with a smaller-surface design, a fuzz target, a snapshot-compat story, and tests can land — because it has done the maintainers' risk analysis for them.
- "QEMU/Cloud-Hypervisor does it" will be met with silence or a no. Those are different projects with different threat models. The comparison table exists to explain why they diverge, not to argue for parity.
Tip: When you review other people's device PRs, applying this bar substantively — "what's the added surface? where's the fuzz target? what about old snapshots?" — is one of the fastest ways to earn maintainer trust. You are doing the analysis they would otherwise have to.
What good looks like
A contributor who has internalized this philosophy:
- Defaults to no on new surface and makes the feature earn its way in.
- Treats "smaller and safer" as a feature, not a limitation.
- Knows the difference between the serverless use case and general virtualization, and does not try to make Firecracker the latter.
- Can articulate, for any device, its attack surface, its overhead, and its snapshot story — without being asked.
- Reaches for an issue and a design argument before reaching for an editor when the change adds surface.
Hold this chapter next to every other one in this section. Snapshotting is constrained by it (restore must not import surface), density is constrained by it (overhead is surface), boot time benefits from it (fewer devices = faster boot). The minimal device model is not one design decision; it is the design decision, refracted through every subsystem.
Next: see the philosophy pay off in raw I/O performance — I/O Engines — where even the storage fast path is kept to a narrow host file interface.