Fuzzer: differential comparison of opt/no-opt

This commit is contained in:
Chris Fallin 2023-02-24 21:24:09 -08:00
parent 573667f37b
commit 2ff4d80286
7 changed files with 196 additions and 204 deletions

View file

@ -19,3 +19,11 @@ rayon = "1.5"
lazy_static = "1.4"
libc = "0.2"
addr2line = "0.19"
# For fuzzing only. Versions must match those in fuzz/Cargo.toml.
libfuzzer-sys = { version = "0.4", optional = true }
wasm-smith = { version = "0.8", optional = true }
[features]
default = []
fuzzing = ["libfuzzer-sys", "wasm-smith"]

View file

@ -19,6 +19,7 @@ wasmtime = "3.0"
[dependencies.waffle]
path = ".."
features = ["fuzzing"]
# Prevent this from interfering with workspaces
[workspace]
@ -47,3 +48,9 @@ name = "differential"
path = "fuzz_targets/differential.rs"
test = false
doc = false
[[bin]]
name = "opt_diff"
path = "fuzz_targets/opt_diff.rs"
test = false
doc = false

View file

@ -1,214 +1,117 @@
#![no_main]
use libfuzzer_sys::{arbitrary, fuzz_target};
use libfuzzer_sys::fuzz_target;
use std::sync::atomic::{AtomicU64, Ordering};
use waffle::{FrontendOptions, Module};
fn reject(bytes: &[u8]) -> bool {
let parser = wasmparser::Parser::new(0);
let mut has_start = false;
let mut has_global_set = false;
let mut num_globals = 0;
for payload in parser.parse_all(bytes) {
match payload.unwrap() {
wasmparser::Payload::CodeSectionEntry(body) => {
for op in body.get_operators_reader().unwrap() {
let op = op.unwrap();
match op {
wasmparser::Operator::GlobalSet { .. } => {
has_global_set = true;
}
_ => {}
}
}
}
wasmparser::Payload::StartSection { .. } => {
has_start = true;
}
wasmparser::Payload::ExportSection(mut reader) => {
for _ in 0..reader.get_count() {
let e = reader.read().unwrap();
match &e.kind {
&wasmparser::ExternalKind::Global => {
num_globals += 1;
}
_ => {}
}
}
}
_ => {}
}
}
fuzz_target!(
|module: wasm_smith::ConfiguredModule<waffle::fuzzing::Config>| {
let _ = env_logger::try_init();
log::debug!("original module: {:?}", module.module);
if !has_start || !has_global_set || num_globals < 1 {
return true;
}
let orig_bytes = module.module.to_bytes();
false
}
#[derive(Debug)]
struct Config;
impl<'a> arbitrary::Arbitrary<'a> for Config {
fn arbitrary(_u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
Ok(Config)
}
}
impl wasm_smith::Config for Config {
fn min_funcs(&self) -> usize {
1
}
fn max_funcs(&self) -> usize {
1
}
fn min_memories(&self) -> u32 {
1
}
fn max_memories(&self) -> usize {
1
}
fn min_globals(&self) -> usize {
10
}
fn max_globals(&self) -> usize {
10
}
fn min_tables(&self) -> u32 {
0
}
fn max_tables(&self) -> usize {
0
}
fn min_imports(&self) -> usize {
0
}
fn max_imports(&self) -> usize {
0
}
fn min_exports(&self) -> usize {
12
}
fn max_exports(&self) -> usize {
12
}
fn allow_start_export(&self) -> bool {
true
}
fn canonicalize_nans(&self) -> bool {
true
}
fn max_memory_pages(&self, _is_64: bool) -> u64 {
1
}
}
fuzz_target!(|module: wasm_smith::ConfiguredModule<Config>| {
let _ = env_logger::try_init();
log::debug!("original module: {:?}", module.module);
let orig_bytes = module.module.to_bytes();
if reject(&orig_bytes[..]) {
log::debug!("Discarding fuzz run. Body:\n{:?}", module);
return;
} else {
log::info!("body: {:?}", module);
}
let mut config = wasmtime::Config::default();
config.consume_fuel(true);
let engine = wasmtime::Engine::new(&config).unwrap();
let orig_module =
wasmtime::Module::new(&engine, &orig_bytes[..]).expect("failed to parse original wasm");
let mut orig_store = wasmtime::Store::new(&engine, ());
orig_store.out_of_fuel_trap();
orig_store.add_fuel(10000).unwrap();
let orig_instance = wasmtime::Instance::new(&mut orig_store, &orig_module, &[]);
let orig_instance = match orig_instance {
Ok(orig_instance) => orig_instance,
Err(e) => {
log::info!("cannot run start on orig intsance ({:?}); discarding", e);
if waffle::fuzzing::reject(&orig_bytes[..]) {
log::debug!("Discarding fuzz run. Body:\n{:?}", module);
return;
} else {
log::info!("body: {:?}", module);
}
};
let mut parsed_module =
Module::from_wasm_bytes(&orig_bytes[..], &FrontendOptions::default()).unwrap();
parsed_module.expand_all_funcs().unwrap();
parsed_module.per_func_body(|body| body.optimize());
let roundtrip_bytes = parsed_module.to_wasm_bytes().unwrap();
let mut config = wasmtime::Config::default();
config.consume_fuel(true);
let engine = wasmtime::Engine::new(&config).unwrap();
let orig_module =
wasmtime::Module::new(&engine, &orig_bytes[..]).expect("failed to parse original wasm");
let mut orig_store = wasmtime::Store::new(&engine, ());
orig_store.out_of_fuel_trap();
orig_store.add_fuel(10000).unwrap();
let orig_instance = wasmtime::Instance::new(&mut orig_store, &orig_module, &[]);
let orig_instance = match orig_instance {
Ok(orig_instance) => orig_instance,
Err(e) => {
log::info!("cannot run start on orig intsance ({:?}); discarding", e);
return;
}
};
if let Ok(filename) = std::env::var("FUZZ_DUMP_WASM") {
std::fs::write(format!("{}_orig.wasm", filename), &orig_bytes[..]).unwrap();
std::fs::write(format!("{}_roundtrip.wasm", filename), &roundtrip_bytes[..]).unwrap();
}
let mut parsed_module =
Module::from_wasm_bytes(&orig_bytes[..], &FrontendOptions::default()).unwrap();
parsed_module.expand_all_funcs().unwrap();
parsed_module.per_func_body(|body| body.optimize());
let roundtrip_bytes = parsed_module.to_wasm_bytes().unwrap();
let total = TOTAL.fetch_add(1, Ordering::Relaxed);
let roundtrip_module = wasmtime::Module::new(&engine, &roundtrip_bytes[..])
.expect("failed to parse roundtripped wasm");
let mut roundtrip_store = wasmtime::Store::new(&engine, ());
roundtrip_store.out_of_fuel_trap();
// After roundtrip, fuel consumption rate may differ. That's fine;
// what matters is that it terminated above without a trap (hence
// halts in a reasonable time).
roundtrip_store.add_fuel(u64::MAX).unwrap();
let roundtrip_instance = wasmtime::Instance::new(&mut roundtrip_store, &roundtrip_module, &[])
.expect("cannot instantiate roundtripped wasm");
// Ensure exports are equal.
let a_globals: Vec<_> = orig_instance
.exports(&mut orig_store)
.filter_map(|e| e.into_global())
.collect();
let a_globals: Vec<wasmtime::Val> = a_globals
.into_iter()
.map(|g| g.get(&mut orig_store))
.collect();
let a_mems: Vec<wasmtime::Memory> = orig_instance
.exports(&mut orig_store)
.filter_map(|e| e.into_memory())
.collect();
let b_globals: Vec<_> = roundtrip_instance
.exports(&mut roundtrip_store)
.filter_map(|e| e.into_global())
.collect();
let b_globals: Vec<wasmtime::Val> = b_globals
.into_iter()
.map(|g| g.get(&mut roundtrip_store))
.collect();
let b_mems: Vec<wasmtime::Memory> = roundtrip_instance
.exports(&mut roundtrip_store)
.filter_map(|e| e.into_memory())
.collect();
log::info!("a_globals = {:?}", a_globals);
log::info!("b_globals = {:?}", b_globals);
assert_eq!(a_globals.len(), b_globals.len());
for (a, b) in a_globals.into_iter().zip(b_globals.into_iter()) {
match (a, b) {
(wasmtime::Val::I32(a), wasmtime::Val::I32(b)) => assert_eq!(a, b),
(wasmtime::Val::I64(a), wasmtime::Val::I64(b)) => assert_eq!(a, b),
(wasmtime::Val::F32(a), wasmtime::Val::F32(b)) => assert_eq!(a, b),
(wasmtime::Val::F64(a), wasmtime::Val::F64(b)) => assert_eq!(a, b),
_ => panic!("mismatching types"),
if let Ok(filename) = std::env::var("FUZZ_DUMP_WASM") {
std::fs::write(format!("{}_orig.wasm", filename), &orig_bytes[..]).unwrap();
std::fs::write(format!("{}_roundtrip.wasm", filename), &roundtrip_bytes[..]).unwrap();
}
}
assert_eq!(a_mems.len(), b_mems.len());
for (a, b) in a_mems.into_iter().zip(b_mems.into_iter()) {
let a_data = a.data(&orig_store);
let b_data = b.data(&roundtrip_store);
assert_eq!(a_data, b_data);
}
let total = TOTAL.fetch_add(1, Ordering::Relaxed);
success(total);
});
let roundtrip_module = wasmtime::Module::new(&engine, &roundtrip_bytes[..])
.expect("failed to parse roundtripped wasm");
let mut roundtrip_store = wasmtime::Store::new(&engine, ());
roundtrip_store.out_of_fuel_trap();
// After roundtrip, fuel consumption rate may differ. That's fine;
// what matters is that it terminated above without a trap (hence
// halts in a reasonable time).
roundtrip_store.add_fuel(u64::MAX).unwrap();
let roundtrip_instance =
wasmtime::Instance::new(&mut roundtrip_store, &roundtrip_module, &[])
.expect("cannot instantiate roundtripped wasm");
// Ensure exports are equal.
let a_globals: Vec<_> = orig_instance
.exports(&mut orig_store)
.filter_map(|e| e.into_global())
.collect();
let a_globals: Vec<wasmtime::Val> = a_globals
.into_iter()
.map(|g| g.get(&mut orig_store))
.collect();
let a_mems: Vec<wasmtime::Memory> = orig_instance
.exports(&mut orig_store)
.filter_map(|e| e.into_memory())
.collect();
let b_globals: Vec<_> = roundtrip_instance
.exports(&mut roundtrip_store)
.filter_map(|e| e.into_global())
.collect();
let b_globals: Vec<wasmtime::Val> = b_globals
.into_iter()
.map(|g| g.get(&mut roundtrip_store))
.collect();
let b_mems: Vec<wasmtime::Memory> = roundtrip_instance
.exports(&mut roundtrip_store)
.filter_map(|e| e.into_memory())
.collect();
log::info!("a_globals = {:?}", a_globals);
log::info!("b_globals = {:?}", b_globals);
assert_eq!(a_globals.len(), b_globals.len());
for (a, b) in a_globals.into_iter().zip(b_globals.into_iter()) {
match (a, b) {
(wasmtime::Val::I32(a), wasmtime::Val::I32(b)) => assert_eq!(a, b),
(wasmtime::Val::I64(a), wasmtime::Val::I64(b)) => assert_eq!(a, b),
(wasmtime::Val::F32(a), wasmtime::Val::F32(b)) => assert_eq!(a, b),
(wasmtime::Val::F64(a), wasmtime::Val::F64(b)) => assert_eq!(a, b),
_ => panic!("mismatching types"),
}
}
assert_eq!(a_mems.len(), b_mems.len());
for (a, b) in a_mems.into_iter().zip(b_mems.into_iter()) {
let a_data = a.data(&orig_store);
let b_data = b.data(&roundtrip_store);
assert_eq!(a_data, b_data);
}
success(total);
}
);
static TOTAL: AtomicU64 = AtomicU64::new(0);
static SUCCESS: AtomicU64 = AtomicU64::new(0);

View file

@ -0,0 +1,56 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use waffle::{FrontendOptions, InterpContext, InterpResult, Module};
fuzz_target!(
|module: wasm_smith::ConfiguredModule<waffle::fuzzing::Config>| {
let _ = env_logger::try_init();
log::debug!("original module: {:?}", module.module);
let orig_bytes = module.module.to_bytes();
if waffle::fuzzing::reject(&orig_bytes[..]) {
log::debug!("Discarding fuzz run. Body:\n{:?}", module);
return;
} else {
log::info!("body: {:?}", module);
}
let mut parsed_module =
Module::from_wasm_bytes(&orig_bytes[..], &FrontendOptions::default()).unwrap();
parsed_module.expand_all_funcs().unwrap();
let start = parsed_module.start_func.unwrap();
let mut orig_ctx = InterpContext::new(&parsed_module);
orig_ctx.fuel = 10000;
match orig_ctx.call(&parsed_module, start, &[]) {
InterpResult::OutOfFuel => {
// Silently reject.
log::trace!("Rejecting due to timeout in orig");
return;
}
InterpResult::Ok(_) => {}
ret => panic!("Bad result: {:?}", ret),
}
let mut opt_module = parsed_module.clone();
opt_module.per_func_body(|body| body.optimize());
let mut opt_ctx = InterpContext::new(&opt_module);
// Allow a little leeway for opts to not actually optimize.
opt_ctx.fuel = 20000;
opt_ctx.call(&opt_module, start, &[]).ok().unwrap();
log::trace!(
"Orig ran in {} fuel; opt ran in {} fuel",
10000 - orig_ctx.fuel,
20000 - opt_ctx.fuel
);
assert_eq!(orig_ctx.memories, opt_ctx.memories);
assert_eq!(orig_ctx.globals, opt_ctx.globals);
}
);

View file

@ -171,3 +171,10 @@ impl<Idx: EntityRef, T: Clone + Debug + Default> IndexMut<Idx> for PerEntity<Idx
&mut self.0[idx.index()]
}
}
impl<Idx: EntityRef, T: Clone + Debug + Default + PartialEq> PartialEq for PerEntity<Idx, T> {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl<Idx: EntityRef, T: Clone + Debug + Default + PartialEq + Eq> Eq for PerEntity<Idx, T> {}

View file

@ -11,9 +11,10 @@ mod wasi;
#[derive(Debug, Clone)]
pub struct InterpContext {
memories: PerEntity<Memory, InterpMemory>,
tables: PerEntity<Table, InterpTable>,
globals: PerEntity<Global, ConstVal>,
pub memories: PerEntity<Memory, InterpMemory>,
pub tables: PerEntity<Table, InterpTable>,
pub globals: PerEntity<Global, ConstVal>,
pub fuel: u64,
}
type MultiVal = SmallVec<[ConstVal; 2]>;
@ -23,6 +24,7 @@ pub enum InterpResult {
Ok(MultiVal),
Exit,
Trap,
OutOfFuel,
}
impl InterpResult {
@ -72,6 +74,7 @@ impl InterpContext {
memories,
tables,
globals,
fuel: u64::MAX,
}
}
@ -105,6 +108,11 @@ impl InterpContext {
}
loop {
self.fuel -= 1;
if self.fuel == 0 {
return InterpResult::OutOfFuel;
}
log::trace!("Interpreting block {}", frame.cur_block);
for &inst in &body.blocks[frame.cur_block].insts {
log::trace!("Evaluating inst {}", inst);
@ -290,15 +298,15 @@ impl InterpStackFrame {
}
}
#[derive(Debug, Clone, Default)]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct InterpMemory {
data: Vec<u8>,
max_pages: usize,
pub data: Vec<u8>,
pub max_pages: usize,
}
#[derive(Debug, Clone, Default)]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct InterpTable {
elements: Vec<Func>,
pub elements: Vec<Func>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]

View file

@ -22,3 +22,6 @@ pub use ops::{Ieee32, Ieee64, Operator};
mod interp;
pub use interp::*;
#[cfg(feature = "fuzzing")]
pub mod fuzzing;