Merge pull request #128 from 0xlillith/fix/fuzz-seed-range

fix: validate fuzz seed ranges without overflow
This commit is contained in:
bherbruck authored and GitHub committed 2026-10-11 07:58:04 -04:00
commit d47b5d3a29
3 files changed
+42 -6

No files matched your search

+7 -6
View File
@@ -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<Job> = Vec::new();
let mut results: Vec<Value> = 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;
+31
View File
@@ -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,"));
}
+4
View File
@@ -11,6 +11,10 @@ solvecraft-cli fuzz --from 0 --count 1000 --jobs 8 --timeout 90 --out runs/ --re
solvecraft-cli fuzz-one <seed> --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