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 → a VmmAction over an mpsc channel → an eventfd wakes the VMM epoll loop → PrebootApiController (before InstanceStart) or RuntimeApiController (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 a VmResources aggregator (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 are PATCH-able at runtime (drive path, rate limiters). Putting a check on the wrong controller breaks the wrong phase. (threading model.)

Representative tasks

TaskField / endpointWhere the check goesFind it with
Reject mem_size_mib == 0 or absurd/machine-configvmm_config/machine_config.rs validaterg -n "mem_size_mib" src/vmm/src/vmm_config/machine_config.rs
Enforce legal vcpu_count topology/machine-configsame`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-interfacesrate_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 bootvariousRuntimeApiController 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 rg finds 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_config type's validation; cross-field/cross-resource checks (duplicate ids) in VmResources; 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_config type and its VmmAction, 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 InstanceStart with OperationNotSupported.

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.

Next: Stage 5 — Device Configuration Issues.