fuzz_qualification.rs (3736B)
1 use std::{collections::BTreeSet, fs, path::Path, process::Command}; 2 3 use serde::Deserialize; 4 5 #[derive(Debug, Deserialize)] 6 struct Contract { 7 schema_version: u16, 8 harness: String, 9 engine: String, 10 toolchain: String, 11 smoke_runs: u32, 12 max_input_bytes: usize, 13 targets: Vec<String>, 14 } 15 16 #[derive(Debug, Deserialize)] 17 struct HarnessManifest { 18 bin: Vec<Bin>, 19 } 20 21 #[derive(Debug, Deserialize)] 22 struct Bin { 23 name: String, 24 } 25 26 pub fn run(root: &Path) -> Result<(), String> { 27 let contract = load(root)?; 28 validate(root, &contract)?; 29 run_cargo_fuzz(root, &["check".to_owned()], &contract)?; 30 for target in &contract.targets { 31 run_cargo_fuzz( 32 root, 33 &[ 34 "run".to_owned(), 35 target.clone(), 36 "--".to_owned(), 37 format!("-runs={}", contract.smoke_runs), 38 format!("-max_len={}", contract.max_input_bytes), 39 ], 40 &contract, 41 )?; 42 } 43 Ok(()) 44 } 45 46 fn load(root: &Path) -> Result<Contract, String> { 47 let path = root.join("contracts/releases/fuzz_matrix.toml"); 48 let raw = fs::read_to_string(&path) 49 .map_err(|error| format!("failed to read {}: {error}", path.display()))?; 50 toml::from_str(&raw).map_err(|error| format!("failed to parse {}: {error}", path.display())) 51 } 52 53 fn validate(root: &Path, contract: &Contract) -> Result<(), String> { 54 if contract.schema_version != 1 55 || contract.engine != "libfuzzer" 56 || !contract.toolchain.starts_with("nightly-") 57 || contract.smoke_runs < 1_000 58 || contract.max_input_bytes < 16_384 59 { 60 return Err("invalid fuzz qualification contract".to_owned()); 61 } 62 let expected = contract.targets.iter().collect::<BTreeSet<_>>(); 63 if expected.len() != 8 || expected.len() != contract.targets.len() { 64 return Err("fuzz matrix requires exactly eight unique parser targets".to_owned()); 65 } 66 let path = root.join(&contract.harness).join("Cargo.toml"); 67 let raw = fs::read_to_string(&path) 68 .map_err(|error| format!("failed to read {}: {error}", path.display()))?; 69 let manifest = toml::from_str::<HarnessManifest>(&raw) 70 .map_err(|error| format!("failed to parse {}: {error}", path.display()))?; 71 let actual = manifest 72 .bin 73 .iter() 74 .map(|bin| &bin.name) 75 .collect::<BTreeSet<_>>(); 76 if actual != expected { 77 return Err(format!( 78 "fuzz target inventory drift: expected {expected:?}, found {actual:?}" 79 )); 80 } 81 Ok(()) 82 } 83 84 fn run_cargo_fuzz(root: &Path, args: &[String], contract: &Contract) -> Result<(), String> { 85 let mut command_args = vec![ 86 format!("+{}", contract.toolchain), 87 "fuzz".to_owned(), 88 args[0].clone(), 89 "--fuzz-dir".to_owned(), 90 contract.harness.clone(), 91 ]; 92 command_args.extend_from_slice(&args[1..]); 93 eprintln!("cargo {}", command_args.join(" ")); 94 let status = Command::new("cargo") 95 .args(&command_args) 96 .current_dir(root) 97 .status() 98 .map_err(|error| format!("failed to start cargo-fuzz: {error}"))?; 99 if status.success() { 100 Ok(()) 101 } else { 102 Err(format!( 103 "fuzz qualification failed: cargo {}", 104 command_args.join(" ") 105 )) 106 } 107 } 108 109 #[cfg(test)] 110 mod tests { 111 use super::{load, validate}; 112 113 #[test] 114 fn current_contract_matches_all_parser_harnesses() { 115 let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) 116 .parent() 117 .and_then(std::path::Path::parent) 118 .expect("workspace root"); 119 validate(root, &load(root).expect("contract")).expect("valid fuzz matrix"); 120 } 121 }