KVM ioctl Cheat-Sheet
Everything Firecracker does to run a guest is, at the bottom, an ioctl() on a file descriptor
under /dev/kvm. This page is the lookup table for those ioctls: which fd level each one applies
to (system, VM, or vCPU), what it does in one line, and the kvm-ioctls wrapper Firecracker calls
instead of the raw ioctl. It closes with the VcpuExit taxonomy — the reasons KVM_RUN returns
to userspace and the kvm_run fields each carries — because decoding a VM exit is the single most
common thing you do reading the vCPU run loop.
Reach for this when you are reading the run loop, writing a minimal KVM VMM by hand in
Lab 1.4, or staring at a KVM_EXIT reason you
don't recognize.
Warning: ioctl numbers (like
KVM_GET_API_VERSION = 12) are stable kernel ABI and safe to cite; thekvm_runstruct fields, capability checks, and which exits Firecracker actually handles are version-sensitive across kernel andkvm-ioctlsversions. Confirm the wrapper API and exit enum against the crate Firecracker pins, not your memory:# The exact kvm-ioctls / kvm-bindings version Firecracker pins (verify on your branch): rg -n 'name = "kvm-ioctls"' -A2 Cargo.lock # Browse the real wrapper API and the VcpuExit enum: cargo doc -p kvm-ioctls --open # Kvm / VmFd / VcpuFd / VcpuExit cargo doc -p kvm-bindings --open # raw kvm_run, kvm_regs, kvm_sregs, … # Where Firecracker drives the run loop and matches on VcpuExit: rg -n "VcpuExit|fn run|KVM_RUN" src/vmm/src/vstate/vcpu/
For the conceptual model — VT-x/AMD-V, EPT/NPT, why the guest is untrusted — read the KVM fundamentals deep dive. For the Rust-wrapper view of these exact calls, read kvm-ioctls and kvm-bindings.
The three fd levels
KVM is a hierarchy of file descriptors. You open one, use it to create the next, and each ioctl is
valid only on the right level. Get this hierarchy wrong and the ioctl returns EINVAL.
open("/dev/kvm") ── SYSTEM fd ── Kvm (capabilities, API version, create a VM)
│ KVM_CREATE_VM
▼
VM fd ── VM fd ────── VmFd (guest memory, IRQ chip, devices, create vCPUs)
│ KVM_CREATE_VCPU
▼
vCPU fd ── vCPU fd ──── VcpuFd (registers, CPUID, KVM_RUN — runs guest code)
| Level | Created by | rust-vmm type | Owns |
|---|---|---|---|
| System | open("/dev/kvm") | Kvm | API version, capability queries, KVM_GET_VCPU_MMAP_SIZE, creating VMs |
| VM | KVM_CREATE_VM (on the system fd) | VmFd | Guest memory regions, the in-kernel IRQ chip, irqfd/ioeventfd, creating vCPUs |
| vCPU | KVM_CREATE_VCPU (on the VM fd) | VcpuFd | Per-CPU registers, CPUID, the kvm_run shared page, and KVM_RUN itself |
In Firecracker, Kvm/VmFd are set up by vstate/vm.rs and the vCPU is owned by vstate/vcpu/
(rg -n "struct Vm\b|Kvm::new|create_vm" src/vmm/src/vstate/vm.rs).
System-fd ioctls (the Kvm handle)
| ioctl | Number | What it does | kvm-ioctls wrapper (Kvm) |
|---|---|---|---|
KVM_GET_API_VERSION | 12 | Returns the KVM ABI version; must be 12 or you abort — the only hard versioning check | Kvm::new() validates it internally |
KVM_CREATE_VM | — | Creates a VM, returns the VM fd | Kvm::create_vm() -> VmFd |
KVM_GET_VCPU_MMAP_SIZE | — | Size of the per-vCPU kvm_run shared page to mmap | Kvm::get_vcpu_mmap_size() |
KVM_CHECK_EXTENSION | — | Query whether a capability (e.g. an IRQ chip, ioeventfd) is supported | Kvm::check_extension(cap) |
KVM_GET_SUPPORTED_CPUID | — | The host CPUID entries KVM can expose to a guest (the basis for CPU templates) | Kvm::get_supported_cpuid(n) |
Note:
KVM_GET_SUPPORTED_CPUIDis a system-level query (what the host/KVM can offer), but it is applied per-vCPU withKVM_SET_CPUID2. Firecracker reads the supported set, runs it through a CPU template to normalize features across heterogeneous hosts, then sets the filtered result on each vCPU.
VM-fd ioctls (the VmFd handle)
| ioctl | What it does | kvm-ioctls wrapper (VmFd) | Deep dive |
|---|---|---|---|
KVM_CREATE_VCPU | Create a vCPU, return its vCPU fd | VmFd::create_vcpu(id) -> VcpuFd | vcpu run loop |
KVM_SET_USER_MEMORY_REGION | Register a host mmap as guest RAM: slot, guest_phys_addr, memory_size, userspace_addr | VmFd::set_user_memory_region(region) | guest memory |
KVM_CREATE_IRQCHIP | Create the in-kernel interrupt controller (x86 PIC/IOAPIC/LAPIC) so most IRQs avoid a userspace exit | VmFd::create_irq_chip() | interrupts & irqchip |
KVM_IRQFD | Bind an eventfd to a guest IRQ line: VMM writes the eventfd → KVM injects the interrupt, no exit | VmFd::register_irqfd(&eventfd, gsi) | interrupts & irqchip |
KVM_IOEVENTFD | Turn a guest MMIO/PIO write to a chosen address into an eventfd signal — no exit to userspace | VmFd::register_ioevent(&eventfd, &addr, data) | virtio transport (MMIO) |
KVM_SET_TSS_ADDR / KVM_SET_IDENTITY_MAP_ADDR | x86 setup KVM needs before running a vCPU | VmFd::set_tss_address(...) | kvm fundamentals |
Tip — the two fast paths.
KVM_IOEVENTFDandKVM_IRQFDare the heart of virtio performance. A guest "kick" (write to the virtqueueQueueNotifyregister) is registered as an ioeventfd, so the notify becomes an eventfd the VMM's EventManager epoll loop wakes on — without aKVM_EXIT_MMIOround-trip to userspace. The device's completion interrupt goes back the other way through an irqfd. If virtio went through plain MMIO exits for every notify and every interrupt, it would be far slower. See Lab 7.2.
vCPU-fd ioctls (the VcpuFd handle)
| ioctl | What it does | kvm-ioctls wrapper (VcpuFd) |
|---|---|---|
KVM_RUN | Enter the guest; blocks until a VM exit, then returns with kvm_run.exit_reason set | VcpuFd::run() -> VcpuExit |
KVM_GET_REGS / KVM_SET_REGS | General-purpose registers (rax…r15, rip, rsp, rflags) | VcpuFd::get_regs() / set_regs(®s) |
KVM_GET_SREGS / KVM_SET_SREGS | Special registers: cr0/cr3/cr4, efer, segment selectors, GDT/IDT — used to put the vCPU in long mode | VcpuFd::get_sregs() / set_sregs(&sregs) |
KVM_GET_SUPPORTED_CPUID (set side) KVM_SET_CPUID2 | Apply the (template-filtered) CPUID the guest will see | VcpuFd::set_cpuid2(&cpuid) |
KVM_GET_MSRS / KVM_SET_MSRS | Read/write Model-Specific Registers (also normalized by CPU templates) | VcpuFd::get_msrs(...) / set_msrs(...) |
KVM_GET_FPU / KVM_SET_FPU | Floating-point / SSE state (matters for snapshot/restore) | VcpuFd::get_fpu() / set_fpu(...) |
KVM_GET_LAPIC / KVM_SET_LAPIC | Local APIC state (x86; snapshot/restore) | VcpuFd::get_lapic() / set_lapic(...) |
#![allow(unused)] fn main() { // The shape of the run loop these ioctls form (illustrative — rg the real one): // rg -n "fn run|match .*VcpuExit" src/vmm/src/vstate/vcpu/ loop { match vcpu_fd.run()? { // KVM_RUN: blocks until a VM exit VcpuExit::IoOut(addr, data) => { /* service a PIO write (e.g. serial) */ } VcpuExit::MmioRead(addr, data) => { /* a device register read */ } VcpuExit::Hlt => break, // guest executed HLT VcpuExit::Shutdown => break, // triple fault / reset other => { /* log + handle or bail */ } } } }
Note: Initial register state at boot is set with these ioctls. For an x86
vmlinux, Firecracker usesKVM_SET_SREGSto enter long mode directly (cr0PE|PG,cr4PAE,eferLME|LMA, identity page tables) andKVM_SET_REGSto setrip = e_entryandrsi = ZERO_PAGE_START— skipping 16-bit real mode entirely. See the boot sequence and the memory layout cheat-sheet.
The kvm_run shared page
KVM_RUN does not pass data by argument — it shares a page. After KVM_CREATE_VCPU, the VMM mmaps
a region of KVM_GET_VCPU_MMAP_SIZE bytes over the vCPU fd; that maps struct kvm_run. On every
return from KVM_RUN you read kvm_run.exit_reason, then read the union field that matches it.
kvm-ioctls hides this: VcpuFd::run() reads the page for you and returns a typed VcpuExit.
mmap(kvm_run_mmap_size, vcpu_fd) ──► struct kvm_run { exit_reason; union { io; mmio; ... } }
▲
KVM_RUN writes exit_reason + the matching union member here
VcpuExit reasons (the taxonomy you dispatch on)
This is the table you keep open while reading the run loop. Each row is a raw KVM_EXIT_* reason, the
kvm-ioctls VcpuExit variant Firecracker matches on, the kvm_run fields that carry the payload,
and what the VMM does. PIO = x86 in/out (KVM_EXIT_IO); MMIO = a load/store to a device
register window (KVM_EXIT_MMIO).
KVM_EXIT_* | VcpuExit variant | kvm_run fields used | What it means / VMM does |
|---|---|---|---|
KVM_EXIT_IO (in) | IoIn(port, data) | io.direction=IN, io.port, io.size, io.count, data at (char*)run + io.data_offset | Guest in from a PIO port (e.g. serial read); VMM fills data |
KVM_EXIT_IO (out) | IoOut(port, data) | io.direction=OUT, io.port, io.size, data at io.data_offset | Guest out to a PIO port (e.g. serial console write); VMM consumes data |
KVM_EXIT_MMIO (read) | MmioRead(addr, data) | mmio.phys_addr, mmio.len, mmio.is_write=0, mmio.data[8] | Guest read a device MMIO register; VMM fills mmio.data |
KVM_EXIT_MMIO (write) | MmioWrite(addr, data) | mmio.phys_addr, mmio.len, mmio.is_write=1, mmio.data[8] | Guest wrote a device MMIO register; VMM dispatches to the device |
KVM_EXIT_HLT | Hlt | — | Guest executed HLT; usually means the guest is done / idle. VMM stops the vCPU |
KVM_EXIT_SHUTDOWN | Shutdown | — | Triple fault / reset request (e.g. guest reboot). VMM tears the microVM down |
KVM_EXIT_FAIL_ENTRY | FailEntry(reason, cpu) | fail_entry.hardware_entry_failure_reason | The CPU could not enter the guest — usually a bad VMCS/VMCB or invalid initial state. A bug, not normal flow |
KVM_EXIT_INTERNAL_ERROR | InternalError | internal.suberror (+ internal.data[]) | KVM hit an internal error (e.g. an emulation failure). Firecracker logs and aborts the vCPU |
KVM_EXIT_INTR | Intr | — | A signal interrupted KVM_RUN (e.g. the VMM asked the vCPU to pause). Re-enter or handle the pause |
KVM_EXIT_SYSTEM_EVENT | SystemEvent(type, data) | system_event.type (SHUTDOWN/RESET/…) | An ACPI/PSCI-style power event (reset/shutdown); common on aarch64 reboot |
KVM_EXIT_UNKNOWN | Unknown | hw.hardware_exit_reason | An exit reason the wrapper doesn't model; investigate |
Tip: The exits you'll see constantly in a healthy microVM are
IoOut/IoIn(serial console, legacy PIO),MmioRead/MmioWrite(virtio device config registers — the configuration path, not the data fast path, which goes through ioeventfd), and eventuallyHlt/Shutdownat teardown.FailEntryandInternalErrorare never normal — if you see them you have a setup bug (bad register state, an unmapped page the guest jumped to, an emulation hole). Build the full taxonomy hands-on in Lab 4.2 and../masterclass/kvm-and-vcpus/lab-02-vm-exit-taxonomy.md.
A minimal VMM in ioctls (the whole arc on one page)
Every box below is one ioctl from the tables above. This is exactly the skeleton you build by hand in
Lab 1.4 before you ever open Firecracker's Vcpu.
flowchart TD
A["open(/dev/kvm) — Kvm"] --> B["KVM_GET_API_VERSION == 12"]
B --> C["KVM_CREATE_VM — VmFd"]
C --> D["mmap guest RAM + KVM_SET_USER_MEMORY_REGION"]
D --> E["KVM_CREATE_VCPU — VcpuFd"]
E --> F["KVM_GET_VCPU_MMAP_SIZE + mmap kvm_run"]
F --> G["KVM_SET_SREGS / KVM_SET_REGS — initial state (long mode, rip)"]
G --> H["KVM_SET_CPUID2 — guest-visible features"]
H --> I["loop: KVM_RUN"]
I --> J{"VcpuExit?"}
J -->|IoOut/IoIn| K["service PIO"]
J -->|MmioRead/Write| L["dispatch to device"]
J -->|Hlt/Shutdown| M["stop"]
K --> I
L --> I
Reading exercise
From a Firecracker checkout (and with cargo doc -p kvm-ioctls open):
# 1. Where does Firecracker create the Kvm/VmFd and check the API version?
rg -n "Kvm::new|create_vm|check_extension|get_api_version" src/vmm/src/vstate/vm.rs
# 2. Where is the run loop, and which VcpuExit variants does it match?
rg -n "fn run|match .*VcpuExit|VcpuExit::" src/vmm/src/vstate/vcpu/
# 3. Where is guest memory registered with KVM_SET_USER_MEMORY_REGION?
rg -n "set_user_memory_region|GuestMemoryMmap|create_guest_memory" src/vmm/src/vstate/memory.rs
# 4. Where are ioeventfd / irqfd registered (the virtio fast path)?
rg -n "register_ioevent|register_irqfd|IoEventAddress" src/vmm/src/
Questions to answer from what you find:
- Which fd level does each of
KVM_CREATE_VCPU,KVM_SET_USER_MEMORY_REGION, andKVM_RUNrequire, and what is thekvm-ioctlstype that owns each? - When a guest writes a virtio device's
QueueNotifyregister, does that produce aKVM_EXIT_MMIOexit or an ioeventfd signal — and why does the answer matter for performance? - Which two
VcpuExitvariants should never appear in a healthy microVM, and what class of bug does each indicate? - For a
MmioWriteexit, whichkvm_run.mmiofields tell the VMM the address, length, and that it is a write?
Related references
- Memory Layout Cheat-Sheet — what the addresses in
KVM_SET_USER_MEMORY_REGIONand the boot registers point at. - Key Types by Crate —
Vcpu/KvmVcpu/Vmand where the run loop lives. - API Endpoint Map —
InstanceStartis what ultimately drives the vCPU threads intoKVM_RUN. - KVM fundamentals and vCPU run loop & VM exits — the prose behind this table.
- kvm-ioctls and kvm-bindings — the Rust wrapper crate
view, with
cargo doc.
Next: Memory Layout Cheat-Sheet — where in guest physical memory each of these addresses lives.