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, mmds as top-level crates). Trust rg over 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>)RoleWhen you read it
vmmThe 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
firecrackerThe firecracker binary + the HTTP API server (src/firecracker/src/api_server/). Drives vmm.API/control-plane work
jailerThe jailer binary: chroot/cgroups/namespaces/privilege drop, then execs firecracker.Isolation/security work
seccompilerCompiles JSON seccomp filters → BPF.Seccomp filter changes
cpu-template-helperCreate/inspect/verify CPU templates.CPUID/MSR template work
snapshot-editor, rebase-snapInspect/edit snapshots; rebase diff memory.Snapshot tooling
acpi-tables, utilsBuild guest ACPI tables; shared internal utilities.ACPI / shared helpers

Warning: The API server lives in the firecracker binary, not in vmm. This trips up everyone. If you rg "ApiServer" and expect it under src/vmm/, you will be confused. It is under src/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 impl of a trait). This replaces most speculative grepping. If you read Firecracker without rust-analyzer, you are working with one hand.
  • cargo doc renders 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:

QuestionCommand
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 rg with git log -S and git 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:

TierWhereWhat it tells you
Unit tests#[cfg(test)] mod tests inline in the Rust source, run with cargo testThe contract of a single function/type, including edge cases and error paths
Integration testspytest under tests/, run with tools/devtool testEnd-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

  1. From a cold start, name the five hops from src/firecracker/src/main.rs to a running Vmm, with the rg that locates each.
  2. 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.
  3. Explain why the API server is in the firecracker crate, not vmm, and locate it.
  4. 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.
  5. Run one git log -S or git log -G against src/vmm/src/ and surface a PR number (#NNNN) — your bridge into the next chapter.
  6. 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.