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:

AbsentWhy it's absentWhat replaces it
BIOS / UEFI firmwareHuge, complex, historically exploit-ridden host code run before the kernelDirect 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 fixedvirtio-MMIO: each device at a fixed MMIO address told to the guest via cmdline / FDT
USB, sound, GPU, legacy ISA devicesEnormous emulation surface for capabilities serverless doesn't needNothing — they simply don't exist
A full keyboard controllerNot needed; full i8042 is surfaceA partial i8042 that handles only reset/reboot
Most of ACPIComplex tables = surfaceA 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:

  1. Is it necessary? Not "useful" — necessary for the serverless use case. "QEMU has it" and "it would be convenient" are explicitly not arguments.
  2. 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?
  3. 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.
  4. 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.
  5. Does it create a cross-tenant observable? Anything that lets one guest infer something about another (à la KSM) fails.
  6. 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 featuresPull 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 QEMUParity with QEMU defeats the whole point
New hardware wants new device supportEach 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.