Skip to main content

provable_contracts/
fuzz_gen.rs

1//! Coverage-guided fuzz target generation.
2//!
3//! Generates `libfuzzer_sys::fuzz_target!` harnesses from YAML contracts.
4//! Each fuzz target gates on contract preconditions and exercises the
5//! bound function, catching panics, sanitizer violations, and crashes.
6
7use std::fmt::Write;
8
9use crate::schema::Contract;
10
11/// Generate a libfuzzer fuzz target from a contract.
12///
13/// Returns the contents of a `fuzz/fuzz_targets/<stem>.rs` file.
14/// The target deserialises arbitrary bytes into the kernel's input type,
15/// filters inputs that violate preconditions, and calls the bound function.
16pub fn generate_fuzz_target(contract: &Contract, stem: &str) -> String {
17    let mut out = String::with_capacity(2048);
18
19    let _ = writeln!(out, "#![no_main]");
20    let _ = writeln!(
21        out,
22        "// Auto-generated fuzz target from contract: {}",
23        contract.metadata.description
24    );
25    let _ = writeln!(out, "// Regenerate with: pv fuzz contracts/{stem}.yaml");
26    let _ = writeln!(out);
27    let _ = writeln!(out, "use libfuzzer_sys::fuzz_target;");
28    let _ = writeln!(out);
29
30    // Collect preconditions from proof obligations
31    let preconditions: Vec<&str> = contract
32        .proof_obligations
33        .iter()
34        .filter(|ob| ob.obligation_type == crate::schema::ObligationType::Precondition)
35        .filter_map(|ob| ob.formal.as_deref())
36        .collect();
37
38    // Collect postconditions for assertion
39    let postconditions: Vec<&str> = contract
40        .proof_obligations
41        .iter()
42        .filter(|ob| ob.obligation_type == crate::schema::ObligationType::Postcondition)
43        .filter_map(|ob| ob.formal.as_deref())
44        .collect();
45
46    let _ = writeln!(out, "fuzz_target!(|data: &[u8]| {{");
47    let _ = writeln!(out, "    // Minimum input size guard");
48    let _ = writeln!(out, "    if data.len() < 8 {{ return; }}");
49    let _ = writeln!(out);
50
51    // Generate input parsing from bytes
52    let _ = writeln!(out, "    // Parse input: interpret bytes as f32 vector");
53    let _ = writeln!(
54        out,
55        "    let len = ((data[0] as usize) % 64).saturating_add(1);"
56    );
57    let _ = writeln!(out, "    let byte_len = len * 4;");
58    let _ = writeln!(out, "    if data.len() < 4 + byte_len {{ return; }}");
59    let _ = writeln!(out, "    let input: Vec<f32> = data[4..4 + byte_len]");
60    let _ = writeln!(out, "        .chunks_exact(4)");
61    let _ = writeln!(
62        out,
63        "        .map(|b| f32::from_le_bytes(b.try_into().unwrap()))"
64    );
65    let _ = writeln!(out, "        .collect();");
66    let _ = writeln!(out);
67
68    // Emit precondition gates
69    if preconditions.is_empty() {
70        let _ = writeln!(
71            out,
72            "    // No explicit preconditions — filter NaN/Inf for numerical stability"
73        );
74    } else {
75        let _ = writeln!(out, "    // Contract precondition gates");
76        for pre in &preconditions {
77            let _ = writeln!(out, "    // PRE: {pre}");
78        }
79    }
80    let _ = writeln!(
81        out,
82        "    if input.is_empty() || input.iter().any(|x| x.is_nan() || x.is_infinite()) {{"
83    );
84    let _ = writeln!(out, "        return;");
85    let _ = writeln!(out, "    }}");
86    let _ = writeln!(out);
87
88    // Emit kernel call (must not panic)
89    let fn_name = stem.replace('-', "_").replace("_v1", "");
90    let _ = writeln!(out, "    // Kernel under test — must not panic");
91    let _ = writeln!(out, "    let result = std::panic::catch_unwind(|| {{");
92    let _ = writeln!(out, "        let mut output = vec![0.0f32; input.len()];");
93    let _ = writeln!(
94        out,
95        "        // TODO: Wire up: {fn_name}(&input, &mut output);"
96    );
97    let _ = writeln!(out, "        output");
98    let _ = writeln!(out, "    }});");
99    let _ = writeln!(out);
100
101    // Emit postcondition checks on successful execution
102    if !postconditions.is_empty() {
103        let _ = writeln!(out, "    // Contract postcondition checks");
104        let _ = writeln!(out, "    if let Ok(output) = result {{");
105        for post in &postconditions {
106            let _ = writeln!(out, "        // POST: {post}");
107        }
108        let _ = writeln!(
109            out,
110            "        assert_eq!(output.len(), input.len(), \"output length mismatch\");"
111        );
112        let _ = writeln!(out, "    }}");
113    }
114
115    let _ = writeln!(out, "}});");
116
117    out
118}
119
120/// Generate a `fuzz/Cargo.toml` for the fuzz workspace member.
121pub fn generate_fuzz_cargo_toml(crate_name: &str) -> String {
122    let mut out = String::with_capacity(512);
123
124    let _ = writeln!(out, "[package]");
125    let _ = writeln!(out, "name = \"{crate_name}-fuzz\"");
126    let _ = writeln!(out, "version = \"0.0.0\"");
127    let _ = writeln!(out, "publish = false");
128    let _ = writeln!(out, "edition = \"2021\"");
129    let _ = writeln!(out);
130    let _ = writeln!(out, "[package.metadata]");
131    let _ = writeln!(out, "cargo-fuzz = true");
132    let _ = writeln!(out);
133    let _ = writeln!(out, "[dependencies]");
134    let _ = writeln!(out, "libfuzzer-sys = \"0.11\"");
135    let _ = writeln!(out);
136    let _ = writeln!(out, "# TODO: Add dependency on the crate under test");
137    let _ = writeln!(out, "# {crate_name} = {{ path = \"..\" }}");
138
139    out
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use crate::schema::parse_contract_str;
146
147    fn softmax_contract() -> Contract {
148        parse_contract_str(
149            r#"
150metadata:
151  version: "1.0.0"
152  description: "Softmax kernel"
153  references: ["Paper A"]
154equations:
155  softmax:
156    formula: "σ(x)_i = exp(x_i - max(x)) / Σ_j exp(x_j - max(x))"
157    domain: "x ∈ ℝ^n, n ≥ 1"
158proof_obligations:
159  - type: precondition
160    property: "Input vector is finite and non-empty"
161    formal: "∀i: ¬isNaN(x_i) ∧ ¬isInf(x_i) ∧ len(x) > 0"
162  - type: postcondition
163    property: "Output is a valid probability distribution"
164    formal: "len(σ(x)) = len(x) ∧ ∀i: 0 < σ(x)_i < 1 ∧ |Σ σ(x)_i - 1| < ε"
165  - type: invariant
166    property: "Output sums to 1"
167falsification_tests:
168  - id: FALSIFY-001
169    rule: "Normalization"
170    prediction: "sum ≈ 1.0"
171    if_fails: "Bug"
172kani_harnesses:
173  - id: KANI-001
174    obligation: "sums to 1"
175    bound: 8
176"#,
177        )
178        .unwrap()
179    }
180
181    #[test]
182    fn generates_fuzz_target() {
183        let contract = softmax_contract();
184        let output = generate_fuzz_target(&contract, "softmax-kernel-v1");
185
186        assert!(output.contains("#![no_main]"));
187        assert!(output.contains("use libfuzzer_sys::fuzz_target;"));
188        assert!(output.contains("fuzz_target!"));
189        assert!(output.contains("catch_unwind"));
190        assert!(output.contains("PRE:"));
191        assert!(output.contains("POST:"));
192    }
193
194    #[test]
195    fn generates_fuzz_target_no_preconditions() {
196        let contract = parse_contract_str(
197            r#"
198metadata:
199  version: "1.0.0"
200  description: "Simple kernel"
201  references: ["X"]
202equations:
203  f:
204    formula: "f(x) = x"
205proof_obligations:
206  - type: invariant
207    property: "identity"
208falsification_tests:
209  - id: F-001
210    rule: "id"
211    prediction: "f(x) = x"
212    if_fails: "bug"
213kani_harnesses:
214  - id: K-001
215    obligation: "id"
216    bound: 8
217"#,
218        )
219        .unwrap();
220
221        let output = generate_fuzz_target(&contract, "identity-v1");
222        assert!(output.contains("No explicit preconditions"));
223        assert!(output.contains("fuzz_target!"));
224    }
225
226    #[test]
227    fn generates_cargo_toml() {
228        let toml = generate_fuzz_cargo_toml("aprender");
229        assert!(toml.contains("aprender-fuzz"));
230        assert!(toml.contains("libfuzzer-sys"));
231        assert!(toml.contains("cargo-fuzz = true"));
232    }
233}