diff --git a/Cargo.toml b/Cargo.toml index 53b4af2..c0df7de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 9c4b632..45632cf 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -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 diff --git a/fuzz/fuzz_targets/differential.rs b/fuzz/fuzz_targets/differential.rs index 344a03d..b72af67 100644 --- a/fuzz/fuzz_targets/differential.rs +++ b/fuzz/fuzz_targets/differential.rs @@ -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| { + 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 { - 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| { - 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 = a_globals - .into_iter() - .map(|g| g.get(&mut orig_store)) - .collect(); - let a_mems: Vec = 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 = b_globals - .into_iter() - .map(|g| g.get(&mut roundtrip_store)) - .collect(); - let b_mems: Vec = 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 = a_globals + .into_iter() + .map(|g| g.get(&mut orig_store)) + .collect(); + let a_mems: Vec = 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 = b_globals + .into_iter() + .map(|g| g.get(&mut roundtrip_store)) + .collect(); + let b_mems: Vec = 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); diff --git a/fuzz/fuzz_targets/opt_diff.rs b/fuzz/fuzz_targets/opt_diff.rs new file mode 100644 index 0000000..e6146f8 --- /dev/null +++ b/fuzz/fuzz_targets/opt_diff.rs @@ -0,0 +1,56 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; + +use waffle::{FrontendOptions, InterpContext, InterpResult, Module}; + +fuzz_target!( + |module: wasm_smith::ConfiguredModule| { + 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); + } +); diff --git a/src/entity.rs b/src/entity.rs index fb03fca..565b56d 100644 --- a/src/entity.rs +++ b/src/entity.rs @@ -171,3 +171,10 @@ impl IndexMut for PerEntity PartialEq for PerEntity { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} +impl Eq for PerEntity {} diff --git a/src/interp.rs b/src/interp.rs index b04f056..1e457f7 100644 --- a/src/interp.rs +++ b/src/interp.rs @@ -11,9 +11,10 @@ mod wasi; #[derive(Debug, Clone)] pub struct InterpContext { - memories: PerEntity, - tables: PerEntity, - globals: PerEntity, + pub memories: PerEntity, + pub tables: PerEntity, + pub globals: PerEntity, + 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, - max_pages: usize, + pub data: Vec, + pub max_pages: usize, } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct InterpTable { - elements: Vec, + pub elements: Vec, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] diff --git a/src/lib.rs b/src/lib.rs index cfa9446..c527f4c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,3 +22,6 @@ pub use ops::{Ieee32, Ieee64, Operator}; mod interp; pub use interp::*; + +#[cfg(feature = "fuzzing")] +pub mod fuzzing;