Level 4: KVM, vCPUs, and the Run Loop
This is the level where Firecracker stops being a clever HTTP service and becomes a virtual machine
monitor. Everything below the API thread you traced in Level 3 — the
VmmAction channel, the EventManager epoll loop, the device managers — exists to set up and feed
one thing: a set of vCPU threads, each spinning in a KVM_RUN loop, executing guest code
directly on the physical CPU until the hardware exits back into Firecracker to ask for help. This
level is about that boundary: the KVM ioctl API, how Firecracker drives it through the rust-vmm
kvm-ioctls crate, the VM exit taxonomy, the in-kernel interrupt controller, and how a vCPU's
CPUID and MSR state is configured (and normalized with CPU templates).
You already wrote a tiny version of this in Lab 1.4:
~70 lines of Rust that opened /dev/kvm, created a VM and a vCPU, mapped a page of guest memory,
loaded a few bytes of machine code, and looped on KVM_RUN printing each exit. That toy is the
skeleton of everything in src/vmm/src/vstate/. The difference between your toy and Firecracker is
not the KVM API — it is the same dozen ioctls. The difference is everything Firecracker layers on
top: a real guest kernel instead of three instructions, a Bus that routes MMIO/PIO exits to virtio
and legacy devices, an in-kernel IRQ chip so the guest can take interrupts without exiting, CPUID
masking for snapshot portability, and a control protocol so the VMM thread can pause and resume vCPUs.
Warning: Do not start Level 4 until Lab 1.4 is behind you and you can trace API → VMM without a guide (Level 3). This level assumes you know what
KVM_RUN, a VM exit, andmmapof thekvm_runshared page are. If those words are fuzzy, re-read the KVM fundamentals deep dive first.
Learning Objectives
By the end of Level 4 you must be able to:
- Name the three KVM fd levels (system → VM → vCPU) and the ioctls that create each, and explain why
guest memory is registered with
KVM_SET_USER_MEMORY_REGIONrather than passed atKVM_RUNtime. - Read Firecracker's vCPU run loop in
src/vmm/src/vstate/vcpu/and explain, without a guide, how it callsVcpuFd::run, matches onVcpuExit, and dispatches PIO/MMIO exits to the device buses. - Distinguish a
KVM_EXIT_IO(PIO) from aKVM_EXIT_MMIOexit: what hardware causes each, how the data is conveyed (io.data_offsetvsmmio.phys_addr/data/len/is_write), and which Firecracker bus handles each. - Explain how the in-kernel irqchip (
KVM_CREATE_IRQCHIP+ LAPIC/IOAPIC/PIT) lets the guest take interrupts without a VM exit, and howKVM_IRQFDandKVM_IOEVENTFDform the virtio fast path. - Explain why a vCPU's CPUID (
KVM_SET_CPUID2) and MSRs must be configured before first run, and why CPU templates exist (consistent guest CPU across heterogeneous hosts; snapshot portability). - Trace how a vCPU error (a failed KVM ioctl, an unhandled exit) propagates from the vCPU thread,
through the
VcpuResponse/event channels, to the VMM thread and the user. - Map the rust-vmm
kvm-ioctlstypes (Kvm,VmFd,VcpuFd,VcpuExit) andkvm-bindingsstructs onto the rawioctl()calls andstruct kvm_runfields they wrap.
The Layer Beneath Everything
KVM (the Kernel-based Virtual Machine) is a Linux kernel module that turns the CPU's
hardware virtualization extensions (Intel VT-x, AMD-V, ARM EL2) into a file-descriptor-and-ioctl
API exposed at /dev/kvm. Firecracker is a userspace program; it cannot run guest code itself. It
asks KVM to do it. The entire relationship is three nested file descriptors and a handful of ioctls:
userspace (Firecracker) kernel (KVM) hardware
┌──────────────────────────┐
│ open("/dev/kvm") │ ── system fd ───► KVM module
│ Kvm │
│ │ KVM_CREATE_VM │ ── VM fd ───────► per-VM state (memory slots, irqchip)
│ ▼ │
│ VmFd │
│ │ KVM_SET_USER_MEMORY_REGION ─────────► guest RAM = host mmap registered as a slot
│ │ KVM_CREATE_IRQCHIP / KVM_IRQFD ─────► in-kernel LAPIC/IOAPIC/PIT
│ │ KVM_CREATE_VCPU │ ── vCPU fd ─────► per-vCPU state (VMCS/VMCB)
│ ▼ │
│ VcpuFd │
│ │ KVM_SET_CPUID2 │ ──────────────► masked CPUID leaves
│ │ KVM_SET_REGS/SREGS │ ──────────────► initial register state
│ │ KVM_RUN ──────────┼──────────────────────────────────────────► runs guest on VT-x/AMD-V
│ │ (blocks) │ │
│ ◄── exit_reason ◄────┼────────────── VM EXIT (IO/MMIO/HLT/...) ◄───┘
│ read kvm_run shared page, handle exit, loop
└──────────────────────────┘
Each KVM_RUN is a trip into the guest: KVM loads the vCPU's saved register state into the physical
CPU, switches into guest mode (VMX non-root / SVM guest), and lets the guest execute at native speed.
It runs until something the hardware can't (or won't) handle by itself — an I/O instruction to an
emulated device, an access to an unmapped MMIO region, a HLT, a triple fault. That event is a VM
exit: the CPU switches back to host mode, KVM fills in struct kvm_run (a page shared between KVM
and Firecracker via mmap), and the KVM_RUN ioctl returns. Firecracker reads kvm_run.exit_reason,
does whatever the guest needed (emulate the device write, return the read data), and calls KVM_RUN
again. That loop — run, exit, handle, run — is the heartbeat of the microVM, and it is the spine of
this entire level. The definitive reference is the kernel's own
Documentation/virt/kvm/api.rst; read it alongside the
vCPU run loop deep dive.
Note: Firecracker never calls
ioctl()directly for these. It uses the rust-vmmkvm-ioctlscrate, which wraps the fds asKvm/VmFd/VcpuFdand returns the exit as a typedVcpuExitenum, andkvm-bindingsfor thestruct kvm_*definitions. Whenever this level names an ioctl, picture thekvm-ioctlsmethod that wraps it (vm.set_user_memory_region(...),vcpu.run(),vcpu.set_cpuid2(...)).
Where Firecracker Keeps vCPU and VM State
Everything in this level lives under one module tree. Find it — don't trust a path you read here:
# The vstate module: VM, vCPU, and guest-memory state.
find src/vmm/src/vstate -name "*.rs" | sort
# The vCPU run loop and the KvmVcpu/Vcpu types.
rg -n "fn run|VcpuExit|fn run_emulation|KVM_RUN" src/vmm/src/vstate/vcpu/
| Module / path | Role |
|---|---|
src/vmm/src/vstate/vm.rs | The Vm type: wraps VmFd, sets up guest memory slots, the irqchip, and holds VM-wide KVM state. |
src/vmm/src/vstate/vcpu/ | The Vcpu (thread + control logic) and the arch KvmVcpu (the VcpuFd and the run/exit dispatch). |
src/vmm/src/vstate/vcpu/mod.rs | The architecture-independent Vcpu: the run loop, VcpuEvent/VcpuResponse handling, pause/resume. |
src/vmm/src/vstate/vcpu/x86_64.rs | x86 KvmVcpu: CPUID/MSR setup, set_regs/set_sregs, the VcpuExit dispatch. (aarch64 sibling exists.) |
src/vmm/src/vstate/memory.rs | Guest memory (GuestMemoryMmap from rust-vmm vm-memory) and slot registration. |
src/vmm/src/vstate/kvm.rs | The Kvm wrapper: opens /dev/kvm, queries capabilities and supported CPUID. (Verify name on your branch.) |
src/vmm/src/cpu_config/ | CPUID/MSR normalization and CPU template logic (static + custom). |
src/vmm/src/arch/x86_64/ | Arch glue: register/segment setup, MSR defaults, layout constants, MPTable/ACPI. |
src/vmm/src/devices/bus.rs (verify) | The Bus abstraction that MMIO/PIO exits are dispatched into. |
Tip: The split between
Vcpu(inmod.rs) andKvmVcpu(in the arch file) is the single most important structural fact of this level.Vcpuis the thread and control plane — it owns theVcpuEvent/VcpuResponsechannels, decides when to run, pause, or exit.KvmVcpuis the KVM plane — it owns theVcpuFd, runsKVM_RUN, and turns oneVcpuExitinto one device dispatch. Keep that boundary in your head; the labs lean on it.
Required Reading
Confirm each exists on your checkout before you start (paths drift between branches):
# Firecracker source you will read this level.
find src/vmm/src/vstate -type f
ls src/vmm/src/cpu_config/ src/vmm/src/arch/x86_64/ 2>/dev/null
# The rust-vmm crate that wraps the ioctls (version pinned in Cargo).
rg -n 'kvm-ioctls|kvm-bindings' Cargo.toml src/vmm/Cargo.toml
| Source / doc | What to extract |
|---|---|
src/vmm/src/vstate/vcpu/mod.rs | The run loop structure: how Vcpu::run drives KvmVcpu, the VcpuEvent state machine (Pause/Resume/Exit), and where exits become VcpuResponses. |
src/vmm/src/vstate/vcpu/x86_64.rs | The match on VcpuExit: IoIn/IoOut/MmioRead/MmioWrite/Hlt/Shutdown/..., and how each is dispatched to a bus. |
src/vmm/src/vstate/vm.rs | Memory-slot registration (KVM_SET_USER_MEMORY_REGION), irqchip creation, VM capabilities. |
src/vmm/src/cpu_config/ + src/vmm/src/arch/x86_64/ | CPUID/MSR configuration, where KVM_SET_CPUID2 is called, and how a CPU template mutates the leaves. |
kvm-ioctls docs (cargo doc -p kvm-ioctls --open) | The Kvm/VmFd/VcpuFd methods and the VcpuExit enum variants — your map from Rust to ioctl. |
Kernel api.rst | The authoritative ioctl + struct kvm_run reference; the exit-reason list. |
| vCPU run loop deep dive | The conceptual model the labs assume. |
| interrupts & irqchip deep dive | Why interrupts mostly don't exit, and how IRQFD/IOEVENTFD work. |
| CPU templates & CPUID deep dive | Why templates exist and how leaves are masked. |
Source Code Areas to Inspect
| Area | Path (verify with find) | Why |
|---|---|---|
| vCPU control + run loop | src/vmm/src/vstate/vcpu/mod.rs | The thread, the event state machine, pause/resume, error propagation. |
| vCPU KVM plane (x86) | src/vmm/src/vstate/vcpu/x86_64.rs | VcpuFd::run, the VcpuExit match, regs/sregs/CPUID/MSR setup. |
| VM + memory + irqchip | src/vmm/src/vstate/vm.rs, memory.rs | Memory-slot registration, KVM_CREATE_IRQCHIP, VM caps. |
| CPU config / templates | src/vmm/src/cpu_config/, cpu-template-helper crate | CPUID/MSR normalization; static + custom templates. |
| Arch glue | src/vmm/src/arch/x86_64/ | Register/segment defaults, MSR list, layout constants, interrupt setup. |
| The bus | src/vmm/src/devices/ (bus.rs + the device managers) | Where a PIO/MMIO exit is routed to a device by address. |
| rust-vmm wrappers | the kvm-ioctls / kvm-bindings crates (external) | What every vcpu.* / vm.* call actually does. |
Key Types Quick Reference
Locate each by role, not line number. Run the command, read what it finds.
| Type | Where (find it) | Role |
|---|---|---|
Vcpu | rg -n "struct Vcpu\b|impl Vcpu\b" src/vmm/src/vstate/vcpu/mod.rs | The vCPU thread + control plane; owns the event channels and run loop. |
KvmVcpu | rg -n "struct KvmVcpu|impl KvmVcpu" src/vmm/src/vstate/vcpu/ | The KVM plane: wraps VcpuFd, runs KVM_RUN, dispatches exits. |
VcpuEvent / VcpuResponse | rg -n "enum VcpuEvent|enum VcpuResponse" src/vmm/src/vstate/vcpu/ | The pause/resume/exit control protocol between VMM and vCPU threads. |
Vm | rg -n "struct Vm\b|impl Vm\b" src/vmm/src/vstate/vm.rs | Wraps VmFd; memory slots, irqchip, VM-wide state. |
Kvm (FC wrapper) | rg -n "struct Kvm\b" src/vmm/src/vstate/ | Opens /dev/kvm; supported CPUID, capability checks. |
VcpuExit | rg -n "VcpuExit::" src/vmm/src/vstate/vcpu/ | The rust-vmm exit enum the run loop matches on. |
CpuConfiguration / template types | rg -n "CpuConfiguration|CpuTemplate|StaticCpuTemplate|CustomCpuTemplate" src/vmm/src/cpu_config/ | The CPUID/MSR config and the template that mutates it. |
VcpuError (or per-arch error enums) | rg -n "enum .*Error" src/vmm/src/vstate/vcpu/ | How vCPU/KVM failures are typed before they propagate. |
GitHub Issue Categories for Level 4
The kinds of real issues a Level 4 graduate can credibly take on (find them by label):
gh issue list --repo firecracker-microvm/firecracker --label "Type: Bug" --search "vcpu OR kvm OR cpuid OR MSR OR exit" --state all
gh issue list --repo firecracker-microvm/firecracker --search "CPU template" --state all
| Category | Example shape |
|---|---|
| Poor error context on a KVM ioctl failure | A KVM_RUN/SET_CPUID2 failure surfaces as a bare errno; add context naming the ioctl and arch. |
Unhandled / mishandled VcpuExit variant | A new or rarely-hit exit reason logged as "unexpected" instead of handled or cleanly errored. |
| CPUID/MSR normalization bug | A leaf not masked, breaking snapshot portability or a static template. |
| CPU template correctness | A static template missing a feature flag fixup; a custom-template validation gap. |
| vCPU state save/restore mismatch | An MSR or CPUID entry not round-tripped across snapshot, breaking restore on a different host. |
Deliverables
Demonstrate all of these before advancing to Level 5:
-
A read-through of
src/vmm/src/vstate/vcpu/with amermaid/ASCII map of one run-loop iteration, and theVcpuExitvariants matched, with file citations (Lab 4.1). -
A boot trace that counts and categorizes VM exits (PIO vs MMIO vs HLT), with PIO exits tied to
0x3f8/0x60-0x64and MMIO exits tied to virtio-MMIO register offsets (Lab 4.2). -
A dump of the host CPUID/MSR config with
cpu-template-helper, a static template applied via/machine-config, and the guest's masked CPUID observed from inside (Lab 4.3). -
A reproduced vCPU/KVM edge-case bug, a fix with proper error context, and a unit test in
vstatethat pins it (Lab 4.4). - From memory: the three fd levels, the four most common exit reasons, why interrupts mostly don't exit, and why CPU templates exist.
Common Mistakes
| Mistake | Consequence | Fix |
|---|---|---|
Thinking KVM_RUN returns once per guest instruction | You misjudge performance; you expect to see every access | It runs guest code at native speed and only returns on an exit — most instructions never exit |
| Conflating PIO and MMIO exits | You look for serial data in mmio.data or virtio writes in io.data | PIO = KVM_EXIT_IO, data at kvm_run + io.data_offset; MMIO = KVM_EXIT_MMIO, data in mmio.data[8] |
Confusing Vcpu with KvmVcpu | You look for the run loop in the wrong file | Vcpu = thread/control (mod.rs); KvmVcpu = the VcpuFd + exit dispatch (arch file) |
| Assuming every interrupt is a VM exit | You misunderstand the fast path | The in-kernel irqchip + KVM_IRQFD inject interrupts without a userspace exit |
Calling KVM_RUN before setting CPUID/regs | Guest triple-faults or sees a wrong CPU | CPUID/MSRs/regs/sregs are set before first run; templates mutate CPUID pre-run |
| Treating a KVM errno as the whole story | Useless bug reports; hard-to-debug failures | Wrap the failure with the ioctl name and arch context before it propagates |
| Reading only x86 | You miss the aarch64 sibling and write x86-only fixes | vstate/vcpu/ has per-arch files; check both when changing the run loop |
How to Verify Success
# 1. You can locate the run loop and the exit match by grep, not memory.
rg -n "fn run|VcpuExit::(IoIn|IoOut|MmioRead|MmioWrite|Hlt|Shutdown)" src/vmm/src/vstate/vcpu/
# 2. You can find where guest memory is registered and the irqchip created.
rg -n "set_user_memory_region|create_irqchip|KVM_CREATE_IRQCHIP" src/vmm/src/vstate/
# 3. You can find where CPUID is pushed into the vCPU and where templates apply.
rg -n "set_cpuid2|SET_CPUID2|CpuConfiguration|apply.*[Tt]emplate" src/vmm/src/
# 4. You can dump a CPU template from the host with the tooling.
ls cpu-template-helper 2>/dev/null || find . -path "*cpu-template-helper*" -name "*.rs" | head
When you can run those four blocks, explain what each line of output means, and answer the validation questions in all four labs without notes, you have Level 4.
PR Profile: Level 4 Graduate
What a graduate of this level can credibly open:
| PR type | Example |
|---|---|
| Error-context fix | Wrap a KVM_RUN/SET_CPUID2/SET_MSRS failure with the ioctl name + arch + value, with a test. |
| Unhandled-exit hardening | Turn an "unexpected VcpuExit" log into a typed, tested error or a correct handler. |
| CPUID/MSR normalization | Mask or fix a leaf that breaks a static template or snapshot portability, with a unit test. |
| CPU-template fixup | Add/correct a feature-flag fixup in a static template; validate a custom template field. |
| Run-loop refactor (small) | Extract a clearer dispatch helper in vstate/vcpu/ with no behavior change, keeping both arches in sync. |
| Documentation | Improve the run-loop / exit comments or docs/ after proving the behavior with a trace. |
These are the bread-and-butter of vstate contribution — small, sharply-scoped, test-backed changes
to the layer where Firecracker meets KVM. The instinct you build here (read the loop, instrument the
exits, name the failure precisely) is exactly what Level 8's real-issue contribution demands.
Begin with Lab 4.1: Read the vCPU Run Loop.