diff --git a/Cargo.lock b/Cargo.lock index 5d9c2b5825..7f3779a132 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -233,7 +233,7 @@ dependencies = [ "mime", "multi_try", "multimap 0.10.1", - "nom", + "nom 7.1.3", "nom_locate", "parking_lot", "percent-encoding", @@ -1387,13 +1387,14 @@ dependencies = [ [[package]] name = "bnf" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c09ea5795b3dd735ff47c4b8adf64c46e3ce056fa3c4880b865a352e4c40a2" +checksum = "35b77b055f8cb1d566fa4ef55bc699f60eefb17927dc25fa454a05b6fabf7aa4" dependencies = [ - "getrandom 0.2.16", - "nom", - "rand 0.8.5", + "getrandom 0.3.4", + "hashbrown 0.16.1", + "nom 8.0.0", + "rand 0.9.2", "serde", "serde_json", ] @@ -4416,6 +4417,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "nom_locate" version = "4.2.0" @@ -4424,7 +4434,7 @@ checksum = "1e3c83c053b0713da60c5b8de47fe8e494fe3ece5267b2f23090a07a053ba8f3" dependencies = [ "bytecount", "memchr", - "nom", + "nom 7.1.3", ] [[package]] @@ -5637,7 +5647,7 @@ dependencies = [ "cookie-factory", "crc16", "log", - "nom", + "nom 7.1.3", ] [[package]] @@ -5941,7 +5951,6 @@ dependencies = [ "http 1.4.0", "libfuzzer-sys", "log", - "rand 0.8.5", "reqwest", "schemars", "serde", @@ -6335,9 +6344,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.148" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "indexmap 2.12.1", "itoa", diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 9cd07ba6d1..e892c8b0e2 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -14,11 +14,9 @@ libfuzzer-sys = "=0.4.10" apollo-federation = { path = "../apollo-federation" } apollo-parser.workspace = true apollo-smith.workspace = true -bnf = "0.5.0" +bnf = "0.6" env_logger = "0.11.0" log = "0.4" -# Required until https://github.com/shnewto/bnf/pull/175, remove when bnf 0.6 is out -rand = "=0.8.5" reqwest = { workspace = true, features = ["json", "blocking"] } serde_json.workspace = true diff --git a/fuzz/fuzz_targets/connector_selection_parse.rs b/fuzz/fuzz_targets/connector_selection_parse.rs index f817d03ba4..d29221b361 100644 --- a/fuzz/fuzz_targets/connector_selection_parse.rs +++ b/fuzz/fuzz_targets/connector_selection_parse.rs @@ -3,23 +3,26 @@ use std::sync::LazyLock; use apollo_federation::connectors::JSONSelection; +use bnf::CoverageGuided; use bnf::Grammar; use libfuzzer_sys::arbitrary; use libfuzzer_sys::arbitrary::Arbitrary; use libfuzzer_sys::fuzz_target; use libfuzzer_sys::Corpus; -use rand::rngs::StdRng; -fuzz_target!(|input: GeneratedSelection| -> Corpus { - // Generating a selection might choose a path which recurses too deeply, so - // we just mark those traversals as being rejected since they would require - // seeding and iterating the Rng. - let Some(selection) = input.0 else { +/// Generations per fuzz input. CoverageGuided prefers grammar productions +/// not yet exercised; multiple generations let that coverage accumulate. +const GENERATIONS_PER_INPUT: usize = 8; + +fuzz_target!(|input: GeneratedSelections| -> Corpus { + if input.0.is_empty() { return Corpus::Reject; - }; + } - let parsed = JSONSelection::parse(&selection).unwrap(); - drop(parsed); + for selection in &input.0 { + let parsed = JSONSelection::parse(selection).unwrap(); + drop(parsed); + } Corpus::Keep }); @@ -84,29 +87,30 @@ const BNF_GRAMMAR: &str = r##" "##; static GRAMMAR: LazyLock = LazyLock::new(|| BNF_GRAMMAR.parse().unwrap()); -struct GeneratedSelection(Option); -impl<'a> Arbitrary<'a> for GeneratedSelection { +/// One fuzz input: a seed produces multiple grammar-generated strings via +/// CoverageGuided, which prefers productions not yet used so we exercise +/// more of the grammar per input. +struct GeneratedSelections(Vec); +impl<'a> Arbitrary<'a> for GeneratedSelections { fn arbitrary(u: &mut libfuzzer_sys::arbitrary::Unstructured<'a>) -> arbitrary::Result { let bytes = <[u8; 32] as Arbitrary>::arbitrary(u)?; - let mut rng: StdRng = rand::SeedableRng::from_seed(bytes); + let mut strategy = CoverageGuided::from_seed(bytes); - let selection = GRAMMAR.generate_seeded(&mut rng).ok(); - Ok(GeneratedSelection(selection)) + let selections: Vec = (0..GENERATIONS_PER_INPUT) + .filter_map(|_| GRAMMAR.generate_seeded_with_strategy(&mut strategy).ok()) + .collect(); + Ok(GeneratedSelections(selections)) } } -impl std::fmt::Debug for GeneratedSelection { +impl std::fmt::Debug for GeneratedSelections { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0 - .as_deref() - .map(|selection| { - write!(f, "```original\n{}\n```", selection)?; - if let Ok(parsed) = JSONSelection::parse(selection) { - write!(f, "\n\n```pretty\n{}\n```", parsed)?; - } - - Ok(()) - }) - .unwrap_or(Ok(())) + for selection in &self.0 { + write!(f, "```original\n{}\n```", selection)?; + if let Ok(parsed) = JSONSelection::parse(selection) { + write!(f, "\n\n```pretty\n{}\n```", parsed)?; + } + } + Ok(()) } }