Workstream: Verification · Part of #89
Context
The CEL engine is verified against the cel-spec conformance corpus (#90) — ~2,123 example-based tests. That proves the engine matches CEL on the cases the corpus enumerates. It does not prove the engine is total over the whole input domain. For a fail-closed ATO target, the residual risk is exactly the adversarial edge a finite corpus never enumerates: i64::MIN / -1, NaN → int, u64::MAX vs i64, range boundaries, etc.
Kani (bit-precise bounded model checker on CBMC) closes that gap: it proves a property holds for all inputs within a bound. Kani never enters the shipped binary or the dependency tree, so it adds zero FIPS/airgap surface — it strengthens the ADR-0002 "fully owned, fully verified" story without compromising it.
Scope (Tier 1 — the bullseye)
Kani harnesses over the pure, total scalar functions — best fit for symbolic execution (scalars, no strings, no heap, no recursion). These functions are already factored as pure helpers (eval/funcs/convert.rs documents itself as "exhaustively testable without the evaluator").
Target files:
crates/schema-forge-cel/src/eval/ops.rs — arithmetic (Add/Sub/Mul/Div/Rem, unary Neg), heterogeneous numeric comparison, Timestamp ± Duration, Duration arithmetic.
crates/schema-forge-cel/src/eval/funcs/convert.rs — int() / uint() / double() conversions, duration-nanos grouping.
Properties to prove, for every scalar input:
- Totality — never panics (no
unwrap/overflow-panic/unreachable).
- No silent wrap/truncate — arithmetic returns the mathematically correct value or
Err(Overflow); never a wrapped result.
- Conversion soundness — out-of-range /
NaN / ±Inf reject with Err, never a UB cast or silent saturation against CEL semantics.
Concrete first harnesses (each a known landmine, each ~5 lines of kani::any()):
Approach
- Harnesses behind
#[cfg(kani)] in schema-forge-cel; run via cargo kani as a separate, slow CI job (NOT the nextest gate).
- Use current Kani function contracts (
#[kani::requires] / #[kani::ensures]) to state the fail-closed postcondition directly on each helper.
- Bound any recursive/loop proof with
#[kani::unwind(N)] / loop contracts.
Non-goals (deliberately routed elsewhere)
- Lexer / parser (
lexer.rs, parser.rs) — symbolic strings detonate CBMC. Cover the string front-end with the conformance corpus plus a cargo-fuzz differential target (parse → eval panic-freedom). Tracked separately.
- The recursive tree-walking
evaluate over arbitrary ASTs — unbounded recursion; owned by the corpus + fuzzing (stack-depth DoS hardening already exists there).
Follow-on (later tiers, out of scope here)
- Tier 2: comparison total-order soundness (folded into Tier 1 above where cheap).
- Tier 3 (bounded,
#[kani::unwind]): value/bridge.rs fail-closed contract (unsupported → Err, never Null); check::infer no-panic for bounded AST depth.
Acceptance
Dependencies: Engine core (#107/#108/#109) complete. Independent of the remaining capability work (#95). Soft.
Workstream: Verification · Part of #89
Context
The CEL engine is verified against the cel-spec conformance corpus (#90) — ~2,123 example-based tests. That proves the engine matches CEL on the cases the corpus enumerates. It does not prove the engine is total over the whole input domain. For a fail-closed ATO target, the residual risk is exactly the adversarial edge a finite corpus never enumerates:
i64::MIN / -1,NaN → int,u64::MAXvsi64, range boundaries, etc.Kani (bit-precise bounded model checker on CBMC) closes that gap: it proves a property holds for all inputs within a bound. Kani never enters the shipped binary or the dependency tree, so it adds zero FIPS/airgap surface — it strengthens the ADR-0002 "fully owned, fully verified" story without compromising it.
Scope (Tier 1 — the bullseye)
Kani harnesses over the pure, total scalar functions — best fit for symbolic execution (scalars, no strings, no heap, no recursion). These functions are already factored as pure helpers (
eval/funcs/convert.rsdocuments itself as "exhaustively testable without the evaluator").Target files:
crates/schema-forge-cel/src/eval/ops.rs— arithmetic (Add/Sub/Mul/Div/Rem, unaryNeg), heterogeneous numeric comparison,Timestamp ± Duration,Durationarithmetic.crates/schema-forge-cel/src/eval/funcs/convert.rs—int()/uint()/double()conversions, duration-nanos grouping.Properties to prove, for every scalar input:
unwrap/overflow-panic/unreachable).Err(Overflow); never a wrapped result.NaN/±Infreject withErr, never a UB cast or silent saturation against CEL semantics.Concrete first harnesses (each a known landmine, each ~5 lines of
kani::any()):i64::MIN / -1andi64::MIN % -1→ range errorchecked_neg(i64::MIN)overflow → errorf64 → i64/u64forNaN,±Inf, and values just past2^63/2^64(mantissa-precision boundary)u64↔i64mixed comparison — sign-correct across the full cross product, incl.u64 > i64::MAXTimeDeltaadd/sub andTimestamp ± Durationat theDateTimerange edges → range error, not panicAdd/Sub/Muloverflow →Err, never wrap (i64 and u64)Approach
#[cfg(kani)]inschema-forge-cel; run viacargo kanias a separate, slow CI job (NOT the nextest gate).#[kani::requires]/#[kani::ensures]) to state the fail-closed postcondition directly on each helper.#[kani::unwind(N)]/ loop contracts.Non-goals (deliberately routed elsewhere)
lexer.rs,parser.rs) — symbolic strings detonate CBMC. Cover the string front-end with the conformance corpus plus acargo-fuzzdifferential target (parse → evalpanic-freedom). Tracked separately.evaluateover arbitrary ASTs — unbounded recursion; owned by the corpus + fuzzing (stack-depth DoS hardening already exists there).Follow-on (later tiers, out of scope here)
#[kani::unwind]):value/bridge.rsfail-closed contract (unsupported →Err, neverNull);check::inferno-panic for bounded AST depth.Acceptance
cargo kaniharnesses for the Tier 1 properties above, green.Dependencies: Engine core (#107/#108/#109) complete. Independent of the remaining capability work (#95). Soft.