kvm-ioctls and kvm-bindings
KVM is an ioctl interface. To create a VM you ioctl(kvm_fd, KVM_CREATE_VM);
to run a vCPU you ioctl(vcpu_fd, KVM_RUN); to register guest RAM you fill a
struct kvm_userspace_memory_region and ioctl(vm_fd, KVM_SET_USER_MEMORY_REGION, ®ion). Doing that in Rust means two things: a set of C struct definitions
that exactly match the kernel's linux/kvm.h, and a layer of safe wrappers so
the rest of the VMM never writes a raw ioctl() by hand. rust-vmm splits those
into two crates — kvm-bindings (the raw FFI structs) and kvm-ioctls
(the safe RAII wrappers) — and Firecracker builds its entire vstate/ machinery
on top of both. This is the foundation of KVM fundamentals
and the vCPU run loop; here you
learn the crates underneath those chapters.
After this chapter you can: explain what each crate provides and why they are
separate; create a VM, register memory, and create + run a vCPU using
kvm-ioctls; read the VcpuExit enum and map it to KVM exit reasons; find where
Firecracker's vstate/ wraps these types; and explain the serde feature that
makes kvm-bindings structs snapshot-serializable.
Note: These crates are the thinnest possible safe layer over the kernel. They add no policy — no memory layout, no boot protocol, no device model. They hand you
Kvm/VmFd/VcpuFdand the raw structs, and get out of the way. Everything interesting is what Firecracker builds on top.
Two crates, one boundary
# Confirm both are dependencies and see the pinned versions (verify on your branch):
rg -n "kvm-ioctls|kvm-bindings" Cargo.toml src/vmm/Cargo.toml Cargo.lock
# Read the pinned-version docs locally — better than docs.rs/latest:
cargo doc -p kvm-ioctls -p kvm-bindings --no-deps --open
| Crate | What it is | docs |
|---|---|---|
kvm-bindings | bindgen-generated Rust definitions of the KVM uapi: kvm_run, kvm_regs, kvm_sregs, kvm_userspace_memory_region, kvm_cpuid2, kvm_msrs, and the ioctl constants. Pure data, #[repr(C)], mostly unsafe-to-fill. | docs.rs/kvm-bindings |
kvm-ioctls | Safe RAII wrappers that own file descriptors and call the ioctls for you: Kvm, VmFd, VcpuFd, DeviceFd, the VcpuExit enum, KvmRunWrapper. | docs.rs/kvm-ioctls |
The split exists because they have different lifecycles. kvm-bindings tracks the
kernel uapi — when KVM adds a field or an ioctl, bindgen regenerates. It is
mechanical and architecture-specific (kvm-bindings has x86_64 and arm64
module trees). kvm-ioctls tracks the safe API — ergonomic wrappers, error
handling, the VcpuExit enum — and changes when the abstraction improves, not
when the kernel does. You will touch kvm-bindings when you need a struct the
wrapper doesn't expose; you will touch kvm-ioctls for everything routine.
your VMM code (Firecracker vstate/)
│ Kvm / VmFd / VcpuFd / VcpuExit ← safe, owns fds, RAII
▼
┌─────────────────────┐
│ kvm-ioctls │ ioctl_with_ref!(vm_fd, KVM_SET_USER_MEMORY_REGION, &r)
└─────────────────────┘
│ raw #[repr(C)] structs + ioctl numbers
▼
┌─────────────────────┐
│ kvm-bindings │ struct kvm_run { ... }, KVM_RUN = _IO(KVMIO, 0x80), ...
└─────────────────────┘
│ syscall
▼
/dev/kvm (Linux KVM)
kvm-ioctls: the RAII fd hierarchy
The three-level KVM fd hierarchy from KVM fundamentals — system → VM → vCPU — maps to three RAII types. Each owns its file descriptor and closes it on drop.
| Type | Wraps | Created by | Key methods |
|---|---|---|---|
Kvm | the system fd (open("/dev/kvm")) | Kvm::new() | create_vm(), get_api_version(), check_extension(), get_vcpu_mmap_size(), get_supported_cpuid() |
VmFd | the VM fd (KVM_CREATE_VM) | kvm.create_vm() | create_vcpu(id), set_user_memory_region(region), create_irq_chip(), register_irqfd(), register_ioevent(), get_dirty_log() |
VcpuFd | the vCPU fd (KVM_CREATE_VCPU) | vm.create_vcpu(id) | run(), get_regs()/set_regs(), get_sregs()/set_sregs(), set_cpuid2(), get_msrs()/set_msrs(), set_one_reg() (aarch64) |
DeviceFd | an in-kernel device fd (KVM_CREATE_DEVICE) | vm.create_device() | set_device_attr(), get_device_attr() — used for the aarch64 GIC |
The crown jewel is VcpuExit. VcpuFd::run() calls ioctl(vcpu_fd, KVM_RUN),
blocks while the guest executes on the physical CPU, and on return decodes
kvm_run.exit_reason into a Rust enum you match on:
# Find the exit variants the crate defines (verify the exact set on your version):
cargo doc -p kvm-ioctls --no-deps --open # then search VcpuExit
# Or grep the unpacked source:
find ~/.cargo/registry/src -maxdepth 2 -type d -name 'kvm-ioctls-*' \
-exec rg -n "enum VcpuExit|IoIn|IoOut|MmioRead|MmioWrite|Hlt|Shutdown|FailEntry|InternalError" {} +
VcpuExit variant | KVM exit reason | Firecracker handles it by |
|---|---|---|
IoIn(port, data) / IoOut(port, data) | KVM_EXIT_IO (PIO) | dispatching to the PIO bus (serial, i8042) |
MmioRead(addr, data) / MmioWrite(addr, data) | KVM_EXIT_MMIO | dispatching to the MMIO bus (virtio-mmio devices) |
Hlt | KVM_EXIT_HLT | treating as a guest halt |
Shutdown | KVM_EXIT_SHUTDOWN | initiating microVM teardown |
FailEntry / InternalError | hardware/KVM error | logging + erroring out the vCPU |
This enum is the literal API of the vCPU run loop. When Firecracker's
vCPU run loop does match self.fd.run()? { VcpuExit::MmioWrite(addr, data) => ... }, that match arm is a
kvm-ioctls variant.
A worked example: a VM and a vCPU from nothing
This is the smallest real program that creates a VM, gives it a page of guest memory containing a tiny bit of code, and runs a vCPU until it exits. It is the exact shape of Lab R1 and of Firecracker's own setup, stripped to the bone.
use kvm_bindings::kvm_userspace_memory_region; use kvm_ioctls::{Kvm, VcpuExit}; fn main() { // 1. System level: open /dev/kvm. let kvm = Kvm::new().expect("open /dev/kvm"); assert_eq!(kvm.get_api_version(), 12); // KVM_GET_API_VERSION is always 12 // 2. VM level: KVM_CREATE_VM. let vm = kvm.create_vm().expect("KVM_CREATE_VM"); // 3. Guest memory: mmap one page on the host, register it with KVM at GPA 0x1000. const MEM_SIZE: usize = 0x4000; let load_addr = unsafe { libc::mmap( std::ptr::null_mut(), MEM_SIZE, libc::PROT_READ | libc::PROT_WRITE, libc::MAP_ANONYMOUS | libc::MAP_SHARED | libc::MAP_NORESERVE, -1, 0, ) as *mut u8 }; let mem_region = kvm_userspace_memory_region { slot: 0, guest_phys_addr: 0x1000, memory_size: MEM_SIZE as u64, userspace_addr: load_addr as u64, flags: 0, }; // set_user_memory_region is unsafe: you promise the host mapping is valid. unsafe { vm.set_user_memory_region(mem_region).expect("register memory"); } // 4. vCPU level: KVM_CREATE_VCPU, then set up registers (omitted: real-mode // code bytes copied into the page, segment/CR setup). See Lab R1. let mut vcpu = vm.create_vcpu(0).expect("KVM_CREATE_VCPU"); // 5. The run loop — this is the heart of every VMM. loop { match vcpu.run().expect("KVM_RUN") { VcpuExit::IoOut(port, data) => { println!("guest wrote {:?} to port {:#x}", data, port); } VcpuExit::Hlt => { println!("guest halted"); break; } VcpuExit::MmioWrite(addr, data) => { println!("guest MMIO write @ {:#x}: {:?}", addr, data); } exit => { println!("unhandled exit: {:?}", exit); break; } } } }
Note the two unsafe blocks. mmap is unsafe because you are handing KVM a raw
host pointer; set_user_memory_region is unsafe because you are promising the
mapping outlives the region registration and is valid for memory_size bytes. The
rest is safe — create_vm, create_vcpu, run, the VcpuExit match all carry
Rust's safety guarantees. That boundary — unsafe only at the memory-registration
seam — is the entire value proposition of kvm-ioctls.
Warning:
set_user_memory_regionbeingunsafeis not ceremony. If the host mapping is freed while KVM still has the region registered, the guest can make KVM touch freed host memory — a host-side use-after-free. Firecracker's guest memory management exists precisely to make that lifetime correct.vm-memory(vm-memory.md) owns the mappings so this seam stays sound.
How Firecracker wraps these in vstate/
Firecracker never sprinkles raw kvm-ioctls calls through the codebase. It wraps
the three fds in its own vstate/ types so the rest of the VMM talks to
Firecracker abstractions, and the KVM details stay in one place.
# The KVM/VM/vCPU state lives under vstate/ — find the wrapper types:
rg -n "use kvm_ioctls|Kvm::new|create_vm|create_vcpu|VcpuFd|VmFd" src/vmm/src/vstate/
rg -n "struct Vm\b|struct KvmVcpu|struct Vcpu\b|self\.fd\.run\(\)|VcpuExit" src/vmm/src/vstate/
# Where kvm-bindings structs are filled (regs, sregs, memory region):
rg -n "kvm_userspace_memory_region|kvm_regs|kvm_sregs|set_user_memory_region|set_sregs|set_regs" src/vmm/src/
| FC type (verify on your branch) | Wraps | Role |
|---|---|---|
Vm (vstate/vm.rs) | a VmFd | owns the VM fd, registers guest memory regions, sets up the irqchip |
KvmVcpu / Vcpu (vstate/vcpu/) | a VcpuFd | owns the vCPU fd, runs the KVM_RUN loop, saves/restores vCPU state |
| (the system handle) | a Kvm | created once, queried for capabilities and supported CPUID |
The pattern is: kvm-ioctls gives the safe fd, Firecracker's vstate/ gives the
policy — which memory regions to register, what CPUID to set
(CPU templates), how to translate a
VcpuExit into a device-bus dispatch, how to serialize the vCPU for a snapshot.
That last point connects directly to the next feature.
The serde feature and snapshots
Firecracker's snapshotting has to save and
restore the exact KVM state of a paused vCPU: its registers (kvm_regs),
special registers (kvm_sregs), CPUID, MSRs, the LAPIC, the FPU, the xsave area —
all of which are kvm-bindings structs. To serialize them, kvm-bindings exposes
a serde feature that derives Serialize/Deserialize on those #[repr(C)]
structs. Firecracker turns it on so its snapshot code can write vCPU state to the
microVM-state file and read it back.
# See whether FC enables the serde feature on kvm-bindings:
rg -n "kvm-bindings" Cargo.toml src/vmm/Cargo.toml # look for features = ["serde", ...] / "fam-wrappers"
# Where vCPU KVM state gets (de)serialized for snapshots:
rg -n "Serialize|Deserialize|kvm_regs|kvm_sregs|VcpuState|save_state|restore_state" src/vmm/src/vstate/vcpu/
Note: This is why a snapshot is only restorable on a compatible host. The serialized blob is literally the kernel's
kvm_*structs; restoring on a CPU with different features needs CPUID/MSR normalization — the reason CPU templates exist. Theserdefeature is the mechanism; CPU templates are the compatibility policy.
Some kvm-bindings structs are FAM structs (flexible-array-member: a header
followed by a runtime-length array, like kvm_cpuid2 with nent entries, or
kvm_msrs). These are handled via vmm-sys-util's FamStructWrapper
(vmm-sys-util.md) — another reason the crates interlock.
Reading exercise
# 1. Confirm both crates and their pinned versions.
rg -n "kvm-ioctls|kvm-bindings" Cargo.toml Cargo.lock
# 2. Read the safe API for the pinned versions.
cargo doc -p kvm-ioctls -p kvm-bindings --no-deps --open
# 3. The VcpuExit variants the run loop matches on.
find ~/.cargo/registry/src -maxdepth 2 -type d -name 'kvm-ioctls-*' \
-exec rg -n "enum VcpuExit" -A40 {} +
# 4. Firecracker's vstate wrappers over VmFd/VcpuFd.
rg -n "struct Vm\b|struct KvmVcpu|VcpuFd|VmFd|\.run\(\)|VcpuExit::" src/vmm/src/vstate/
# 5. Where kvm-bindings structs get filled (memory region, regs).
rg -n "kvm_userspace_memory_region|set_user_memory_region|kvm_regs|kvm_sregs" src/vmm/src/
# 6. The serde feature path for snapshots.
rg -n "kvm-bindings.*serde|VcpuState|save_state|restore_state" Cargo.toml src/vmm/
Answer:
- Why are
kvm-bindingsandkvm-ioctlstwo crates? What does each track, and when would you edit one versus the other? - Name the three RAII fd types, how each is created, and the ioctl behind each creation step.
- Walk the worked example: which two operations are
unsafe, and what unsafe contract does each one carry? - Take three
VcpuExitvariants and say how Firecracker's run loop dispatches each. Where is that match invstate/? - What does the
serdefeature onkvm-bindingsenable, and why does snapshotting need it? How does that connect to CPU templates? - What is a FAM struct, give a KVM example, and which crate provides the wrapper for it?
Common bugs and symptoms
| Symptom | Root cause | Where to look |
|---|---|---|
KVM_RUN returns EFAULT | a guest-memory region registered with a stale/invalid host pointer | set_user_memory_region; vm-memory.md lifetime |
A VcpuExit variant panics "unhandled" | guest used a device/port the bus doesn't model | the exit match in vstate/vcpu/; the device bus |
| Snapshot restore fails on a different host | serialized CPUID/MSRs incompatible with the new CPU | CPU templates; VcpuState (de)serialization |
create_vcpu fails with EINVAL (aarch64) | vCPU not initialized (KVM_ARM_VCPU_INIT) before use | the aarch64 vCPU setup in vstate/ |
| New kernel exit reason not handled | kvm-ioctls pinned version predates the new VcpuExit | bump kvm-ioctls; possibly an upstream fix |
Next: vm-memory — the guest-memory model that owns the host
mappings you just registered with set_user_memory_region, and lets devices read
and write guest RAM safely.