Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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)
Expand Down
128 changes: 75 additions & 53 deletions crates/fuzzing/src/bin/seed_to_wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <seed-file> [output.wasm]
//! seed_to_wasm <seed-file> --stdout
//! seed_to_wasm [--target no_traps|validate] <seed-file> [output.wasm]
//! seed_to_wasm [--target no_traps|validate] <seed-file> --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] <seed-file> [output.wasm]");
eprintln!(" {program} [--target no_traps|validate] <seed-file> --stdout");
eprintln!();
eprintln!("Convert fuzzer seed artifacts to Wasm binaries.");
eprintln!();
eprintln!("Options:");
eprintln!(
" --target <no_traps|validate> 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<String> = env::args().collect();
let program = args
.first()
.cloned()
.unwrap_or_else(|| "seed_to_wasm".to_string());

if args.len() < 2 {
eprintln!("Usage: {} <seed-file> [output.wasm]", args[0]);
eprintln!(" {} <seed-file> --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<String> = 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: <seed>.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);
});

Expand Down
54 changes: 54 additions & 0 deletions crates/fuzzing/src/generators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<u8>> {
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<u8> {
(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(),
);
}
}
Loading