diff --git a/Makefile b/Makefile index dfefc0c..4d0e8b2 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ trace: exit 1; \ fi @echo "Converting seed to Wasm and tracing execution (release mode)..." - @cargo run --release -p spacewasm-fuzzing --bin seed_to_wasm $(CRASH) --stdout 2>/dev/null | \ + @cargo run --release -p spacewasm-fuzzing --bin seed_to_wasm -- $(if $(TARGET),--target $(TARGET)) $(CRASH) --stdout 2>/dev/null | \ cargo run --release -p spacewasm_util --bin spacewasm-trace -- --stdin --limit $(or $(LIMIT),50) # Convert seed to Wasm and trace execution (debug mode with ASAN) @@ -50,7 +50,7 @@ trace-debug: exit 1; \ fi @echo "Converting seed to Wasm and tracing execution (debug mode with ASAN)..." - @cargo run -p spacewasm-fuzzing --bin seed_to_wasm $(CRASH) --stdout 2>/dev/null | \ + @cargo run -p spacewasm-fuzzing --bin seed_to_wasm -- $(if $(TARGET),--target $(TARGET)) $(CRASH) --stdout 2>/dev/null | \ RUSTFLAGS="-Zsanitizer=address" cargo +nightly run -p spacewasm_util --bin spacewasm-trace -- --stdin --limit $(or $(LIMIT),50) # Convert fuzzer seed to Wasm @@ -61,9 +61,9 @@ seed-to-wasm: exit 1; \ fi @if [ -n "$(OUT)" ]; then \ - cargo run --release -p spacewasm-fuzzing --bin seed_to_wasm -- $(CRASH) $(OUT); \ + cargo run --release -p spacewasm-fuzzing --bin seed_to_wasm -- $(if $(TARGET),--target $(TARGET)) $(CRASH) $(OUT); \ else \ - cargo run --release -p spacewasm-fuzzing --bin seed_to_wasm -- $(CRASH) $(CRASH).wasm; \ + cargo run --release -p spacewasm-fuzzing --bin seed_to_wasm -- $(if $(TARGET),--target $(TARGET)) $(CRASH) $(CRASH).wasm; \ fi # Trace Wasm file execution (release mode) diff --git a/crates/fuzzing/src/bin/seed_to_wasm.rs b/crates/fuzzing/src/bin/seed_to_wasm.rs index 89c73cb..61affd2 100644 --- a/crates/fuzzing/src/bin/seed_to_wasm.rs +++ b/crates/fuzzing/src/bin/seed_to_wasm.rs @@ -4,80 +4,102 @@ //! wasm-smith to generate valid Wasm. This tool converts seeds to Wasm //! for analysis with other tools (disassemblers, trace utilities, etc.). //! +//! The `no_traps` and `validate` fuzz targets configure wasm-smith +//! differently, so a seed must be decoded with the same generator that +//! produced it. Select it with `--target` (default: `no_traps`). +//! //! Usage: -//! seed_to_wasm [output.wasm] -//! seed_to_wasm --stdout +//! seed_to_wasm [--target no_traps|validate] [output.wasm] +//! seed_to_wasm [--target no_traps|validate] --stdout -use arbitrary::{Arbitrary, Unstructured}; -use spacewasm_fuzzing::generators::NoTrapsModule; +use spacewasm_fuzzing::generators::{wasm_from_seed, Target}; use std::env; use std::fs; use std::io::{self, Write}; use std::process; +fn usage(program: &str) -> ! { + eprintln!("Usage: {program} [--target no_traps|validate] [output.wasm]"); + eprintln!(" {program} [--target no_traps|validate] --stdout"); + eprintln!(); + eprintln!("Convert fuzzer seed artifacts to Wasm binaries."); + eprintln!(); + eprintln!("Options:"); + eprintln!( + " --target Generator to decode the seed with (default: no_traps)" + ); + eprintln!(); + eprintln!("Examples:"); + eprintln!(" {program} fuzz/artifacts/no_traps/crash-xxx output.wasm"); + eprintln!( + " {program} --target validate fuzz/artifacts/validate/crash-xxx --stdout | wasm2wat" + ); + process::exit(1); +} + fn main() { let args: Vec = env::args().collect(); + let program = args + .first() + .cloned() + .unwrap_or_else(|| "seed_to_wasm".to_string()); - if args.len() < 2 { - eprintln!("Usage: {} [output.wasm]", args[0]); - eprintln!(" {} --stdout", args[0]); - eprintln!(); - eprintln!("Convert fuzzer seed artifacts to Wasm binaries."); - eprintln!(); - eprintln!("Examples:"); - eprintln!(" # Write to file"); - eprintln!( - " {} fuzz/artifacts/no_traps/crash-xxx output.wasm", - args[0] - ); - eprintln!(); - eprintln!(" # Write to stdout"); - eprintln!( - " {} fuzz/artifacts/no_traps/crash-xxx --stdout | wasm2wat", - args[0] - ); - process::exit(1); + // Pull the optional `--target` flag out of the argument list; everything + // else stays positional so existing invocations keep working. + let mut target = Target::NoTraps; + let mut positional: Vec = Vec::new(); + let mut i = 1; + while i < args.len() { + if args[i] == "--target" { + target = match args.get(i + 1).map(String::as_str) { + Some("no_traps") => Target::NoTraps, + Some("validate") => Target::Validate, + other => { + eprintln!( + "Invalid --target '{}' (expected no_traps or validate)", + other.unwrap_or("") + ); + usage(&program); + } + }; + i += 2; + } else { + positional.push(args[i].clone()); + i += 1; + } } - let seed_file = &args[1]; - let seed_bytes = fs::read(seed_file).unwrap_or_else(|e| { - eprintln!("Failed to read seed file '{}': {}", seed_file, e); + let Some(seed_file) = positional.first().cloned() else { + usage(&program); + }; + + let seed_bytes = fs::read(&seed_file).unwrap_or_else(|e| { + eprintln!("Failed to read seed file '{seed_file}': {e}"); process::exit(1); }); - // Generate Wasm from seed - let mut unstructured = Unstructured::new(&seed_bytes); - let module = match NoTrapsModule::arbitrary(&mut unstructured) { - Ok(m) => m, - Err(e) => { - eprintln!("Failed to generate Wasm from seed: {:?}", e); - eprintln!("The seed may be too small or corrupted."); - process::exit(1); - } - }; - - let wasm_bytes = module.wasm(); + let wasm_bytes = wasm_from_seed(&seed_bytes, target).unwrap_or_else(|e| { + eprintln!("Failed to generate Wasm from seed: {e:?}"); + eprintln!("The seed may be too small or corrupted."); + process::exit(1); + }); - // Output handling - if args.len() > 2 && args[2] == "--stdout" { - // Write to stdout - io::stdout().write_all(wasm_bytes).unwrap_or_else(|e| { - eprintln!("Failed to write to stdout: {}", e); + // Output: `--stdout` or an optional file path (default: .wasm). + if positional.get(1).map(String::as_str) == Some("--stdout") { + io::stdout().write_all(&wasm_bytes).unwrap_or_else(|e| { + eprintln!("Failed to write to stdout: {e}"); process::exit(1); }); } else { - // Write to file - let output_file = if args.len() > 2 { - args[2].clone() - } else { - // Default: seed filename + .wasm - format!("{}.wasm", seed_file) - }; + let output_file = positional + .get(1) + .cloned() + .unwrap_or_else(|| format!("{seed_file}.wasm")); - eprintln!("Writing to: {}", output_file); + eprintln!("Writing to: {output_file}"); - fs::write(&output_file, wasm_bytes).unwrap_or_else(|e| { - eprintln!("Failed to write '{}': {}", output_file, e); + fs::write(&output_file, &wasm_bytes).unwrap_or_else(|e| { + eprintln!("Failed to write '{output_file}': {e}"); process::exit(1); }); diff --git a/crates/fuzzing/src/generators.rs b/crates/fuzzing/src/generators.rs index ff69827..d89947d 100644 --- a/crates/fuzzing/src/generators.rs +++ b/crates/fuzzing/src/generators.rs @@ -171,3 +171,57 @@ impl<'a> Arbitrary<'a> for NoTrapsModule { Self::new(u) } } + +/// Which fuzz target's generator produced a seed. +/// +/// The `no_traps` and `validate` targets configure wasm-smith differently +/// ([`NoTrapsModule`] sets `disallow_traps`), so they consume input bytes +/// differently and produce different modules from the same seed. A seed must be +/// decoded with the generator that produced it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Target { + /// The `no_traps` target, which uses [`NoTrapsModule`]. + NoTraps, + /// The `validate` target, which uses [`FuzzModule`]. + Validate, +} + +/// Generate the Wasm module a fuzz `target` would produce from `seed`. +/// +/// This reproduces what the corresponding fuzz target feeds to its oracle, so a +/// crash artifact can be decoded back into the exact module that triggered it. +pub fn wasm_from_seed(seed: &[u8], target: Target) -> Result> { + let mut u = Unstructured::new(seed); + match target { + Target::NoTraps => NoTrapsModule::new(&mut u).map(|m| m.wasm), + Target::Validate => FuzzModule::new(&mut u).map(|m| m.wasm), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // A fixed seed with enough bytes for wasm-smith to build a non-trivial module. + fn seed() -> Vec { + (0..=u8::MAX).collect() + } + + #[test] + fn targets_produce_different_wasm_from_same_seed() { + // The two generators configure wasm-smith differently, so the same seed + // yields different modules -- which is why seed_to_wasm must decode with + // the generator that produced the seed. + let no_traps = wasm_from_seed(&seed(), Target::NoTraps).unwrap(); + let validate = wasm_from_seed(&seed(), Target::Validate).unwrap(); + assert_ne!(no_traps, validate); + } + + #[test] + fn wasm_from_seed_is_deterministic() { + assert_eq!( + wasm_from_seed(&seed(), Target::Validate).unwrap(), + wasm_from_seed(&seed(), Target::Validate).unwrap(), + ); + } +}