Reading the Firecracker Codebase
Firecracker is small for a hypervisor — far smaller than QEMU or the Linux kernel — but it is still a multi-crate Rust workspace where one process spins up KVM, three classes of thread, a virtio device model, a snapshot engine, and an HTTP control plane, all woven together with channels, eventfds, and an epoll loop. You will not read it front to back, and you should not try. The skill is not memory; it is navigation — finding the right path in minutes and ignoring the other 95% of the tree until you need it.
This chapter gives you the strategies the maintainers actually use. Everything assumes a clone at
~/fc-src (git clone https://github.com/firecracker-microvm/firecracker.git ~/fc-src). Because
code moves between branches, every struct or function named here comes with the rg/find that
locates it on your checkout. Never trust a line number; run the command.
Note: Firecracker recently went through a large refactor that merged several historically separate crates into
vmm. Older blog posts and Stack Overflow answers will point at crate paths that no longer exist (devices,rate_limiter,mmdsas top-level crates). Trustrgover your memory and over the internet. Verify the workspace shape on your branch first.
The Crate Map First
Before reading any code, learn the workspace shape. The authoritative list is the workspace manifest, not a guess at directories:
cd ~/fc-src
rg -n '^\s*"' Cargo.toml # workspace members
ls src/ # the crate directories
The crates that matter, and when you read each:
Crate (src/<name>) | Role | When you read it |
|---|---|---|
vmm | The core VMM library. Machine model, vCPU/KVM state, all device emulation, MMDS, rate limiting, snapshots, the action dispatch. The big merged crate — 90% of your time. | Almost always |
firecracker | The firecracker binary + the HTTP API server (src/firecracker/src/api_server/). Drives vmm. | API/control-plane work |
jailer | The jailer binary: chroot/cgroups/namespaces/privilege drop, then execs firecracker. | Isolation/security work |
seccompiler | Compiles JSON seccomp filters → BPF. | Seccomp filter changes |
cpu-template-helper | Create/inspect/verify CPU templates. | CPUID/MSR template work |
snapshot-editor, rebase-snap | Inspect/edit snapshots; rebase diff memory. | Snapshot tooling |
acpi-tables, utils | Build guest ACPI tables; shared internal utilities. | ACPI / shared helpers |
Warning: The API server lives in the
firecrackerbinary, not invmm. This trips up everyone. If yourg "ApiServer"and expect it undersrc/vmm/, you will be confused. It is undersrc/firecracker/src/api_server/.
Key subsystems now live inside src/vmm/src/ (they used to be their own crates). Confirm them:
ls src/vmm/src/ # builder.rs, resources.rs, rpc_interface.rs, persist.rs...
ls src/vmm/src/devices/ src/vmm/src/devices/virtio/ # block, net, vsock, balloon, rng...
ls src/vmm/src/vstate/ # vcpu/, vm.rs, memory.rs (KVM state)
ls src/vmm/src/arch/ # x86_64/, aarch64/
Pin a one-line-per-crate note file. You will re-read it constantly:
mkdir -p ~/fc-notes && $EDITOR ~/fc-notes/crate-map.md
Strategy 1: Start From the Entrypoint, Trace Inward
There is exactly one binary entrypoint for the VMM, and it leads, in a few hops, to the running microVM object. This is the spine of the whole program; learn it first.
# The binary's main(). It does almost no work itself — it parses args and dispatches.
rg -n "fn main" src/firecracker/src/main.rs
main parses arguments, sets up logging, and then chooses between two run modes depending on whether
an API socket was given. The wiring of threads and channels is not in main.rs — it is in the
adapter:
# The two run modes: with the HTTP API, or with --no-api (config file only).
rg -n "fn run_with_api|fn run_without_api" src/firecracker/src/
run_with_api is the canonical path. It spawns the API thread, creates the action channel, and
builds the microVM. From there you reach the builder, which constructs and boots the Vmm:
# The construction/boot entrypoints — the heart of the machine model.
rg -n "fn build_microvm_for_boot|fn build_and_boot_microvm|fn build_microvm_from_snapshot" src/vmm/src/builder.rs
# The running microVM object itself.
rg -n "pub struct Vmm" src/vmm/src/lib.rs
The canonical reading order — burn this into memory, then prove each hop with the command above:
src/firecracker/src/main.rs (parse args, choose run mode)
↓
run_with_api / run_without_api (spawn API thread, create action channel + eventfd, call builder)
↓
builder.rs: build_and_boot_microvm (build VmResources → build the Vmm → start vCPUs)
↓
Vmm (src/vmm/src/lib.rs) (the running microVM; owned by the VMM thread)
↓
EventManager epoll loop (device emulation, MMDS, rate limiting — the steady state)
↓
Vcpu run loop (KVM_RUN) (one per vCPU thread; handles VM exits)
Five hops from main to a running guest. Everything else in the codebase hangs off this spine. The
threading-model deep dive and the
api-server-and-action-channel deep dive take these
hops apart in detail; here you only need to know that the spine is where you start reading.
Strategy 2: Follow Types, Not Files
The single most important habit in a Rust codebase: navigate by type, not by file. Rust modules
are organized around types and traits, and a type's definition plus its impl blocks tell you
everything — its fields, its methods, what traits it satisfies. A file is just a container; a type is
a contract.
When you want to understand a subsystem, find its central type's definition and its impls:
# Definition of a type:
rg -n "struct VmResources|enum VmmAction|struct DeviceManager" src/vmm/src/
# Everything implemented on a type (its methods and trait impls):
rg -n "impl .*Vmm|impl Vmm" src/vmm/src/lib.rs
The control-plane command type is the perfect example of "follow the type." Every API request becomes
a VmmAction; understanding that enum is understanding the entire control surface:
# The enum of everything the API thread can ask the VMM to do.
rg -n "pub enum VmmAction" src/vmm/src/rpc_interface.rs
# Where each variant is handled (the match that dispatches them):
rg -n "VmmAction::" src/vmm/src/rpc_interface.rs | head -40
Read the variants of VmmAction and you have read the contract between the API thread and the VMM
thread without reading a single line of HTTP code. The variants are the API, distilled.
The two tools that make type-following fast:
- rust-analyzer in your editor: "Go to Definition" (jumps to a type/fn), "Find All References"
(every caller), "Go to Implementations" (every
implof a trait). This replaces most speculative grepping. If you read Firecracker without rust-analyzer, you are working with one hand. cargo docrenders the type graph as browsable HTML — the fastest way to see a crate's public surface and how types relate:cargo doc --no-deps -p vmm --open # builds & opens vmm's API docs
Strategy 3: rg Is Your Index
You do not have the codebase memorized; rg (ripgrep) is your index into it. A few patterns recur
constantly. Learn these as muscle memory:
| Question | Command |
|---|---|
Where is type/fn X defined? | rg -n "struct X|enum X|fn X" src/ |
Who constructs/uses X? | rg -n "X::new|X {" src/ |
| Which file handles this API endpoint? | rg -n "boot-source|/drives|/machine-config" src/firecracker/ |
| Where is this KVM ioctl used? | rg -n "KVM_RUN|VcpuExit|KVM_SET_USER_MEMORY_REGION" src/vmm/src/vstate/ |
| Where does a device register on the bus? | rg -n "register_mmio|MMIODeviceManager" src/vmm/src/device_manager/ |
| What does this error mean? | rg -n "VmmActionError|thiserror|#\[error" src/vmm/src/ |
A worked instinct: when you read a struct field whose meaning isn't obvious, rg the field name to
see where it's written and read. The write sites are the documentation.
Tip: Pair
rgwithgit log -Sandgit log -G.-S "track_dirty_pages"finds the commits that changed how often a string appears (when a symbol was introduced or removed);-G "regex"finds commits whose diff touches matching lines. Both surface the PR number(#NNNN)that is your doorway into Design via GitHub.
Strategy 4: Read the Tests to Understand Intent
Source tells you what the code does; tests tell you what it is supposed to do, including the edge cases the author was worried about. Firecracker has two tiers, and you read them in this order:
| Tier | Where | What it tells you |
|---|---|---|
| Unit tests | #[cfg(test)] mod tests inline in the Rust source, run with cargo test | The contract of a single function/type, including edge cases and error paths |
| Integration tests | pytest under tests/, run with tools/devtool test | End-to-end microVM behavior: boot, API, devices, snapshots, security — across the whole binary |
Crucially, the primary integration harness is pytest in tests/, not cargo test. A new
behavior is proven by a Python integration test that boots a real microVM and asserts on it. Find the
tests for the area you're reading:
# Unit tests inline with the code you're reading:
rg -n "#\[cfg\(test\)\]|fn test_" src/vmm/src/rate_limiter/
# The integration tests, by topic:
ls tests/integration_tests/
rg -n "def test_" tests/integration_tests/functional/test_api.py | head
For any subsystem, the test that exercises it is the cheapest spec you will find. Before you guess
how VmResources validates a machine config, read the test that feeds it bad input and asserts the
error. The author already encoded the behavior you are about to reverse-engineer.
Strategy 5: Keep a Reading Log, and Know When to Stop
Maintainers carry the codebase in their heads because they wrote much of it. You do not. Compensate with notes. Keep one file and append a dated entry every time you trace a path:
cat >> ~/fc-notes/reading-log.md <<'EOF'
## 2026-06-18 — API request → VMM action path
- main.rs: parse args → run_with_api (src/firecracker/src/api_server_adapter.rs)
- API thread: HTTP on UDS → ParsedRequest → VmmAction (boxed) sent over std::sync::mpsc
- eventfd wakes the VMM EventManager epoll loop
- PrebootApiController / RuntimeApiController dispatch the VmmAction (rpc_interface.rs)
- reply: Box<Result<VmmData, VmmActionError>> back over the response channel
EOF
Knowing when to stop is as important as knowing where to look. It is easy to follow a type three
layers into kvm-ioctls and lose the afternoon. Set a question before you start ("how does a PATCH
to a drive's rate limiter reach the device?") and stop the moment you can answer that question. The
depth below — how the token bucket actually refills, how the ioctl is marshalled — belongs to the
relevant deep dive, not to your current trace. Drowning happens when you
read without a question.
Worked Example: "How does PATCH /drives/{id} change a running device?" (60 minutes)
Goal: answer a real "how does X work" by navigation alone, in an hour, recording the path. Do not read the hop names back from a deep dive — discover them with the commands.
Step 1 (10 min) — Find the API surface for the endpoint
cd ~/fc-src
# Where is the drives endpoint parsed?
rg -n "drives|DriveConfig|PATCH" src/firecracker/src/api_server/ | head
Find the request parser that turns PATCH /drives/{id} into a ParsedRequest. Note that it produces
a VmmAction variant (something like UpdateBlockDevice). Write the file + the variant name in your
log.
Step 2 (10 min) — Follow the action into the VMM
# Where is that VmmAction variant defined and dispatched?
rg -n "UpdateBlockDevice|VmmAction::" src/vmm/src/rpc_interface.rs | head
This proves the runtime controller (RuntimeApiController, because a drive PATCH happens after
boot) handles the variant. Read that match arm. Note what it calls on the Vmm.
Step 3 (20 min) — Drop into the device
# The block device and its update path:
rg -n "fn update|RateLimiter|patch" src/vmm/src/devices/virtio/block/
Trace how the controller reaches the live Block device and updates its path or rate limiter while
the microVM runs. This is the boundary where the control plane (VMM thread) touches a device the
guest is actively driving — note the synchronization. (Depth on the token bucket itself belongs to
the rate-limiting deep dive — do not go there now;
just confirm the hop.)
Step 4 (10 min) — Verify with a test, then run it for real
# The integration test that exercises a live drive PATCH:
rg -n "patch.*drive|update.*drive|rate_limiter" tests/integration_tests/functional/ | head
# Optional: boot a microVM (see Lab 1.3) and PATCH a drive over the socket:
# curl -X PATCH --unix-socket /tmp/fc.sock --data '{"drive_id":"rootfs","rate_limiter":{...}}' \
# http://localhost/drives/rootfs
Record the four-hop trace in ~/fc-notes/reading-log.md. If you can reproduce it tomorrow without
this page, you have the navigation skill.
Validation: Prove You Understand This
- From a cold start, name the five hops from
src/firecracker/src/main.rsto a runningVmm, with thergthat locates each. - Read the variants of
VmmAction(rg -n "pub enum VmmAction" src/vmm/src/rpc_interface.rs) and explain why that enum is the control-plane contract. - Explain why the API server is in the
firecrackercrate, notvmm, and locate it. - For one subsystem (block, net, rate limiter, or MMDS), find its central type, its inline unit tests, and the pytest integration test that exercises it.
- Run one
git log -Sorgit log -Gagainstsrc/vmm/src/and surface a PR number(#NNNN)— your bridge into the next chapter. - State the question you set before your last code-reading session, and where you stopped. If you can't, you were drowning.
You have absorbed this chapter when you can find any feature in Firecracker in under fifteen minutes, starting from the entrypoint and following types, without re-reading these steps. The next chapter — Understanding Design via GitHub — tells you where the decisions behind that code actually lived.