Verification
July 19, 2026 ยท View on GitHub
This page states exactly what NONOS proves, what it does not, and how to reproduce every claim from a
clean checkout. It is written to be audited, not believed. The machinery it summarizes lives under
verification/ and in the proof crates under userland/*_proofs/, and the deeper narrative is in
verification/ARCHITECTURE.md; this page is the wiki-level map of it.
The thesis: proofs over the code that runs
A proof is only as strong as the distance between the thing proved and the thing that boots. A kernel that carries a formal model and proves theorems about that model has established something real, but whether the code that actually runs refines the model is a separate question. For a total-correctness effort like seL4 that question is itself answered by a machine-checked refinement proof from the model down to the C. For most projects that claim "formal verification" it is not answered at all, and that unproven gap is where defects live.
NONOS closes the gap a different way for the properties it proves: the runnable proofs include the
real src/ and capsule source, unmodified, through Rust's #[path] mechanism (only the syscall
clock is shimmed), and execute it. Where a property is naturally an abstract theorem it is stated in
Lean, and a second proof, in Verus over the real bit-operations or in a runnable proof over the real
code, shows the implementation satisfies it. The model is never left standing on its own. NONOS does
not claim total functional correctness of the whole kernel; it claims the security-critical properties,
proven over the running code, with nothing left as an unproven placeholder.
The layers, strongest at the bottom
Layer 0, source hygiene. nonos-verify hygiene scans all production Rust under src/ and
userland/ and fails the build on panic paths (unwrap, expect, panic!), stub macros (todo!,
unimplemented!, unreachable!), dead-code allow markers, and temporary comment markers. Proof crates
are excluded because their assertions are allowed to panic. This is not a proof of correctness; it is a
machine-enforced floor that the production kernel contains no panic path and no stubbed logic, re-checked
on every push.
Layer 1, runnable proofs over the real source. Host crates that #[path]-include the actual kernel
and capsule code and run it with cargo test:
userland/fs_proofs(58 passing): VFS store operations, path-security canonicalization and the/capsulesread-only guard including slash-smuggling, the protocol codec against hostile input, caller attestation rejecting userspace impersonation, and fuzz proofs asserting the parsers never panic and never violate their invariants over millions of structured and random inputs. Writing these found and fixed real bugs.userland/crypto_proofs: the real kernel crypto checked against standard vectors, SHA-256/512 (FIPS 180-4), SHA-3 (FIPS 202), BLAKE3, HMAC-SHA-256 (RFC 4231), HKDF (RFC 5869), ChaCha20-Poly1305 (RFC 8439), AES-GCM (NIST), Ed25519 (RFC 8032), P-256/P-384 ECDSA, secp256k1, and RSA, each including tamper rejection.userland/net_proofs,driver_proofs,stark_proofs,kernel_proofs,usb_proofs, and the in-image proof capsules (capsule_gui_proof,capsule_std_proof,capsule_input_proof,capsule_proof_io), each asserting a specific guarantee over real code.
Layer 1b, bounded model-checking (Kani). Several proof crates carry Kani harnesses
(nonos-bootloader/boot_proofs/src/kani_proofs.rs, userland/fs_proofs/src/kani_proofs.rs,
userland/driver_proofs, and userland/stark_proofs/src/kani_proofs.rs, which proves the untrusted
attestation-trailer deserializer is total on any input). Kani exhaustively checks a function over all
inputs within a bound, which is
stronger than testing for the bounded region and catches the arithmetic and boundary cases fuzzing can
miss. It is bounded, not unbounded: it proves the property for inputs up to the bound, not for all
inputs of unbounded size.
Layer 2, Lean theorems. The Lean files under verification/lean/Nonos/ carry 887 theorems across
120 modules with zero sorry (Lean's placeholder for an unproven step), so every stated theorem is
fully proven. The tree is core-only: no Mathlib, so any recent Lean 4 toolchain checks it. The live
counts are in the evidence manifest, regenerated and checked on every push.
The modules map one to one onto the trusted path. A representative slice:
Capability / CapMask / CapToken grant/revoke/attenuate algebra and delegation-subset: a delegated
mask never carries a capability its parent lacks; a token is
admitted only if signed, unexpired and unrevoked
UserCopy the user/kernel boundary: an accepted copy range lies wholly in
user space, never the null page or the kernel half
Paging / Isolation / DemandPaging no writable-and-executable page; a served demand page is never
executable; no kernel-half address is demand-backed
AntiRollback / AntiRollbackState a monotone version floor that never falls; the concrete bootloader
check refines the abstract theorem
DmaMap / IrqBind / MsixExclusion the hardware broker: bounded DMA owned on a fresh epoch, a bounded
MSI-X bind, and no raw mapping ever reaching a protected register
ElfPhdr / ElfReloc / LoadProtect the loader: in-bounds program headers, a bounded relocation write,
RELRO sealed read-only before entry
FdAlloc / PidAlloc / SyscallRoute the allocators and the first-match syscall dispatch
Wpa2Handshake / CcmpReplay the Wi-Fi trusted path: keys install only on a valid message 3, a
replayed packet number is dead forever
The 168 flagship theorems are listed in verification/lean/AxiomProfile.lean, which prints each one's
exact axiom closure. The CI Lean job runs it as a gate: a closure may name only Lean's three standard
axioms (propext, Classical.choice, Quot.sound) and never sorryAx, so a sorry anywhere in a
proven theorem's dependency graph fails the build. This is machine-enforced, not a convention.
The transparent STARK attestation adds a second body of proof, 203 theorems across the
Nonos/Stark/ modules plus SigningKey and KeyLifecycle: Merkle membership soundness and
collision-freedom, the binding of a proof to its capsule identity, capabilities, policy epoch and
domain, length-prefixed measurement injectivity, the money-grade soundness budget, trailer parse
safety, and signing-key rollback, revocation and validity windows. These pin the model the STARK
spawn gate refines; see attestation and the
proof system.
Layer 2b, Verus refinement. verification/verus/ proves that the real Rust bit-operations match
the Lean model: the capability has/grant/revoke/attenuate functions (revoke_is_monotonic,
revoke_drops_the_right, and their companions), the page-permission spec, and the IPC-length spec are
proven in Verus directly over the Rust semantics. The attestation adds stark_attestation.rs, which
SMT-checks that a trailer length capped at the bytes remaining never over-reserves and that the gate
accepts only the conjunction of the root, context and enrollment checks. This is the bridge that ties
the abstract Lean theorem to the concrete bits & !bit the kernel executes.
Layer 2c, mechanical extraction (Charon and Aeneas). This is the strongest link between proof and
code, and it removes the transcription step entirely. Charon
lowers real kernel functions from the MIR that rustc compiles into LLBC, and
Aeneas translates that into a pure Lean definition. The Lean
theorems in verification/extraction/lean/ are then proven directly on that extracted definition, so
the thing proved is the real function, not a model a human retyped from it. The extraction crates under
verification/extraction/ include the real src/ files unmodified through #[path]; nothing is
copied. Extracted and proven so far:
capabilities::bits::{has,add,remove}_capability the capability word operations; extracted_remove_confines
proves the extracted revoke never grants a new capability
usercopy::policy::check_range the user/kernel range check; check_range_spec proves it
accepts exactly the non-null, in-bound, non-wrapping ranges,
tied to the abstract Isolation.Accepts
memory::paging is_wx_violation, to_pte_flags the W^X page encoder; extracted_no_wx_page proves the
extracted encoder never emits a writable-executable page
broker::irq::validate_msix_request the MSI-X bind validator; a request with an unknown flag
bit is refused before any device state is read
The extraction is regenerated in CI and diffed against the checked-in Lean, so it can never silently
drift from the source. Where Aeneas models a core library method as an opaque axiom (for example
Option::ok_or), that axiom appears in the theorem's closure and is documented as such; it is a
faithful stand-in for the standard method, not a sorry.
The evidence manifest
verification/evidence/EVIDENCE.json is a machine-readable inventory of the whole verification surface,
generated from the source tree by verification/evidence/collect-evidence.sh and checked for drift on every
push. It carries the live counts (Lean modules and theorems, the sorry count, the axiom-profiled
theorems, the extracted functions with their file hashes, and the Verus, Kani and runnable-proof
totals) and the pinned toolchains. As of this writing:
Lean specification 120 modules, 887 theorems, 0 sorry, 168 axiom-profiled
mechanical extraction 7 functions lowered from real MIR (Charon + Aeneas)
Verus refinement 5 source files over the real Rust bit-operations
Kani model checking 65 harnesses
runnable proofs 32 proof crates over the real source
Because the manifest is regenerated and diffed in CI, these numbers in the repo are always current and CI-checked, not a claim in prose that can rot. The green Lean, Verus, Kani and extraction jobs are the proof that the surface actually checks; the manifest is the inventory of it.
What is established
Over the real code, machine-checked, reproducible, and re-run on every push:
- The capability algebra is sound: authority only shrinks under attenuation and revoke, grant never removes, and the operations compose as the model says (Lean, plus Verus over the real bit-ops).
- Address-space isolation and page-permission invariants hold (Lean, Verus): no writable-and-executable page, no capsule reach outside its mappings.
- Freed memory is zeroized (Lean, plus the runnable zeroization checks): no cross-tenant residue.
- Attestation and anti-rollback reject the substitution and rollback cases (Lean, plus the runnable attestation proofs rejecting impersonation).
- Path canonicalization and the
/capsulesguard resist smuggling (Lean, plus fs_proofs fuzz). - The parsers never panic and never violate their invariants over millions of hostile inputs (fuzz).
- The crypto primitives conform to their standard vectors and reject tampering (crypto_proofs).
- The production source carries no panic path or stub (hygiene).
What is NOT established
Stated plainly, because an honest scope is the point:
- Not total functional correctness. NONOS does not prove that the entire kernel does exactly what a full specification says, the way seL4 does. It proves the security-critical properties above, not every behavior of every subsystem. A subsystem can be correct against these properties and still have a functional bug outside them.
- Kani is bounded. The model-checked properties hold for inputs up to the harness bound, not for all unbounded inputs.
- Known-answer vectors are conformance, not a universal proof. Passing FIPS/RFC/NIST vectors shows the implementation agrees with the standard on those vectors and rejects tampering; it is strong evidence of correctness but is not a proof of the algorithm for every possible input.
- Side channels are out of scope of these proofs. The crypto is portable software; the crypto pages state honestly where a primitive is not constant-time. The proofs are functional and structural, not timing proofs.
- The hardware is trusted below the IOMMU line. With the IOMMU backend not engaged, the DMA safety argument rests on the broker's software bounds plus non-malicious device hardware; this is stated on the DMA page.
Versus seL4, and versus marketing
Versus seL4. seL4 is the gold standard for total kernel verification: it proves full functional correctness of the whole kernel in Isabelle/HOL and proves that the C refines that model. NONOS does not match that scope and does not claim to. What NONOS does differently is (1) prove a focused set of security-critical properties rather than total correctness, (2) run those proofs over the actual Rust source rather than only an abstract model, and (3) build on a memory-safe language, which removes by construction a large class of the memory-corruption bugs a C kernel's proof must rule out. The two are different points on the same spectrum: seL4 proves everything about a minimal C kernel; NONOS proves the things that matter most about a larger Rust system, over the code that runs, and is honest that the rest is tested rather than proven.
Versus "formally verified" marketing. The common failure is to prove a model with no link to the
running code, or to leave theorems as sorry, or to call a test suite a proof. NONOS's Lean carries
zero sorry, its Verus proofs are over the real bit-operations, its runnable proofs include the real
source, and every claim on this page is reproducible from a clean checkout by the commands below. The
scope is narrower than the marketing usually implies and stated as such.
Reproduce it
# Layer 0: source hygiene
cargo run --manifest-path nonos-verify/Cargo.toml -- hygiene
# Layer 1: runnable proofs over the real source
cd userland/fs_proofs && cargo test --release
cd userland/crypto_proofs && cargo test --release
# Layer 1b: bounded model-checking
cd userland/fs_proofs && cargo kani --output-format terse
# Layer 2: Lean theorems (requires the Lean toolchain in verification/lean)
cd verification/lean && lake build
# the axiom gate: prints each flagship theorem's axiom closure, must show no sorryAx
cd verification/lean && lake env lean AxiomProfile.lean
# the reproducible proof-corpus commitment (refuses to form on any sorry)
./verification/lean/proof-corpus-root.sh
# Layer 2c: mechanical extraction (requires Charon, Aeneas, and the Lean toolchain)
cd verification/extraction/lean && lake exe cache get && lake build
# The evidence manifest: regenerate and confirm it matches the committed copy
./verification/evidence/collect-evidence.sh | diff - verification/evidence/EVIDENCE.json
Source map
verification/evidence/EVIDENCE.json the machine-readable inventory, CI-checked for drift
verification/evidence/collect-evidence.sh regenerates the manifest from the source tree
verification/README.md the layered framing and the run commands
verification/ARCHITECTURE.md the thesis, threat model, and what is / is not established
verification/lean/Nonos/*.lean the 887 Lean theorems across 120 modules (zero sorry)
verification/lean/AxiomProfile.lean the 168-theorem axiom gate (no sorryAx, standard axioms only)
verification/lean/proof-corpus-root.sh the reproducible commitment over the proven corpus
verification/extraction/ Charon + Aeneas crates and the proofs on the extracted MIR
verification/verus/src/*.rs the Verus refinement of capabilities, paging, IPC lengths
userland/fs_proofs/ runnable + Kani proofs over the real VFS/parser/attestation code
userland/crypto_proofs/ the crypto known-answer and tamper-rejection proofs
userland/*_proofs/ 32 proof crates, one guarantee per subsystem, over the real source
nonos-verify/ the source-hygiene gate
Every reference above is verified against those trees. The properties proven here are what the mission rests on, the capability algebra is specified on the capabilities page, the attestation on the attestation page, and the crypto primitives on the crypto pages.
Get involved
The verification surface grows one machine-checked property at a time, and the on-ramp is real. Good first contributions, roughly easiest first:
- Add a Lean theorem over a real invariant. Pick a small, self-contained function in
src/(a bounds check, an allocator, a validator), model it faithfully in a newverification/lean/Nonos/module, and prove its safety property. The tree is core-only, so no Mathlib to learn; the existing modules are the template. Add the flagship theorems toAxiomProfile.leanand confirm the axiom closure stays clean. - Strengthen a runnable proof. The
userland/*_proofs/crates run the real source. A new assertion, a tighter invariant, or a fuzz harness that finds a real bug is as welcome as a new theorem. - Mechanically extract a function. Add a real kernel function to an extraction crate under
verification/extraction/, regenerate with Charon and Aeneas, and prove its property on the extracted definition. This is the strongest kind of contribution because it closes the model-to-code gap. - Extend the evidence and CI. New proof systems, new drift checks, better badges: the manifest and the workflows are the project's public statement of what it proves.
The contributing guide covers the house rules; the reward program in rewards weights work on the trusted path and on verification highest.
FAQ
Is this "formally verified" like seL4? No, and the page says so plainly above. seL4 proves total functional correctness of a minimal C kernel. NONOS proves a focused set of security-critical properties, over the real Rust source, on a memory-safe language. Different scope, honestly stated.
How many theorems are there, really? 887 Lean theorems across 120 modules with zero sorry, plus
203 in the STARK attestation body, plus the Verus, Kani and runnable proofs. The live counts are in
verification/evidence/EVIDENCE.json, regenerated and diff-checked on every push, so the number in the repo is
never stale.
What does "zero sorry" actually guarantee? A sorry is Lean's placeholder for an unproven step. If
any proven theorem depended on one, its axiom closure would name sorryAx, and the CI axiom gate
(AxiomProfile.lean) fails on exactly that. So every stated theorem is fully proven under at most
Lean's three standard axioms.
How is the Lean tied to the code that runs? Three ways, strongest last: the runnable proofs execute
the real src/ through #[path]; the Verus proofs run over the real Rust bit-operations; and the
mechanical extraction lowers real functions from rustc's MIR and proves on that. Where a property is
only an abstract Lean theorem, a second proof connects it to the implementation.
Can I trust the numbers on this page? They are checked. verification/evidence/EVIDENCE.json is regenerated
from the source and diffed in CI, so a stale number fails the build. Every command in "Reproduce it"
runs from a clean checkout.
What is NOT proven? Total functional correctness, unbounded model checking, timing side channels, and hardware below the IOMMU line. The "What is NOT established" section above is the honest scope.
Where do I start reading the proofs? verification/lean/Nonos/Capability.lean for the algebra,
UserCopy.lean for a boundary check, and verification/extraction/lean/NonosExtraction/Refinement.lean
for a proof landed on mechanically-extracted code.