Merge pull request #128 from 0xlillith/fix/fuzz-seed-range
fix: validate fuzz seed ranges without overflow
This commit is contained in:
3 files changed
+42
-6
No files matched your search
@@ -64,6 +64,8 @@ struct Job {
|
|||||||
pub fn run(args: &[String]) -> Result<(), String> {
|
pub fn run(args: &[String]) -> Result<(), String> {
|
||||||
let from = flag(args, "--from", 0u64);
|
let from = flag(args, "--from", 0u64);
|
||||||
let count = flag(args, "--count", 100u64);
|
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 jobs = flag(args, "--jobs", 4usize).clamp(1, 64);
|
||||||
let timeout = Duration::from_secs(flag(args, "--timeout", 120u64));
|
let timeout = Duration::from_secs(flag(args, "--timeout", 120u64));
|
||||||
let steps = flag(args, "--steps", 8usize);
|
let steps = flag(args, "--steps", 8usize);
|
||||||
@@ -74,14 +76,14 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
|||||||
if let Some(d) = &out_dir {
|
if let Some(d) = &out_dir {
|
||||||
std::fs::create_dir_all(d).map_err(|e| format!("{d}: {e}"))?;
|
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 running: Vec<Job> = Vec::new();
|
||||||
let mut results: Vec<Value> = Vec::new();
|
let mut results: Vec<Value> = Vec::new();
|
||||||
let t0 = Instant::now();
|
let t0 = Instant::now();
|
||||||
while next < from + count || !running.is_empty() {
|
while seeds.peek().is_some() || !running.is_empty() {
|
||||||
while running.len() < jobs && next < from + count {
|
while running.len() < jobs {
|
||||||
|
let Some(seed) = seeds.next() else { break };
|
||||||
let mut cmd = Command::new(&exe);
|
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 {
|
if minimise {
|
||||||
cmd.arg("--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() });
|
running.push(Job { seed, child, started: Instant::now(), err: rx, last: String::new() });
|
||||||
next += 1;
|
|
||||||
}
|
}
|
||||||
std::thread::sleep(Duration::from_millis(50));
|
std::thread::sleep(Duration::from_millis(50));
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
|
|||||||
@@ -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,"));
|
||||||
|
}
|
||||||
@@ -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
|
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
|
- **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
|
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
|
turns, sometimes touching the axis). It then adds up to eight steps: joins and cuts of
|
||||||
|
|||||||
Reference in new issue
Block a user