Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve debug output of fuzzers #1344

Merged
merged 6 commits into from
Jan 24, 2025
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
2 changes: 2 additions & 0 deletions crates/fuzz/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod config;
mod crash_inputs;
mod error;
mod module;
#[cfg(feature = "differential")]
pub mod oracle;
mod value;
Expand All @@ -9,5 +10,6 @@ pub use self::{
config::{FuzzSmithConfig, FuzzWasmiConfig},
crash_inputs::generate_crash_inputs,
error::{FuzzError, TrapCode},
module::{FuzzModule, WasmSource, WatSource},
value::{FuzzVal, FuzzValType},
};
87 changes: 87 additions & 0 deletions crates/fuzz/src/module.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use arbitrary::Unstructured;
use std::fmt::{self, Debug};

/// A Wasm module fuzz input.
pub struct FuzzModule {
module: wasm_smith::Module,
}

impl FuzzModule {
/// Creates a new [`FuzzModule`] from the given `config` and fuzz input bytes, `u`.
pub fn new(
config: impl Into<wasm_smith::Config>,
u: &mut Unstructured,
) -> arbitrary::Result<Self> {
let config = config.into();
let module = wasm_smith::Module::new(config, u)?;
Ok(Self { module })
}

/// Ensure that all of this Wasm module’s functions will terminate when executed.
///
/// Read more about this API [here](wasm_smith::Module::ensure_termination).
pub fn ensure_termination(&mut self, default_fuel: u32) {
if let Err(err) = self.module.ensure_termination(default_fuel) {
panic!("unexpected invalid Wasm module: {err}")
}
}

/// Returns the machine readble [`WasmSource`] code.
pub fn wasm(&self) -> WasmSource {
WasmSource {
bytes: self.module.to_bytes(),
}
}
}

impl Debug for FuzzModule {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let config = self.module.config();
let wat = self.wasm().to_wat();
f.debug_struct("FuzzModule")
.field("config", config)
.field("wat", &wat)
.finish()
}
}

/// A `.wasm` source code.
pub struct WasmSource {
bytes: Vec<u8>,
}

impl WasmSource {
/// Consumes `self` and returns the underlying bytes of the [`WasmSource`].
pub fn into_bytes(self) -> Box<[u8]> {
self.bytes.into()
}

/// Returns the underlying bytes of the [`WasmSource`].
pub fn as_bytes(&self) -> &[u8] {
&self.bytes[..]
}

/// Converts the [`WasmSource`] to human readable `.wat` formatted source.
///
/// The returned [`WatSource`] is convenience for debugging.
pub fn to_wat(&self) -> WatSource {
let wat = match wasmprinter::print_bytes(&self.bytes[..]) {
Ok(wat) => wat,
Err(err) => panic!("invalid Wasm: {err}"),
};
WatSource { text: wat }
}
}

/// A `.wat` source code.
///
/// Convenience type for debug printing `.wat` formatted Wasm source code.
pub struct WatSource {
text: String,
}

impl fmt::Debug for WatSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "\n{}", self.text)
}
}
38 changes: 7 additions & 31 deletions fuzz/fuzz_targets/differential.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
#![no_main]

use std::fmt;

use arbitrary::{Arbitrary, Unstructured};
use libfuzzer_sys::fuzz_target;
use wasmi::Val;
use wasmi_fuzz::{
config::FuzzSmithConfig,
oracle::{ChosenOracle, DifferentialOracle, DifferentialOracleMeta, WasmiOracle},
FuzzModule,
FuzzVal,
};

/// Fuzzing input for differential fuzzing.
#[derive(Debug)]
pub struct FuzzInput {
/// The chosen Wasm runtime oracle to compare against Wasmi.
chosen_oracle: ChosenOracle,
/// The fuzzed Wasm module and its configuration.
smith_module: wasm_smith::Module,
module: FuzzModule,
}

impl<'a> Arbitrary<'a> for FuzzInput {
Expand All @@ -28,43 +28,19 @@ impl<'a> Arbitrary<'a> for FuzzInput {
WasmiOracle::configure(&mut fuzz_config);
chosen_oracle.configure(&mut fuzz_config);
let smith_config: wasm_smith::Config = fuzz_config.into();
let mut smith_module = wasm_smith::Module::new(smith_config, u)?;
smith_module.ensure_termination(1_000 /* fuel */)
.expect("`ensure_termination` can only fail for modules that have not been created by `wasm_smith`");
let mut smith_module = FuzzModule::new(smith_config, u)?;
smith_module.ensure_termination(1_000 /* fuel */);
Ok(Self {
chosen_oracle,
smith_module,
module: smith_module,
})
}
}

impl fmt::Debug for FuzzInput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
/// Prints the internal `String` as `Display` on `Debug` output.
pub struct Text(String);
impl fmt::Debug for Text {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "\n{}", self.0.as_str())
}
}

let wasm = self.smith_module.to_bytes();
let wat = match wasmprinter::print_bytes(wasm) {
Ok(wat) => wat,
Err(err) => format!("invalid Wasm: {err}"),
};
f.debug_struct("FuzzInput")
.field("chosen_oracle", &self.chosen_oracle)
.field("module", &self.smith_module)
.field("wasm", &Text(wat))
.finish()
}
}

impl FuzzInput {
/// Returns the fuzzed Wasm input bytes.
pub fn wasm(&self) -> Box<[u8]> {
self.smith_module.to_bytes().into()
self.module.wasm().into_bytes()
}
}

Expand Down
69 changes: 49 additions & 20 deletions fuzz/fuzz_targets/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,61 @@ use wasmi::{
StoreLimitsBuilder,
Val,
};
use wasmi_fuzz::{config::ValidationMode, FuzzVal, FuzzValType, FuzzWasmiConfig};
use wasmi_fuzz::{
config::ValidationMode,
FuzzModule,
FuzzSmithConfig,
FuzzVal,
FuzzValType,
FuzzWasmiConfig,
};

fuzz_target!(|seed: &[u8]| {
let mut u = Unstructured::new(seed);
let Ok(wasmi_config) = FuzzWasmiConfig::arbitrary(&mut u) else {
return;
};
let Ok(mut fuzz_config) = wasmi_fuzz::FuzzSmithConfig::arbitrary(&mut u) else {
return;
};
fuzz_config.export_everything();
let Ok(smith_module) = wasm_smith::Module::new(fuzz_config.into(), &mut u) else {
return;
};
let wasm_bytes = smith_module.to_bytes();
let wasm = wasm_bytes.as_slice();
#[derive(Debug)]
pub struct FuzzInput<'a> {
config: FuzzWasmiConfig,
module: FuzzModule,
u: Unstructured<'a>,
}

impl<'a> Arbitrary<'a> for FuzzInput<'a> {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
let config = FuzzWasmiConfig::arbitrary(u)?;
let mut fuzz_config = FuzzSmithConfig::arbitrary(u)?;
fuzz_config.export_everything();
let module = FuzzModule::new(fuzz_config, u)?;
Ok(Self {
config,
module,
u: Unstructured::new(&[]),
})
}

fn arbitrary_take_rest(mut u: Unstructured<'a>) -> arbitrary::Result<Self> {
Self::arbitrary(&mut u).map(|mut input| {
input.u = u;
input
})
}
}

fuzz_target!(|input: FuzzInput| {
let FuzzInput {
config,
module,
mut u,
} = input;
let wasm_bytes = module.wasm().into_bytes();
let wasm = &wasm_bytes[..];

let config = {
let mut config = Config::from(wasmi_config);
let engine_config = {
let mut config = Config::from(config);
// We use Wasmi's built-in fuel metering since it is way faster
// than `wasm_smith`'s fuel metering and thus allows the fuzzer
// to expand its test coverage faster.
config.consume_fuel(true);
config
};
let engine = Engine::new(&config);
let engine = Engine::new(&engine_config);
let linker = Linker::new(&engine);
let limiter = StoreLimitsBuilder::new()
.memory_size(1000 * 0x10000)
Expand All @@ -48,15 +77,15 @@ fuzz_target!(|seed: &[u8]| {
let Ok(_) = store.set_fuel(1000) else {
return;
};
if matches!(wasmi_config.validation_mode, ValidationMode::Unchecked) {
if matches!(config.validation_mode, ValidationMode::Unchecked) {
// We validate the Wasm module before handing it over to Wasmi
// despite `wasm_smith` stating to only produce valid Wasm.
// Translating an invalid Wasm module is undefined behavior.
if Module::validate(&engine, wasm).is_err() {
return;
}
}
let status = match wasmi_config.validation_mode {
let status = match config.validation_mode {
ValidationMode::Checked => Module::new(&engine, wasm),
ValidationMode::Unchecked => {
// Safety: we have just checked Wasm validity above.
Expand Down
41 changes: 24 additions & 17 deletions fuzz/fuzz_targets/translate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,40 @@ use libfuzzer_sys::fuzz_target;
use wasmi::{Config, Engine, Module};
use wasmi_fuzz::{
config::{ParsingMode, ValidationMode},
FuzzModule,
FuzzWasmiConfig,
};

fuzz_target!(|seed: &[u8]| {
let mut u = Unstructured::new(seed);
let Ok(wasmi_config) = FuzzWasmiConfig::arbitrary(&mut u) else {
return;
};
let Ok(fuzz_config) = wasmi_fuzz::FuzzSmithConfig::arbitrary(&mut u) else {
return;
};
let Ok(smith_module) = wasm_smith::Module::new(fuzz_config.into(), &mut u) else {
return;
};
let wasm_bytes = smith_module.to_bytes();
let wasm = wasm_bytes.as_slice();
let config = Config::from(wasmi_config);
let engine = Engine::new(&config);
if matches!(wasmi_config.validation_mode, ValidationMode::Unchecked) {
#[derive(Debug)]
pub struct FuzzInput {
config: FuzzWasmiConfig,
module: FuzzModule,
}

impl<'a> Arbitrary<'a> for FuzzInput {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
let config = FuzzWasmiConfig::arbitrary(u)?;
let fuzz_config = wasmi_fuzz::FuzzSmithConfig::arbitrary(u)?;
let module = wasmi_fuzz::FuzzModule::new(fuzz_config, u)?;
Ok(Self { config, module })
}
}

fuzz_target!(|input: FuzzInput| {
let FuzzInput { config, module } = input;
let wasm_source = module.wasm();
let wasm = wasm_source.as_bytes();
let engine_config = Config::from(config);
let engine = Engine::new(&engine_config);
if matches!(config.validation_mode, ValidationMode::Unchecked) {
// We validate the Wasm module before handing it over to Wasmi
// despite `wasm_smith` stating to only produce valid Wasm.
// Translating an invalid Wasm module is undefined behavior.
if Module::validate(&engine, wasm).is_err() {
return;
}
}
let status = match (wasmi_config.parsing_mode, wasmi_config.validation_mode) {
let status = match (config.parsing_mode, config.validation_mode) {
(ParsingMode::Streaming, ValidationMode::Checked) => Module::new_streaming(&engine, wasm),
(ParsingMode::Buffered, ValidationMode::Checked) => Module::new(&engine, wasm),
(ParsingMode::Streaming, ValidationMode::Unchecked) => {
Expand Down
Loading