← notes on building k7d

Notes on building k7d · Part 6

Proving the fork: Kani, Aeneas, and budgets as CI

Formal methods where they pay off — and a precise account of where I deliberately stopped.

August 2026 · ~11 min read

Let me set the scope honestly before the first proof appears, because "formally verified VMM" is a claim I am specifically not making. k7d machine-checks selected critical pieces — the unsafe memory arithmetic that a fork depends on, and the snapshot tree's budget/eviction model — not the whole runtime. The rest of the correctness story is ordinary engineering: integration tests, fuzzing, and latency budgets enforced in CI. This post is about how those layers fit together, and why the two places that got real proofs are the two places where testing structurally can't reach.

Why these two places

The common thread of this series' bugs (the repo's CHALLENGES.md has the full catalog) is that fork bugs fail silently. A wrong dirty-page union doesn't crash — it resurrects stale bytes in some future fork. An eviction off-by-one doesn't crash — it deletes the protected winner of a six-hour search, once, under memory pressure. Tests catch what you thought to assert on inputs you thought to try. For code whose failure mode is "quietly wrong, rarely, later", I wanted a stronger word than "tested".

Two candidates cleared the cost-benefit bar:

Both share the property that makes verification tractable: they're deterministic logic with no I/O, extracted into functions a tool can reason about exhaustively. That's not luck — the code was shaped to make it true, and reshaping code to be provable turns out to improve it anyway.

Layer one: Kani on the unsafe paths

Kani does bounded model checking for Rust: a proof harness declares symbolic inputs ("any u64", "any size in 1..=8"), and Kani + CBMC exhaustively verify that no input within bounds can cause a panic, an overflow, or a memory-safety violation. Not "10,000 random inputs" — all inputs, within the harness's bounds. The harnesses live next to the code they check and run via make kani; they cover selected unsafe and address-arithmetic paths in the memory/fork machinery.

Kani also taught me its own lesson about code shape, the hard way. A harness for the parallel-copy chunking originally verified a helper that returned a Vec of chunk ranges with a symbolic capacity — and CBMC promptly ran out of memory, because a heap allocation whose size is symbolic forces the model checker to consider the allocator's behavior for every candidate capacity. The fix was to refactor the helper into a pure copy_chunk_size(mem_size, max_chunks) → u64 and drive the copy loop off plain arithmetic — no Vec at runtime either. The proof went from OOM to verifying in seconds, and the production code got simpler. This kept happening: the version of the code that can be verified is usually the version you should have written anyway.

Layer two: Aeneas → Lean on the tree model

Kani answers "does this arithmetic ever panic". For the eviction model I wanted a functional-correctness statement: the algorithm never selects a protected node as a victim, and the budget math is right. That needs a proof assistant.

The pipeline: the tree bookkeeping lives in one Rust file (tree_model.rs), deliberately free of I/O and clever borrows. Charon extracts it to an intermediate representation; Aeneas translates that into pure Lean functions; and the theorems in verif/ are proved against hand-written Lean mirrors of the same logic, with equivalence lemmas tying the generated code to the mirrors. make verif-gen verif-build regenerates and re-proves the lot; CI runs it whenever tree_model.rs changes, so the proof can't drift from the code it's about.

THREE LAYERS, ONE COMMIT GATE Strong words only where a machine checked them tree_model.rs pure, no I/O Charon extract to IR Aeneas IR → pure Lean lake build — theorems vs mirrors eviction never selects a protected node axiom guard — diff vs allowlist a new axiom fails the build unsafe memory address arithmetic Kani / CBMC — bounded model checking all inputs within bounds, not 10,000 random ones budget constants one source file integration tests · fuzzing · Miri real KVM, real k3s, adversarial bytes CI — any drift, any hole → red proofs regenerate from the code they’re about Both proven components are small, pure, and fail silently when wrong. That's the selection rule.
fig 1: the verification stack. proofs for the two silent-failure hotspots, an axiom guard so the proofs can't weaken unnoticed, and budget assertions so the performance claims can't rot.

Getting Rust through Aeneas imposed a discipline that's interesting in its own right. Aeneas's borrow-tracking symbolic execution must join the environments of every if/match arm, and a shared borrow held across arms — or a &&-chain where each conjunct re-borrows — produces joins it can't reconcile, even when rustc is perfectly happy. So tree_model.rs follows house rules: copy scalar fields to locals before branching, evaluate eligibility conjuncts into named bools, no reference lives across an arm boundary. Purely mechanical, no semantic change — and, once again, the constrained version is more readable than the original. The proofs themselves cost real effort in tactic work (the repo's CHALLENGES.md records the hours), but the model is small enough that the total stayed sane.

The axiom allowlist

Here's the failure mode nobody warns you about, and the reason I'd call this section the most transferable part of the post. When Aeneas meets a Rust primitive its Lean standard library doesn't model, it doesn't fail — it emits an axiom. An axiom is an assumption: "trust me, this exists". Anything your generated code touches through an axiom is a hole in the proof, and the build stays green.

I hit this concretely: u64::saturating_mul had no Lean model, and the derived Clone for a struct containing Option<u64> went through an Option::clone the extraction left abstract. Both arrived as axioms; both made "this function never panics" formally unprovable in a way no error message announces. The fixes were semantics-preserving Rust rewrites — an explicit overflow guard instead of saturating_mul, a manual field-wise Clone — verified by the equivalence theorems themselves.

THE FAILURE MODE NOBODY WARNS YOU ABOUT A green build with a hole in it RUST PRIMITIVE u64::saturating_mul derived Clone on Option<u64> NO LEAN MODEL → AXIOM “trust me, this exists” build stays green · the theorem is now hollow FIX, NOT ALLOWLIST explicit overflow guard manual field-wise Clone semantics-preserving rewrites CI: grep the generated Lean for `axiom`, diff against a committed allowlist, fail on anything new adding an entry requires a human to write down, in a reviewed file, “we are choosing to assume this” Without a guard like this, a proof can weaken over time without anyone noticing.
fig 2: the axiom guard. an unmodelled primitive doesn't fail the extraction — it quietly becomes an assumption, which is why the allowlist diff is a build gate rather than a code review habit.

But the standing rule matters more than the instances: grep the generated Lean for axiom, and treat every hit as a bug until proven otherwise. That rule is now automated — a CI script diffs the axioms in the generated file against a committed allowlist and fails the build on any new one. Adding to the allowlist requires a human to write down, in a reviewed file, "we are choosing to assume this". Without a guard like this, a proof can weaken over time without anyone noticing.

Layer three: the unglamorous rest

Below the proofs, the ordinary machinery, each piece scoped to what it's actually good at:

Latency budgets are CI, too

One more thing gets the assertion treatment, and I consider it part of verification in the same spirit: the performance claims. Every number this series has quoted lives in one Rust file of budget constants; every budget is enforced by an integration test (fork < 50 ms, cluster fork < 1 s, 50-fork batch < 20 s, and so on down to agent ping RTT). If a refactor regresses the fork path past its budget, CI goes red before the README goes wrong. Benchmark tables rot; assertions don't. Budgets sit at roughly 2× typical, and where one is deliberately generous — the full suite runs serially with shared KVM state, which inflates tails — LATENCY_BUDGETS.md says so in as many words rather than quietly widening the gap.

What this does and doesn't buy

SCOPE, STATED PLAINLY · BOTH DIRECTIONS What the proofs do and don’t buy ≤25k LINES — TESTS, FUZZING, MIRI KVM ioctls virtio devices network plumbing containerd shim daemon API no security audit yet · a proof of the eviction model is not a proof the daemon has no bugs PROVEN — ALL INPUTS IN BOUNDS address arithmetic cannot overflow or panic Kani · under every fork PROVEN — FUNCTIONAL CORRECTNESS eviction cannot select a protected node Aeneas → Lean · the autonomous killer
fig 3: the honest scope. two small components are machine-checked because their failures are silent, rare and catastrophic; the rest of the codebase gets ordinary engineering.

Scope, stated plainly, both directions. The proofs do not cover: the KVM ioctl sequences, the virtio device implementations, the network plumbing, the containerd shim — the majority of the ≤25k lines, guarded by tests and fuzzing, not theorems. A security audit hasn't happened yet; a proof of the eviction model is not a proof the daemon has no bugs.

What they do buy: the two components whose failures are silent, rare, and catastrophic are exhaustively checked — for all inputs within bounds, not all inputs I thought of. The eviction logic that kills VMs autonomously under budget pressure provably cannot select a protected node. The address arithmetic under every fork provably cannot overflow or panic. And the proof infrastructure is wired so that the proofs can't silently detach from the code (regeneration in CI) or hollow out from within (the axiom guard).

For a young project making strong claims, that's the deal I'd want as a reader: strong words only where the machine checked them, plain words everywhere else.

End of series

That's the whole arc: why RL needs identical environments (Part 1), why the existing tools don't fork a running cluster (Part 2), the 45-second disk-only attempt that taught me the real problem (Part 3), the VMM (Part 4), the budgeted tree (Part 5), and the verification (this one).

What's next, per the roadmap: a proper config file for the daemon, re-enabling the stock k3s add-ons (CoreDNS, ingress) in the benchmark fixture, and cross-node fork. The code is Apache-2.0 at github.com/katakate/k7d, small enough to actually read, with docs at docs.katakate.org. Would love feedback, and contributors — the roadmap is clear and open. If you train agents on infrastructure, try it and tell me what breaks.

Star k7d on GitHub