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
77 changes: 37 additions & 40 deletions crates/fuzzing/src/generators/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,23 @@ pub struct Config {

impl Config {
/// Indicates that this configuration is being used for differential
/// execution so only a single function should be generated since that's all
/// that's going to be exercised.
/// execution.
///
/// The purpose of this function is to update the configuration which was
/// generated to be compatible with execution in multiple engines. The goal
/// is to produce the exact same result in all engines so we need to paper
/// over things like nan differences and memory/table behavior differences.
pub fn set_differential_config(&mut self) {
let config = &mut self.module_config.config;

// Disable the start function for now.
//
// TODO: should probably allow this after testing it works with the new
// differential setup in all engines.
config.allow_start_export = false;

// Make sure there's a type available for the function.
// Make it more likely that there are types available to generate a
// function with.
config.min_types = 1;
config.max_types = config.max_types.max(1);

Expand Down Expand Up @@ -70,15 +79,6 @@ impl Config {
// can paper over NaN differences between engines.
config.canonicalize_nans = true;

// When diffing against a non-wasmtime engine then disable wasm
// features to get selectively re-enabled against each differential
// engine.
config.bulk_memory_enabled = false;
config.reference_types_enabled = false;
config.simd_enabled = false;
config.memory64_enabled = false;
config.threads_enabled = false;

// If using the pooling allocator, update the instance limits too
if let InstanceAllocationStrategy::Pooling {
instance_limits: limits,
Expand All @@ -103,34 +103,6 @@ impl Config {
}
}

/// Force `self` to be a configuration compatible with `other`. This is
/// useful for differential execution to avoid unhelpful fuzz crashes when
/// one engine has a feature enabled and the other does not.
pub fn make_compatible_with(&mut self, other: &Self) {
// Use the same `wasm-smith` configuration as `other` because this is
// used for determining what Wasm features are enabled in the engine
// (see `to_wasmtime`).
self.module_config = other.module_config.clone();

// Use the same allocation strategy between the two configs.
//
// Ideally this wouldn't be necessary, but, during differential
// evaluation, if the `lhs` is using ondemand and the `rhs` is using the
// pooling allocator (or vice versa), then the module may have been
// generated in such a way that is incompatible with the other
// allocation strategy.
//
// We can remove this in the future when it's possible to access the
// fields of `wasm_smith::Module` to constrain the pooling allocator
// based on what was actually generated.
self.wasmtime.strategy = other.wasmtime.strategy.clone();
if let InstanceAllocationStrategy::Pooling { .. } = &other.wasmtime.strategy {
// Also use the same memory configuration when using the pooling
// allocator.
self.wasmtime.memory_config = other.wasmtime.memory_config.clone();
}
}

/// Uses this configuration and the supplied source of data to generate
/// a wasm module.
///
Expand Down Expand Up @@ -416,6 +388,31 @@ pub struct WasmtimeConfig {
native_unwind_info: bool,
}

impl WasmtimeConfig {
/// Force `self` to be a configuration compatible with `other`. This is
/// useful for differential execution to avoid unhelpful fuzz crashes when
/// one engine has a feature enabled and the other does not.
pub fn make_compatible_with(&mut self, other: &Self) {
// Use the same allocation strategy between the two configs.
//
// Ideally this wouldn't be necessary, but, during differential
// evaluation, if the `lhs` is using ondemand and the `rhs` is using the
// pooling allocator (or vice versa), then the module may have been
// generated in such a way that is incompatible with the other
// allocation strategy.
//
// We can remove this in the future when it's possible to access the
// fields of `wasm_smith::Module` to constrain the pooling allocator
// based on what was actually generated.
self.strategy = other.strategy.clone();
if let InstanceAllocationStrategy::Pooling { .. } = &other.strategy {
// Also use the same memory configuration when using the pooling
// allocator.
self.memory_config = other.memory_config.clone();
}
}
}

#[derive(Arbitrary, Clone, Debug, PartialEq, Eq, Hash)]
enum OptLevel {
None,
Expand Down
45 changes: 40 additions & 5 deletions crates/fuzzing/src/generators/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub enum DiffValue {
F32(u32),
F64(u64),
V128(u128),
FuncRef { null: bool },
ExternRef { null: bool },
}

impl DiffValue {
Expand All @@ -23,6 +25,8 @@ impl DiffValue {
DiffValue::F32(_) => DiffValueType::F32,
DiffValue::F64(_) => DiffValueType::F64,
DiffValue::V128(_) => DiffValueType::V128,
DiffValue::FuncRef { .. } => DiffValueType::FuncRef,
DiffValue::ExternRef { .. } => DiffValueType::ExternRef,
}
}

Expand Down Expand Up @@ -51,7 +55,18 @@ impl DiffValue {
(1.0f32).to_bits(),
f32::MAX.to_bits(),
];
DiffValue::F32(biased_arbitrary_value(u, known_f32_values)?)
let bits = biased_arbitrary_value(u, known_f32_values)?;

// If the chosen bits are NAN then always use the canonical bit
// pattern of nan to enable better compatibility with engines
// where arbitrary nan patterns can't make their way into wasm
// (e.g. v8 through JS can't do that).
let bits = if f32::from_bits(bits).is_nan() {
f32::NAN.to_bits()
} else {
bits
};
DiffValue::F32(bits)
}
F64 => {
// TODO once `to_bits` is stable as a `const` function, move
Expand All @@ -66,9 +81,23 @@ impl DiffValue {
(1.0f64).to_bits(),
f64::MAX.to_bits(),
];
DiffValue::F64(biased_arbitrary_value(u, known_f64_values)?)
let bits = biased_arbitrary_value(u, known_f64_values)?;
// See `f32` above for why canonical nan patterns are always
// used.
let bits = if f64::from_bits(bits).is_nan() {
f64::NAN.to_bits()
} else {
bits
};
DiffValue::F64(bits)
}
V128 => DiffValue::V128(biased_arbitrary_value(u, KNOWN_U128_VALUES)?),

// TODO: this isn't working in most engines so just always pass a
// null in which if an engine supports this is should at least
// support doing that.
FuncRef => DiffValue::FuncRef { null: true },
ExternRef => DiffValue::ExternRef { null: true },
};
arbitrary::Result::Ok(val)
}
Expand Down Expand Up @@ -111,6 +140,8 @@ impl Hash for DiffValue {
DiffValue::F32(n) => n.hash(state),
DiffValue::F64(n) => n.hash(state),
DiffValue::V128(n) => n.hash(state),
DiffValue::ExternRef { null } => null.hash(state),
DiffValue::FuncRef { null } => null.hash(state),
}
}
}
Expand Down Expand Up @@ -144,20 +175,24 @@ impl PartialEq for DiffValue {
let r0 = f64::from_bits(*r0);
l0 == r0 || (l0.is_nan() && r0.is_nan())
}
(Self::FuncRef { null: a }, Self::FuncRef { null: b }) => a == b,
(Self::ExternRef { null: a }, Self::ExternRef { null: b }) => a == b,
_ => false,
}
}
}

/// Enumerate the supported value types.
#[derive(Clone, Debug, Arbitrary, Hash)]
#[derive(Copy, Clone, Debug, Arbitrary, Hash)]
#[allow(missing_docs)]
pub enum DiffValueType {
I32,
I64,
F32,
F64,
V128,
FuncRef,
ExternRef,
}

impl TryFrom<wasmtime::ValType> for DiffValueType {
Expand All @@ -170,8 +205,8 @@ impl TryFrom<wasmtime::ValType> for DiffValueType {
F32 => Ok(Self::F32),
F64 => Ok(Self::F64),
V128 => Ok(Self::V128),
FuncRef => Err("unable to convert reference types"),
ExternRef => Err("unable to convert reference types"),
FuncRef => Ok(Self::FuncRef),
ExternRef => Ok(Self::ExternRef),
}
}
}
59 changes: 37 additions & 22 deletions crates/fuzzing/src/oracles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,10 @@ mod stacks;

use self::diff_wasmtime::WasmtimeInstance;
use self::engine::DiffInstance;
use crate::generators::{self, DiffValue};
use crate::generators::{self, DiffValue, DiffValueType};
use arbitrary::Arbitrary;
pub use stacks::check_stacks;
use std::cell::Cell;
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
use std::rc::Rc;
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
use std::sync::{Arc, Condvar, Mutex};
Expand All @@ -34,9 +32,7 @@ use wasmtime::*;
use wasmtime_wast::WastContext;

#[cfg(not(any(windows, target_arch = "s390x")))]
pub use self::v8::*;
#[cfg(not(any(windows, target_arch = "s390x")))]
mod v8;
mod diff_v8;

static CNT: AtomicUsize = AtomicUsize::new(0);

Expand Down Expand Up @@ -343,12 +339,24 @@ pub fn differential(
rhs: &mut WasmtimeInstance,
name: &str,
args: &[DiffValue],
result_tys: &[DiffValueType],
) -> anyhow::Result<()> {
log::debug!("Evaluating: {}({:?})", name, args);
let lhs_results = lhs.evaluate(name, args);
log::debug!("Evaluating: `{}` with {:?}", name, args);
let lhs_results = match lhs.evaluate(name, args, result_tys) {
Ok(Some(results)) => Ok(results),
Err(e) => Err(e),
// this engine couldn't execute this type signature, so discard this
// execution by returning success.
Ok(None) => return Ok(()),
};
log::debug!(" -> results on {}: {:?}", lhs.name(), &lhs_results);
let rhs_results = rhs.evaluate(name, args);

let rhs_results = rhs
.evaluate(name, args, result_tys)
// wasmtime should be able to invoke any signature, so unwrap this result
.map(|results| results.unwrap());
log::debug!(" -> results on {}: {:?}", rhs.name(), &rhs_results);

match (lhs_results, rhs_results) {
// If the evaluation succeeds, we compare the results.
(Ok(lhs_results), Ok(rhs_results)) => assert_eq!(lhs_results, rhs_results),
Expand All @@ -362,19 +370,26 @@ pub fn differential(
(Err(_), Ok(_)) => panic!("only the `lhs` ({}) failed for this input", lhs.name()),
};

let hash = |i: &mut dyn DiffInstance| -> anyhow::Result<u64> {
let mut hasher = DefaultHasher::new();
i.hash(&mut hasher)?;
Ok(hasher.finish())
};

if lhs.is_hashable() && rhs.is_hashable() {
log::debug!("Hashing instances:");
let lhs_hash = hash(lhs)?;
log::debug!(" -> hash of {}: {:?}", lhs.name(), lhs_hash);
let rhs_hash = hash(rhs)?;
log::debug!(" -> hash of {}: {:?}", rhs.name(), rhs_hash);
assert_eq!(lhs_hash, rhs_hash);
for (global, ty) in rhs.exported_globals() {
log::debug!("Comparing global `{global}`");
let lhs = match lhs.get_global(&global, ty) {
Some(val) => val,
None => continue,
};
let rhs = rhs.get_global(&global, ty).unwrap();
assert_eq!(lhs, rhs);
}
for (memory, shared) in rhs.exported_memories() {
log::debug!("Comparing memory `{memory}`");
let lhs = match lhs.get_memory(&memory, shared) {
Some(val) => val,
None => continue,
};
let rhs = rhs.get_memory(&memory, shared).unwrap();
if lhs == rhs {
continue;
}
panic!("memories have differing values");
}

Ok(())
Expand Down
Loading