Skip to content

Commit 40cd9b2

Browse files
avrabeclaude
andcommitted
verify: fix/document 1 of 3 Kani matrix failures (format/merkle/wasm_module)
wasm_module — FIXED. The workflow filter was `wasm_module::tests`, which matched zero harnesses (the Kani module is named `component_proofs`, not `tests`), so `cargo kani --harness wasm_module::tests` exited non-zero. Pointed the matrix entry at `wasm_module::component_proofs`. The single underlying harness has no loops and no crypto, so it should run cleanly at --default-unwind 4. Mask lifted. merkle — DOCUMENTED. Three of five harnesses (proof_leaf_node_domain_separation, proof_leaf_hash_deterministic, proof_node_hash_deterministic) call SHA-256 with symbolic input. The sha2 crate's 64-round compression loop blows past --default-unwind 4, producing an unwinding-assertion failure before the assert is reached. proof_leaf_node_domain_separation additionally asks Kani to disprove SHA-256 collisions, which is outside bounded model checking entirely. Added per-harness doc-comments with the diagnosis. Mask retained. format — DOCUMENTED. Static analysis of the four harnesses shows sound, loop-free properties on 4 symbolic bytes, yet CI reports them failing under cargo kani --default-unwind 4 --harness format. Suspected SMT-state-space blow-up similar to the documented slowness already noted in proof_consistency_validation_agrees_with_detection. Added a mod-level doc-comment recording the analysis and the suggested next attempt (split harnesses per magic-byte pattern). Mask retained. Also restructured the Kani matrix to carry `tolerate_failure` per include entry so the step-level continue-on-error is now per-matrix- job rather than blanket — this is what audit C-7's partial-closure plan required. Closes audit C-7 partially — 1 of 3 jobs no longer needs continue-on-error. varint + dsse remain unmasked; wasm_module is newly unmasked; merkle + format stay masked with explicit diagnosis. Refs: audit C-7 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 64f4f54 commit 40cd9b2

4 files changed

Lines changed: 96 additions & 8 deletions

File tree

.github/workflows/formal-verification.yml

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,38 @@ jobs:
6464
include:
6565
- module: varint
6666
harness: wasm_module::varint
67+
tolerate_failure: false
6768
- module: merkle
6869
harness: signature::keyless::merkle
70+
# WIP — see audit C-7 partial closure. Three of the five harnesses
71+
# in this module call SHA-256 with symbolic input; the sha2 crate's
72+
# 64-round compression loop exceeds --default-unwind 4. Bumping the
73+
# per-harness #[kani::unwind] is necessary but may still hit Kani
74+
# SMT timeouts on SHA-256. proof_leaf_node_domain_separation also
75+
# asks Kani to prove SHA-256 collision-resistance, which is outside
76+
# the scope of bounded model checking entirely.
77+
tolerate_failure: true
6978
- module: dsse
7079
harness: dsse
80+
tolerate_failure: false
7181
- module: format
7282
harness: format
83+
# WIP — see audit C-7 partial closure. CI observation: the format
84+
# harnesses fail under cargo kani --default-unwind 4 despite static
85+
# analysis showing the properties are sound and loop-free. Suspected
86+
# root cause is Kani SMT slowness on 4 fully-symbolic bytes combined
87+
# with multiple equality checks (similar to the documented blow-up
88+
# noted in proof_consistency_validation_agrees_with_detection).
89+
# Next attempt: try concretizing each magic-byte pattern as its own
90+
# harness rather than enumerating in one 4-byte-symbolic harness.
91+
tolerate_failure: true
7392
- module: wasm_module
74-
harness: wasm_module::tests
93+
# Filter was wasm_module::tests, which matched no harness (the
94+
# module is named component_proofs, not tests). Fixed in this PR
95+
# so the single harness — header mutual-exclusivity, no loops, no
96+
# crypto — actually runs.
97+
harness: wasm_module::component_proofs
98+
tolerate_failure: false
7599
steps:
76100
- uses: actions/checkout@v4
77101

@@ -85,18 +109,16 @@ jobs:
85109
cargo install --locked kani-verifier
86110
cargo kani setup
87111
88-
# Audit 2026-04-30 finding C-7 (partial): on inspection, several
89-
# matrix jobs (format, merkle, wasm_module) fail when continue-on-error
90-
# is removed. The harnesses cover specific bounded properties and not
91-
# all are passing today. Mask retained per-job until the failing
92-
# harnesses are fixed; varint and dsse pass cleanly today and could be
93-
# ungated in a follow-up.
112+
# Audit 2026-04-30 finding C-7 (partial): mask is now per-matrix-entry via
113+
# matrix.tolerate_failure. varint, dsse, and wasm_module run unmasked;
114+
# merkle and format remain masked with diagnosis at the matrix include
115+
# site above and in the harness doc-comments.
94116
- name: Run Kani ${{ matrix.module }} proofs
95117
env:
96118
HARNESS: ${{ matrix.harness }}
97119
run: cargo kani -p wsc --default-unwind 4 --output-format terse --harness "$HARNESS"
98120
timeout-minutes: 60
99-
continue-on-error: true # WIP — see audit C-7 partial closure
121+
continue-on-error: ${{ matrix.tolerate_failure }}
100122

101123
rocq-proofs:
102124
name: Rocq/coq-of-rust Translation

src/lib/src/format/mod.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,26 @@ mod tests {
205205
// ============================================================================
206206
// Kani proof harnesses for format detection
207207
// ============================================================================
208+
//
209+
// AUDIT C-7 (partial closure): the `Kani format` matrix entry remains masked
210+
// (continue-on-error). Static analysis of the four harnesses below shows the
211+
// properties are sound, loop-free, and operate on small symbolic inputs (4
212+
// bytes max). They should all pass — but they do not, on CI, with
213+
// `cargo kani -p wsc --default-unwind 4 --harness format`.
214+
//
215+
// Suspected causes, to investigate next:
216+
// 1. `proof_consistency_validation_agrees_with_detection` already documents
217+
// that the previous variant blew up the SMT state space via `format!()`
218+
// in an unreachable error path. The current variant inlines the logic,
219+
// but the same Kani slowness pattern may apply to other harnesses too.
220+
// 2. assert_ne! on &'static str compiles to a length+pointer/byte compare;
221+
// Kani may force evaluation of both sides as symbolic, ballooning state.
222+
// 3. The 4-byte fully-symbolic input space (~4 billion combinations) is
223+
// tractable for SMT only if the equality checks short-circuit cleanly;
224+
// a Kani version regression could change this.
225+
// Next-attempt direction: split each magic-byte pattern into its own
226+
// per-pattern harness (concrete on one pattern, symbolic on the others) to
227+
// localize whichever harness is hanging. Until then this job stays masked.
208228
#[cfg(kani)]
209229
mod proofs {
210230
use super::*;

src/lib/src/signature/keyless/merkle.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,12 @@ mod tests {
459459
// ============================================================================
460460
// Kani proof harnesses for Merkle tree operations
461461
// ============================================================================
462+
//
463+
// AUDIT C-7 (partial closure): the `Kani merkle` matrix entry remains
464+
// masked (continue-on-error). proof_largest_power_of_two_* run cleanly,
465+
// but the three SHA-256-touching harnesses (proof_leaf_node_domain_separation,
466+
// proof_leaf_hash_deterministic, proof_node_hash_deterministic) do not pass
467+
// at --default-unwind 4 — see per-harness doc-comments below for diagnosis.
462468
#[cfg(kani)]
463469
mod proofs {
464470
use super::*;
@@ -504,6 +510,19 @@ mod proofs {
504510
///
505511
/// This is the domain separation property: leaf hash uses 0x00 prefix,
506512
/// node hash uses 0x01 prefix, so they cannot collide.
513+
///
514+
/// AUDIT C-7 (partial closure) — currently masked in CI.
515+
/// This harness is unprovable with Kani as written:
516+
/// 1. Verifying assert_ne! on two SHA-256 outputs requires reasoning
517+
/// about SHA-256 collision-resistance — a cryptographic assumption,
518+
/// not a computational property bounded model checking can discharge.
519+
/// 2. Even setting that aside, the sha2 crate's compression loop runs
520+
/// 64 rounds; --default-unwind 4 cannot unroll it, so the harness
521+
/// hits an unwinding-assertion failure before reaching the assert.
522+
/// To make this prov-able the leaf/node domain-separation property should
523+
/// be moved to a tool that can model SHA-256 as an uninterpreted function
524+
/// (e.g. Verus or Lean axiomatisation). Until then, the prefix-byte check
525+
/// in #[cfg(test)] (test_node_hash_computation) is the operational guard.
507526
#[kani::proof]
508527
fn proof_leaf_node_domain_separation() {
509528
// For a 64-byte input that could be interpreted as either a leaf or
@@ -524,6 +543,14 @@ mod proofs {
524543
}
525544

526545
/// Prove: compute_leaf_hash is deterministic.
546+
///
547+
/// AUDIT C-7 (partial closure) — currently masked in CI.
548+
/// Determinism is trivial for any pure Rust function, but Kani still
549+
/// has to fully symbolically execute the SHA-256 compression on both
550+
/// calls. That loop is 64 rounds, so --default-unwind 4 produces an
551+
/// unwinding-assertion failure long before the assert_eq is reached.
552+
/// A per-harness #[kani::unwind(65)] is necessary; even then Kani's
553+
/// SMT backend may time out within the 60-minute job budget.
527554
#[kani::proof]
528555
fn proof_leaf_hash_deterministic() {
529556
let b0: u8 = kani::any();
@@ -536,6 +563,12 @@ mod proofs {
536563
}
537564

538565
/// Prove: compute_node_hash is deterministic.
566+
///
567+
/// AUDIT C-7 (partial closure) — currently masked in CI.
568+
/// Same blocker as proof_leaf_hash_deterministic: SHA-256's 64-round
569+
/// compression loop is too deep for --default-unwind 4 and a per-harness
570+
/// #[kani::unwind(65)] may still hit SMT timeout because inputs are two
571+
/// fully-symbolic 32-byte arrays.
539572
#[kani::proof]
540573
fn proof_node_hash_deterministic() {
541574
let l: [u8; 32] = [kani::any(); 32];

src/lib/src/wasm_module/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,10 +1026,23 @@ mod tests {
10261026
// ============================================================================
10271027
// Kani proof harnesses for WASM module/component headers
10281028
// ============================================================================
1029+
//
1030+
// AUDIT C-7 (partial closure): the `Kani wasm_module` matrix entry is
1031+
// unmasked as of this PR. The previous workflow filter was
1032+
// `--harness wasm_module::tests`, which matched zero harnesses (this module
1033+
// is named `component_proofs`, not `tests`) and therefore caused Kani to
1034+
// exit non-zero. The workflow now uses `wasm_module::component_proofs`.
1035+
//
1036+
// The single harness below has no loops and no crypto; it should run in
1037+
// well under a minute at --default-unwind 4.
10291038
#[cfg(kani)]
10301039
mod component_proofs {
10311040
use super::*;
10321041

1042+
/// Prove: a WASM module header and a WASM component header are mutually
1043+
/// exclusive. They share the first four magic bytes (\0asm) but differ at
1044+
/// byte 4 (0x01 for modules, 0x0d for components), so no 8-byte sequence
1045+
/// can satisfy both.
10331046
#[kani::proof]
10341047
fn proof_component_module_header_mutual_exclusivity() {
10351048
let b: [u8; 8] = kani::any();

0 commit comments

Comments
 (0)