Stage 4 — API and Configuration Validation
What class of issue this is
Stage 4 is where you start changing when Firecracker says no. The REST API is the entire
user-facing surface of the VMM: an orchestrator PUTs JSON for boot-source, drives,
machine-config, network-interfaces, vsock, balloon, mmds-config, and so on, and
Firecracker turns each request into a VmmAction that configures the microVM. A validation bug is a
request field that should be rejected but is accepted — a zero or absurd mem_size_mib, a
vcpu_count that isn't a legal topology, a rate-limiter size of 0, two drives with the same
drive_id, a config change attempted after boot when it is pre-boot-only. The damage shows up later:
a confusing boot failure, a device that misbehaves, or an invariant a deeper subsystem assumed.
A Stage 4 PR adds or tightens a check on an API/config field and returns a good rejection error
(the Stage 3 skill, now applied to a new Err).
Why it's at this difficulty
Adding validation is genuinely behaviour-changing: a request that used to be accepted now fails. That
is a (small) API compatibility decision — you must be sure the rejected input was always invalid,
not merely unusual, or you break a working caller. You also must place the check at the right layer:
in the request parser, in the vmm_config type's validation, or in the builder, with the right
pre-boot-vs-runtime gating. Maps to Level 3 (the API → VMM action path) and
pairs with the API server & action channel deep dive.
What you must already understand
- The request path. API thread parses JSON → builds a
ParsedRequest→ aVmmActionover an mpsc channel → an eventfd wakes the VMM epoll loop →PrebootApiController(beforeInstanceStart) orRuntimeApiController(after) dispatches it. Trace it on your branch:
rg -n "ParsedRequest|enum VmmAction\b|PrebootApiController|RuntimeApiController" \
src/firecracker/src/api_server/ src/vmm/src/rpc_interface.rs | head
- Where config types live and validate. Each endpoint has a config type under
src/vmm/src/vmm_config/(e.g.machine_config.rs,drive.rs,net.rs,boot_source.rs) and aVmResourcesaggregator (src/vmm/src/resources.rs) that holds them pre-boot:
ls src/vmm/src/vmm_config/
rg -n "struct VmResources|fn set_|fn build_|impl VmResources" src/vmm/src/resources.rs | head
- The swagger contract. The OpenAPI spec is the documented surface; a validation change often needs the spec updated too:
rg -n "machine-config|MachineConfig|minimum|maximum" src/firecracker/swagger/firecracker.yaml | head
- Pre-boot vs runtime. Some fields are settable only before
InstanceStart; some arePATCH-able at runtime (drive path, rate limiters). Putting a check on the wrong controller breaks the wrong phase. (threading model.)
Representative tasks
| Task | Field / endpoint | Where the check goes | Find it with |
|---|---|---|---|
Reject mem_size_mib == 0 or absurd | /machine-config | vmm_config/machine_config.rs validate | rg -n "mem_size_mib" src/vmm/src/vmm_config/machine_config.rs |
Enforce legal vcpu_count topology | /machine-config | same | `rg -n "vcpu_count |
Reject duplicate drive_id | /drives/{id} | vmm_config/drive.rs / VmResources | `rg -n "drive_id |
Reject rate-limiter size == 0 | /drives, /network-interfaces | rate_limiter config / drive/net config | `rg -n "size |
Validate guest_mac / iface id | /network-interfaces/{id} | vmm_config/net.rs | `rg -n "guest_mac |
| Reject a pre-boot-only field after boot | various | RuntimeApiController dispatch | `rg -n "RuntimeApiController |
Device-specific config (block/net/vsock/balloon internals, rate-limiter semantics, MMDS) gets its own treatment in Stage 5. Stage 4 is the general validation skill; Stage 5 is the device-by-device application of it.
How to approach one — worked example: tightening a machine-config bound
Illustrative of the pattern. The
rgfinds the real validation function; the refactor moved these, so run it rather than trusting a path.
Symptom: an issue reports that PUT /machine-config accepts mem_size_mib: 0, and the microVM
then fails to boot with an opaque KVM error far downstream instead of a clear rejection at config
time.
Step 1 — reproduce against a running binary
Validation bugs are cheap to reproduce — drive the API directly:
sudo ./firecracker --api-sock /tmp/fc.sock &
curl -s -X PUT --unix-socket /tmp/fc.sock \
--data '{"vcpu_count":2,"mem_size_mib":0}' \
http://localhost/machine-config
# BUG: returns 204/No Content (accepted) instead of a 400 with a clear message.
Step 2 — find the validation seam and confirm it's missing
rg -n "fn check_|fn validate|mem_size_mib|MAX_SUPPORTED_VCPUS" \
src/vmm/src/vmm_config/machine_config.rs
You will find the per-field checks; the mem_size_mib > 0 check is absent.
Step 3 — comment your plan on the issue, then diff
Stage 4 is the first stage where you open a discussion before coding. Post three sentences (see
the roadmap index): the symptom, your read (no lower bound on mem_size_mib), your plan
(add the check in MachineConfig validation, reject with a clear error, add a unit + integration
test). A maintainer will confirm the bound and whether the spec needs a minimum.
--- a/src/vmm/src/vmm_config/machine_config.rs
+++ b/src/vmm/src/vmm_config/machine_config.rs
@@ impl MachineConfig { // or the update/validate fn the rg found
+ if self.mem_size_mib == 0 {
+ return Err(MachineConfigError::InvalidMemorySize);
+ }
@@ pub enum MachineConfigError {
+ #[error("The memory size (MiB) is invalid! It must be strictly greater than 0.")]
+ InvalidMemorySize,
Update the OpenAPI spec so the documented contract matches:
--- a/src/firecracker/swagger/firecracker.yaml
+++ b/src/firecracker/swagger/firecracker.yaml
@@ mem_size_mib:
type: integer
+ minimum: 1
description: Memory size of VM
Step 4 — test both layers
A unit test on the config type, and an integration test asserting the API now returns a 400 with the message (new functionality requires an integration test in Firecracker):
#![allow(unused)] fn main() { #[test] fn test_rejects_zero_memory() { let mut cfg = MachineConfig::default(); cfg.mem_size_mib = 0; assert!(matches!(cfg.validate(), Err(MachineConfigError::InvalidMemorySize))); } }
cargo test -p vmm machine_config
rg -n "def test_.*machine_config|mem_size_mib" tests/integration_tests/ | head
tools/devtool test -- -k machine_config
The pytest test should PUT mem_size_mib: 0 and assert the response is a 400 with the expected
fault_message.
What a good PR looks like
- A clear argument that the rejected input was always invalid. Either it could never boot, or it violated an invariant a downstream subsystem assumed. If a real caller might depend on the old behaviour, that is a maintainer call — raise it on the issue first.
- The check at the right layer. Field-shape and bounds in the
vmm_configtype's validation; cross-field/cross-resource checks (duplicate ids) inVmResources; phase rules (pre-boot-only) on the right controller. Not buried in the builder where it fires too late. - A good rejection error (Stage 3 skill): names the field, the bound, and the value, and is pinned by a test.
- The swagger spec updated when the constraint is part of the documented contract.
- Both a unit test and an integration test — the integration test PUTs the bad request and asserts the rejection. This is non-negotiable for new behaviour.
- CHANGELOG entry: a tightened validation is user-visible (
### Changed/### Fixed).
Graduation criteria — ready for Stage 5 when
- You have one merged validation PR that adds/tightens a check, returns a clear error, and is covered by a unit test and an integration test that PUTs the rejected request.
- You can trace a single API field from the JSON body, through parsing, to its
vmm_configtype and itsVmmAction, and say where validation belongs and why. - You can articulate when a validation change is an API-compatibility decision and when it is obviously safe — and you reach for the issue discussion before the diff.
- You understand pre-boot vs runtime gating well enough to reject a pre-boot-only field after
InstanceStartwithOperationNotSupported.
You now validate generic config. Stage 5 applies the skill device by device — block, net, vsock, balloon, the rate limiter, and MMDS — where the config feeds real device behaviour.