-
Notifications
You must be signed in to change notification settings - Fork 80
feat(fuzzer): add bounded fuzzing and CI action for prism-core #298
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| name: Fuzz | ||
|
|
||
| on: | ||
| push: | ||
| branches: ["main"] | ||
| pull_request: | ||
|
|
||
| jobs: | ||
| fuzz: | ||
| name: Run rust-address-fuzzer | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Install Rust toolchain | ||
| uses: dtolnay/rust-toolchain@stable | ||
|
|
||
| - name: Run Fuzzer | ||
| run: | | ||
| cd examples/rust-address-fuzzer | ||
| cargo run --release -- --random 100000 --max-iterations 100000 | ||
|
|
||
| - name: Upload Reproducer | ||
| if: failure() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: fuzzer-reproducer | ||
| path: examples/rust-address-fuzzer/reproducer.txt | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| mod parse; | ||
| mod report; | ||
|
|
||
| use std::io::{self, BufRead}; | ||
| use std::path::PathBuf; | ||
|
|
@@ -26,6 +27,10 @@ struct Cli { | |
| #[arg(long, value_name = "U64")] | ||
| seed: Option<u64>, | ||
|
|
||
| /// Stop fuzzing after this many iterations | ||
| #[arg(long, value_name = "N")] | ||
| max_iterations: Option<usize>, | ||
|
|
||
| /// Print every result, not just failures | ||
| #[arg(long, short)] | ||
| verbose: bool, | ||
|
|
@@ -37,6 +42,7 @@ struct Stats { | |
| ok: usize, | ||
| err: usize, | ||
| panics: usize, | ||
| report: report::Report, | ||
| } | ||
|
|
||
| fn main() { | ||
|
|
@@ -54,20 +60,26 @@ fn main() { | |
| eprintln!("PRNG seed: {seed}"); | ||
| } | ||
|
|
||
| let stats = if let Some(n) = cli.random { | ||
| run_random(&mut rng, n, cli.verbose) | ||
| let mut stats = if let Some(n) = cli.random { | ||
| let max = cli.max_iterations.unwrap_or(n); | ||
| run_random(&mut rng, max.min(n), cli.verbose) | ||
| } else if let Some(path) = cli.corpus { | ||
| run_corpus(&path, cli.verbose) | ||
| run_corpus(&path, cli.verbose, cli.max_iterations) | ||
| } else { | ||
| run_stdin(cli.verbose) | ||
| run_stdin(cli.verbose, cli.max_iterations) | ||
| }; | ||
|
|
||
| stats.report.inputs_run = stats.total; | ||
| stats.report.findings_count = stats.panics; // In the future, logic errors would also go here. | ||
|
|
||
| eprintln!( | ||
| "Done – {} inputs | {} ok | {} err | {} panics", | ||
| stats.total, stats.ok, stats.err, stats.panics | ||
| "Done – {} inputs | {} ok | {} err | {} findings", | ||
| stats.total, stats.ok, stats.err, stats.report.findings_count | ||
| ); | ||
|
|
||
| if stats.panics > 0 { | ||
| stats.report.print_json(); | ||
|
|
||
| if stats.report.findings_count > 0 || stats.report.divergences > 0 { | ||
| std::process::exit(1); | ||
| } | ||
| } | ||
|
|
@@ -80,41 +92,55 @@ fn run_random(rng: &mut StdRng, n: usize, verbose: bool) -> Stats { | |
| stats | ||
| } | ||
|
|
||
| fn run_corpus(path: &PathBuf, verbose: bool) -> Stats { | ||
| fn run_corpus(path: &PathBuf, verbose: bool, max_iters: Option<usize>) -> Stats { | ||
| let file = std::fs::File::open(path).unwrap_or_else(|e| { | ||
| eprintln!("error: cannot open corpus file {}: {e}", path.display()); | ||
| std::process::exit(2); | ||
| }); | ||
| let mut stats = Stats::default(); | ||
| for line in io::BufReader::new(file).lines() { | ||
| for (i, line) in io::BufReader::new(file).lines().enumerate() { | ||
| if let Some(m) = max_iters { | ||
| if i >= m { break; } | ||
| } | ||
| fuzz_one(&line.unwrap_or_default(), verbose, &mut stats); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Do not fuzz an empty string on input read failure.
📍 Affects 1 file
🤖 Prompt for AI Agents |
||
| } | ||
| stats | ||
| } | ||
|
|
||
| fn run_stdin(verbose: bool) -> Stats { | ||
| fn run_stdin(verbose: bool, max_iters: Option<usize>) -> Stats { | ||
| let mut stats = Stats::default(); | ||
| for line in io::stdin().lock().lines() { | ||
| for (i, line) in io::stdin().lock().lines().enumerate() { | ||
| if let Some(m) = max_iters { | ||
| if i >= m { break; } | ||
| } | ||
| fuzz_one(&line.unwrap_or_default(), verbose, &mut stats); | ||
| } | ||
| stats | ||
| } | ||
|
|
||
| fn fuzz_one(input: &str, verbose: bool, stats: &mut Stats) { | ||
| stats.total += 1; | ||
| match parse::parse(input) { | ||
| Ok(addr) => { | ||
| let input_owned = input.to_owned(); | ||
| let res = std::panic::catch_unwind(|| parse::parse(&input_owned)); | ||
|
|
||
| match res { | ||
| Ok(Ok(addr)) => { | ||
| stats.ok += 1; | ||
| if verbose { | ||
| eprintln!("OK {:?} ← {input:?}", addr.kind()); | ||
| } | ||
| } | ||
| Err(e) => { | ||
| Ok(Err(e)) => { | ||
| stats.err += 1; | ||
| if verbose { | ||
| eprintln!("ERR {e:?} ← {input:?}"); | ||
| } | ||
| } | ||
| Err(_) => { | ||
| stats.panics += 1; | ||
| eprintln!("PANIC ← {input:?}"); | ||
| let _ = std::fs::write("reproducer.txt", input); | ||
|
Comment on lines
+139
to
+142
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Preserve each distinct panic reproducer. Writing every finding to
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| pub struct Report { | ||
| pub inputs_run: usize, | ||
| pub findings_count: usize, | ||
| pub divergences: usize, | ||
| } | ||
|
|
||
| impl Report { | ||
| pub fn new() -> Self { | ||
| Self { | ||
| inputs_run: 0, | ||
| findings_count: 0, | ||
| divergences: 0, | ||
| } | ||
| } | ||
|
Comment on lines
+1
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
ast-grep outline examples/rust-address-fuzzer/src/main.rs --items all --type struct,impl
rg -n -A10 -B3 'struct Stats|impl Default for Stats|derive\(.*Default' examples/rust-address-fuzzer/src/main.rsRepository: Boxkit-Labs/stellar-address-kit Length of output: 452 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== report.rs =="
cat -n examples/rust-address-fuzzer/src/report.rs
echo
echo "== main.rs stats section =="
sed -n '1,90p' examples/rust-address-fuzzer/src/main.rsRepository: Boxkit-Labs/stellar-address-kit Length of output: 3517 Implement 🤖 Prompt for AI Agents |
||
|
|
||
| pub fn print_json(&self) { | ||
| println!( | ||
| r#"{{ | ||
| "inputs_run": {}, | ||
| "findings_count": {}, | ||
| "divergences": {} | ||
| }}"#, | ||
| self.inputs_run, self.findings_count, self.divergences | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Run the fuzzer on every push.
The branch filter limits push-triggered fuzzing to
main; direct pushes to other branches are skipped, contrary to the PR objective.🤖 Prompt for AI Agents