Add a fuzz mode to stress unaligned wasm addresses (#3516)
Alignment on all memory instructions in wasm is currently best-effort and not actually required, meaning that whatever wasm actually uses as an address should work regardless of whether the address is aligned or not. This is theoretically tested in the fuzzers via wasm-smith-generated code, but wasm-smith doesn't today have too too high of a chance of generating an actual successful load/store. This commit adds a new configuration option to the `Config` generator for fuzzing which forces usage of a custom linear memory implementation which is backed by Rust's `Vec<u8>` and forces the base address of linear memory to be off-by-one relative to the base address of the `Vec<u8>` itself. This should theoretically force host addresses to almost always be unaligned, even if wasm addresses are otherwise aligned. The main interesting fuzz coverage here is likely to be in the existing `differential` target which compares running the same module in wasmtime with two different `Config` values to ensure the same results are produced. This probably won't increase coverage all that much in the near future due to wasm-smith rarely generating successful loads/stores, but in the meantime by hooking this up into `Config` it also means that we'll be running in comparison against v8 and also ensuring that all spec tests succeed if misalignment is forced at the hardware level. As a side effect this commit also cleans up the fuzzers slightly: * The `DifferentialConfig` struct is removed and folded into `Config` * The `init_hang_limit` processing is removed since we don't use `-ttf`-generated modules from binaryen any more. * Traps are now asserted to have the same trap code, otherwise differential fuzzing fails. * Some more debug logging was added to the differential fuzzer
This commit is contained in:
@@ -12,29 +12,10 @@ pub mod api;
|
||||
|
||||
pub mod table_ops;
|
||||
|
||||
use anyhow::Result;
|
||||
use arbitrary::{Arbitrary, Unstructured};
|
||||
|
||||
/// A description of configuration options that we should do differential
|
||||
/// testing between.
|
||||
#[derive(Arbitrary, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct DifferentialConfig {
|
||||
opt_level: OptLevel,
|
||||
force_jump_veneers: bool,
|
||||
}
|
||||
|
||||
impl DifferentialConfig {
|
||||
/// Convert this differential fuzzing config into a `wasmtime::Config`.
|
||||
pub fn to_wasmtime_config(&self) -> anyhow::Result<wasmtime::Config> {
|
||||
let mut config = crate::fuzz_default_config(wasmtime::Strategy::Cranelift)?;
|
||||
config.cranelift_opt_level(self.opt_level.to_wasmtime());
|
||||
if self.force_jump_veneers {
|
||||
unsafe {
|
||||
config.cranelift_flag_set("wasmtime_linkopt_force_jump_veneer", "true")?;
|
||||
}
|
||||
}
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
use std::sync::Arc;
|
||||
use wasmtime::{LinearMemory, MemoryCreator, MemoryType};
|
||||
|
||||
#[derive(Arbitrary, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
enum OptLevel {
|
||||
@@ -54,7 +35,7 @@ impl OptLevel {
|
||||
}
|
||||
|
||||
/// Implementation of generating a `wasmtime::Config` arbitrarily
|
||||
#[derive(Arbitrary, Debug)]
|
||||
#[derive(Arbitrary, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct Config {
|
||||
opt_level: OptLevel,
|
||||
debug_info: bool,
|
||||
@@ -62,13 +43,29 @@ pub struct Config {
|
||||
interruptable: bool,
|
||||
#[allow(missing_docs)]
|
||||
pub consume_fuel: bool,
|
||||
memory_config: MemoryConfig,
|
||||
force_jump_veneers: bool,
|
||||
}
|
||||
|
||||
// Note that we use 32-bit values here to avoid blowing the 64-bit address
|
||||
// space by requesting ungodly-large sizes/guards.
|
||||
#[derive(Arbitrary, Debug, Eq, Hash, PartialEq)]
|
||||
enum MemoryConfig {
|
||||
/// Configuration for linear memories which correspond to normal
|
||||
/// configuration settings in `wasmtime` itself. This will tweak various
|
||||
/// parameters about static/dynamic memories.
|
||||
///
|
||||
/// Note that we use 32-bit values here to avoid blowing the 64-bit address
|
||||
/// space by requesting ungodly-large sizes/guards.
|
||||
Normal {
|
||||
static_memory_maximum_size: Option<u32>,
|
||||
static_memory_guard_size: Option<u32>,
|
||||
dynamic_memory_guard_size: Option<u32>,
|
||||
guard_before_linear_memory: bool,
|
||||
},
|
||||
|
||||
/// Configuration to force use of a linear memory that's unaligned at its
|
||||
/// base address to force all wasm addresses to be unaligned at the hardware
|
||||
/// level, even if the wasm itself correctly aligns everything internally.
|
||||
CustomUnaligned,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -76,18 +73,102 @@ impl Config {
|
||||
pub fn to_wasmtime(&self) -> wasmtime::Config {
|
||||
let mut cfg = crate::fuzz_default_config(wasmtime::Strategy::Auto).unwrap();
|
||||
cfg.debug_info(self.debug_info)
|
||||
.static_memory_maximum_size(self.static_memory_maximum_size.unwrap_or(0).into())
|
||||
.static_memory_guard_size(self.static_memory_guard_size.unwrap_or(0).into())
|
||||
.dynamic_memory_guard_size(self.dynamic_memory_guard_size.unwrap_or(0).into())
|
||||
.guard_before_linear_memory(self.guard_before_linear_memory)
|
||||
.cranelift_nan_canonicalization(self.canonicalize_nans)
|
||||
.cranelift_opt_level(self.opt_level.to_wasmtime())
|
||||
.interruptable(self.interruptable)
|
||||
.consume_fuel(self.consume_fuel);
|
||||
|
||||
if self.force_jump_veneers {
|
||||
unsafe {
|
||||
cfg.cranelift_flag_set("wasmtime_linkopt_force_jump_veneer", "true")
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
match &self.memory_config {
|
||||
MemoryConfig::Normal {
|
||||
static_memory_maximum_size,
|
||||
static_memory_guard_size,
|
||||
dynamic_memory_guard_size,
|
||||
guard_before_linear_memory,
|
||||
} => {
|
||||
cfg.static_memory_maximum_size(static_memory_maximum_size.unwrap_or(0).into())
|
||||
.static_memory_guard_size(static_memory_guard_size.unwrap_or(0).into())
|
||||
.dynamic_memory_guard_size(dynamic_memory_guard_size.unwrap_or(0).into())
|
||||
.guard_before_linear_memory(*guard_before_linear_memory);
|
||||
}
|
||||
MemoryConfig::CustomUnaligned => {
|
||||
cfg.with_host_memory(Arc::new(UnalignedMemoryCreator))
|
||||
.static_memory_maximum_size(0)
|
||||
.dynamic_memory_guard_size(0)
|
||||
.static_memory_guard_size(0)
|
||||
.guard_before_linear_memory(false);
|
||||
}
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
}
|
||||
|
||||
struct UnalignedMemoryCreator;
|
||||
|
||||
unsafe impl MemoryCreator for UnalignedMemoryCreator {
|
||||
fn new_memory(
|
||||
&self,
|
||||
_ty: MemoryType,
|
||||
minimum: usize,
|
||||
maximum: Option<usize>,
|
||||
reserved_size_in_bytes: Option<usize>,
|
||||
guard_size_in_bytes: usize,
|
||||
) -> Result<Box<dyn LinearMemory>, String> {
|
||||
assert_eq!(guard_size_in_bytes, 0);
|
||||
assert!(reserved_size_in_bytes.is_none() || reserved_size_in_bytes == Some(0));
|
||||
Ok(Box::new(UnalignedMemory {
|
||||
src: vec![0; minimum + 1],
|
||||
maximum,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// A custom "linear memory allocator" for wasm which only works with the
|
||||
/// "dynamic" mode of configuration where wasm always does explicit bounds
|
||||
/// checks.
|
||||
///
|
||||
/// This memory attempts to always use unaligned host addresses for the base
|
||||
/// address of linear memory with wasm. This means that all jit loads/stores
|
||||
/// should be unaligned, which is a "big hammer way" of testing that all our JIT
|
||||
/// code works with unaligned addresses since alignment is not required for
|
||||
/// correctness in wasm itself.
|
||||
struct UnalignedMemory {
|
||||
/// This memory is always one byte larger than the actual size of linear
|
||||
/// memory.
|
||||
src: Vec<u8>,
|
||||
maximum: Option<usize>,
|
||||
}
|
||||
|
||||
unsafe impl LinearMemory for UnalignedMemory {
|
||||
fn byte_size(&self) -> usize {
|
||||
// Chop off the extra byte reserved for the true byte size of this
|
||||
// linear memory.
|
||||
self.src.len() - 1
|
||||
}
|
||||
|
||||
fn maximum_byte_size(&self) -> Option<usize> {
|
||||
self.maximum
|
||||
}
|
||||
|
||||
fn grow_to(&mut self, new_size: usize) -> Result<()> {
|
||||
// Make sure to allocate an extra byte for our "unalignment"
|
||||
self.src.resize(new_size + 1, 0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn as_ptr(&self) -> *mut u8 {
|
||||
// Return our allocated memory, offset by one, so that the base address
|
||||
// of memory is always unaligned.
|
||||
self.src[1..].as_ptr() as *mut _
|
||||
}
|
||||
}
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/spectests.rs"));
|
||||
|
||||
/// A spec test from the upstream wast testsuite, arbitrarily chosen from the
|
||||
|
||||
@@ -238,7 +238,7 @@ pub fn compile(wasm: &[u8], strategy: Strategy) {
|
||||
/// we call the exported functions for all of our different configs.
|
||||
pub fn differential_execution(
|
||||
module: &crate::generators::GeneratedModule,
|
||||
configs: &[crate::generators::DifferentialConfig],
|
||||
configs: &[crate::generators::Config],
|
||||
) {
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
@@ -252,18 +252,13 @@ pub fn differential_execution(
|
||||
return;
|
||||
}
|
||||
|
||||
let configs: Vec<_> = match configs.iter().map(|c| c.to_wasmtime_config()).collect() {
|
||||
Ok(cs) => cs,
|
||||
// If the config is trying to use something that was turned off at
|
||||
// compile time just continue to the next fuzz input.
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let configs: Vec<_> = configs.iter().map(|c| (c.to_wasmtime(), c)).collect();
|
||||
let mut export_func_results: HashMap<String, Result<Box<[Val]>, Trap>> = Default::default();
|
||||
let wasm = module.module.to_bytes();
|
||||
log_wasm(&wasm);
|
||||
|
||||
for mut config in configs {
|
||||
for (mut config, fuzz_config) in configs {
|
||||
log::debug!("fuzz config: {:?}", fuzz_config);
|
||||
// Disable module linking since it isn't enabled by default for
|
||||
// `GeneratedModule` but is enabled by default for our fuzz config.
|
||||
// Since module linking is currently a breaking change this is required
|
||||
@@ -272,6 +267,9 @@ pub fn differential_execution(
|
||||
|
||||
let engine = Engine::new(&config).unwrap();
|
||||
let mut store = create_store(&engine);
|
||||
if fuzz_config.consume_fuel {
|
||||
store.add_fuel(u64::max_value()).unwrap();
|
||||
}
|
||||
|
||||
let module = Module::new(&engine, &wasm).unwrap();
|
||||
|
||||
@@ -293,10 +291,7 @@ pub fn differential_execution(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for (name, f) in exports {
|
||||
// Always call the hang limit initializer first, so that we don't
|
||||
// infinite loop when calling another export.
|
||||
init_hang_limit(&mut store, instance);
|
||||
|
||||
log::debug!("invoke export {:?}", name);
|
||||
let ty = f.ty(&store);
|
||||
let params = dummy::dummy_values(ty.params());
|
||||
let mut results = vec![Val::I32(0); ty.results().len()];
|
||||
@@ -312,17 +307,6 @@ pub fn differential_execution(
|
||||
}
|
||||
}
|
||||
|
||||
fn init_hang_limit<T>(store: &mut Store<T>, instance: Instance) {
|
||||
match instance.get_export(&mut *store, "hangLimitInitializer") {
|
||||
None => return,
|
||||
Some(Extern::Func(f)) => {
|
||||
f.call(store, &[], &mut [])
|
||||
.expect("initializing the hang limit should not fail");
|
||||
}
|
||||
Some(_) => panic!("unexpected hangLimitInitializer export"),
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_same_export_func_result(
|
||||
lhs: &Result<Box<[Val]>, Trap>,
|
||||
rhs: &Result<Box<[Val]>, Trap>,
|
||||
@@ -337,7 +321,11 @@ pub fn differential_execution(
|
||||
};
|
||||
|
||||
match (lhs, rhs) {
|
||||
(Err(_), Err(_)) => {}
|
||||
(Err(a), Err(b)) => {
|
||||
if a.trap_code() != b.trap_code() {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
(Ok(lhs), Ok(rhs)) => {
|
||||
if lhs.len() != rhs.len() {
|
||||
fail();
|
||||
|
||||
@@ -4,8 +4,8 @@ use libfuzzer_sys::fuzz_target;
|
||||
use wasmtime_fuzzing::{generators, oracles};
|
||||
|
||||
fuzz_target!(|data: (
|
||||
generators::DifferentialConfig,
|
||||
generators::DifferentialConfig,
|
||||
generators::Config,
|
||||
generators::Config,
|
||||
generators::GeneratedModule,
|
||||
)| {
|
||||
let (lhs, rhs, mut wasm) = data;
|
||||
|
||||
Reference in New Issue
Block a user