|
| 1 | +//! Hostile-input suite: the front end is the first supply-chain boundary for |
| 2 | +//! AI-generated and untrusted source, so it must never panic and must fail |
| 3 | +//! closed (produce diagnostics) on malformed input rather than silently |
| 4 | +//! yielding a "clean" partial result. |
| 5 | +
|
| 6 | +use proptest::prelude::*; |
| 7 | + |
| 8 | +/// Analyze every file under tests/corpus/malformed/. None may panic. Files not |
| 9 | +/// prefixed `gap-` must also fail closed (report a diagnostic); `gap-` files are |
| 10 | +/// known fail-open gaps the fuzzer surfaced (still must not panic). |
| 11 | +#[test] |
| 12 | +fn malformed_corpus_never_panics_and_fails_closed() { |
| 13 | + let dir = std::path::Path::new("tests/corpus/malformed"); |
| 14 | + let mut entries: Vec<_> = std::fs::read_dir(dir) |
| 15 | + .expect("malformed corpus dir exists") |
| 16 | + .filter_map(|e| e.ok().map(|e| e.path())) |
| 17 | + .filter(|p| p.extension().map(|x| x == "rss").unwrap_or(false)) |
| 18 | + .collect(); |
| 19 | + entries.sort(); |
| 20 | + assert!(!entries.is_empty(), "malformed corpus is empty"); |
| 21 | + |
| 22 | + for path in entries { |
| 23 | + let source = std::fs::read_to_string(&path).unwrap_or_default(); |
| 24 | + let name = path.display().to_string(); |
| 25 | + let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); |
| 26 | + let result = std::panic::catch_unwind(|| rsscript::analyze_source(&name, &source)); |
| 27 | + assert!(result.is_ok(), "analyzer panicked on malformed input {name}"); |
| 28 | + let diagnostics = result.unwrap(); |
| 29 | + if !file_name.starts_with("gap-") { |
| 30 | + assert!( |
| 31 | + !diagnostics.is_empty(), |
| 32 | + "malformed input {name} produced no diagnostics (should fail closed)" |
| 33 | + ); |
| 34 | + } |
| 35 | + } |
| 36 | +} |
| 37 | + |
| 38 | +/// A few explicit adversarial strings (kept inline so the intent is visible). |
| 39 | +#[test] |
| 40 | +fn adversarial_strings_do_not_panic() { |
| 41 | + let inputs = [ |
| 42 | + "", |
| 43 | + "\"", |
| 44 | + "\u{202e}\u{202d}", |
| 45 | + "let x = \"\\(", |
| 46 | + "fn f() { match x {", |
| 47 | + "\u{0}\u{0}\u{0}", |
| 48 | + "fn f() -> Int { return 0x }", |
| 49 | + &"(".repeat(5000), |
| 50 | + &"fn f(){}\n".repeat(2000), |
| 51 | + ]; |
| 52 | + for (index, source) in inputs.iter().enumerate() { |
| 53 | + let result = |
| 54 | + std::panic::catch_unwind(|| rsscript::analyze_source("adversarial.rss", source)); |
| 55 | + assert!( |
| 56 | + result.is_ok(), |
| 57 | + "analyzer panicked on adversarial input #{index}: {source:?}" |
| 58 | + ); |
| 59 | + } |
| 60 | +} |
| 61 | + |
| 62 | +proptest! { |
| 63 | + // proptest catches panics and shrinks to a minimal reproducer, so a bare |
| 64 | + // call is the fuzz target: any string the generator produces must not crash |
| 65 | + // the front end. |
| 66 | + #![proptest_config(ProptestConfig { cases: 400, ..ProptestConfig::default() })] |
| 67 | + |
| 68 | + #[test] |
| 69 | + fn arbitrary_text_never_panics_the_front_end(source in ".{0,400}") { |
| 70 | + let _ = rsscript::analyze_source("fuzz.rss", &source); |
| 71 | + } |
| 72 | + |
| 73 | + #[test] |
| 74 | + fn arbitrary_utf8_bytes_never_panic(bytes in proptest::collection::vec(any::<u8>(), 0..400)) { |
| 75 | + if let Ok(source) = String::from_utf8(bytes) { |
| 76 | + let _ = rsscript::analyze_source("fuzz.rss", &source); |
| 77 | + } |
| 78 | + } |
| 79 | + |
| 80 | + // Source biased toward RSScript tokens is more likely to reach deep parser |
| 81 | + // and checker paths. |
| 82 | + #[test] |
| 83 | + fn token_soup_never_panics( |
| 84 | + tokens in proptest::collection::vec( |
| 85 | + prop::sample::select(vec![ |
| 86 | + "fn", "let", "return", "struct", "native", "effects", "read", "mut", |
| 87 | + "take", "fresh", "match", "(", ")", "{", "}", "<", ">", ":", ",", |
| 88 | + "->", "Int", "String", "x", "\"", "|", "=", "0", "99999999999", |
| 89 | + ]), |
| 90 | + 0..80, |
| 91 | + ), |
| 92 | + ) { |
| 93 | + let source = tokens.join(" "); |
| 94 | + let _ = rsscript::analyze_source("fuzz.rss", &source); |
| 95 | + } |
| 96 | +} |
0 commit comments