From 347230f5d396aac7e437df2d8e6d337df84ac295 Mon Sep 17 00:00:00 2001 From: lillith <35337584+0xlillith@users.noreply.github.com> Date: Sun, 11 Oct 2026 00:41:49 +0200 Subject: [PATCH] fix: validate fuzz seed ranges without overflow --- apps/solvecraft-cli/src/fuzz.rs | 13 ++++++----- apps/solvecraft-cli/tests/fuzz_range.rs | 31 +++++++++++++++++++++++++ docs/robustness.md | 4 ++++ 3 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 apps/solvecraft-cli/tests/fuzz_range.rs diff --git a/apps/solvecraft-cli/src/fuzz.rs b/apps/solvecraft-cli/src/fuzz.rs index f8fc26b3..7a985f64 100644 --- a/apps/solvecraft-cli/src/fuzz.rs +++ b/apps/solvecraft-cli/src/fuzz.rs @@ -64,6 +64,8 @@ struct Job { pub fn run(args: &[String]) -> Result<(), String> { let from = flag(args, "--from", 0u64); let count = flag(args, "--count", 100u64); + let last = from.checked_add(count.saturating_sub(1)).ok_or("seed range exceeds u64::MAX: reduce --from or --count")?; + let mut seeds = (count > 0).then_some(from..=last).into_iter().flatten().peekable(); let jobs = flag(args, "--jobs", 4usize).clamp(1, 64); let timeout = Duration::from_secs(flag(args, "--timeout", 120u64)); let steps = flag(args, "--steps", 8usize); @@ -74,14 +76,14 @@ pub fn run(args: &[String]) -> Result<(), String> { if let Some(d) = &out_dir { std::fs::create_dir_all(d).map_err(|e| format!("{d}: {e}"))?; } - let mut next = from; let mut running: Vec = Vec::new(); let mut results: Vec = Vec::new(); let t0 = Instant::now(); - while next < from + count || !running.is_empty() { - while running.len() < jobs && next < from + count { + while seeds.peek().is_some() || !running.is_empty() { + while running.len() < jobs { + let Some(seed) = seeds.next() else { break }; let mut cmd = Command::new(&exe); - cmd.args(["fuzz-one", &next.to_string(), "--steps", &steps.to_string()]); + cmd.args(["fuzz-one", &seed.to_string(), "--steps", &steps.to_string()]); if minimise { cmd.arg("--minimise"); } @@ -96,8 +98,7 @@ pub fn run(args: &[String]) -> Result<(), String> { } }); } - running.push(Job { seed: next, child, started: Instant::now(), err: rx, last: String::new() }); - next += 1; + running.push(Job { seed, child, started: Instant::now(), err: rx, last: String::new() }); } std::thread::sleep(Duration::from_millis(50)); let mut i = 0; diff --git a/apps/solvecraft-cli/tests/fuzz_range.rs b/apps/solvecraft-cli/tests/fuzz_range.rs new file mode 100644 index 00000000..60c2ae20 --- /dev/null +++ b/apps/solvecraft-cli/tests/fuzz_range.rs @@ -0,0 +1,31 @@ +//! Seed ranges must not crash or wrap, including at the last representable seed. + +use std::process::Command; + +#[test] +fn overflowing_seed_range_is_an_error_not_a_panic() { + let output = + Command::new(env!("CARGO_BIN_EXE_solvecraft-cli")).args(["fuzz", "--from", "18446744073709551615", "--count", "2"]).output().unwrap(); + assert_eq!(output.status.code(), Some(1)); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("seed range exceeds u64::MAX"), "{stderr}"); + assert!(!stderr.contains("panicked"), "{stderr}"); +} + +#[test] +fn empty_seed_range_at_maximum_is_valid() { + let output = + Command::new(env!("CARGO_BIN_EXE_solvecraft-cli")).args(["fuzz", "--from", "18446744073709551615", "--count", "0"]).output().unwrap(); + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + assert!(String::from_utf8_lossy(&output.stdout).starts_with("0 seeds, 0 design steps,")); +} + +#[test] +fn maximum_seed_runs_once_without_wrapping() { + let output = Command::new(env!("CARGO_BIN_EXE_solvecraft-cli")) + .args(["fuzz", "--from", "18446744073709551615", "--count", "1", "--steps", "0", "--timeout", "0"]) + .output() + .unwrap(); + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + assert!(String::from_utf8_lossy(&output.stdout).starts_with("1 seeds,")); +} diff --git a/docs/robustness.md b/docs/robustness.md index 440b96bd..4639aa10 100644 --- a/docs/robustness.md +++ b/docs/robustness.md @@ -11,6 +11,10 @@ solvecraft-cli fuzz --from 0 --count 1000 --jobs 8 --timeout 90 --out runs/ --re solvecraft-cli fuzz-one --minimise # one design, cut down to the steps that matter ``` +`--count` seeds are run starting at `--from`. A range that exceeds `u64::MAX` is +rejected before any jobs or output directories are created. The maximum seed +(`18446744073709551615`) is valid with `--count 1`; `--count 0` runs no seeds. + - **Designs** (`crates/engine/src/fuzz.rs`): a seeded generator starts from a box, an extruded sketch profile closed by an arc (sometimes notched), or a revolved section (partial or full turns, sometimes touching the axis). It then adds up to eight steps: joins and cuts of