This addresses #4307. For the static API we generate 100 arbitrary test cases at build time, each of which includes 0-5 parameter types, a result type, and a WAT fragment containing an imported function and an exported function. The exported function calls the imported function, which is implemented by the host. At runtime, the fuzz test selects a test case at random and feeds it zero or more sets of arbitrary parameters and results, checking that values which flow host-to-guest and guest-to-host make the transition unchanged. The fuzz test for the dynamic API follows a similar pattern, the only difference being that test cases are generated at runtime. Signed-off-by: Joel Dice <joel.dice@fermyon.com>
263 lines
8.9 KiB
Rust
263 lines
8.9 KiB
Rust
//! A simple fuzzer for FACT
|
|
//!
|
|
//! This is an intentionally small fuzzer which is intended to only really be
|
|
//! used during the development of FACT itself when generating adapter modules.
|
|
//! This creates arbitrary adapter signatures and then generates the required
|
|
//! trampoline for that adapter ensuring that the final output wasm module is a
|
|
//! valid wasm module. This doesn't actually validate anything about the
|
|
//! correctness of the trampoline, only that it's valid wasm.
|
|
|
|
#![no_main]
|
|
|
|
use arbitrary::Arbitrary;
|
|
use component_fuzz_util::Type as ValType;
|
|
use libfuzzer_sys::fuzz_target;
|
|
use wasmparser::{Validator, WasmFeatures};
|
|
use wasmtime_environ::component::*;
|
|
use wasmtime_environ::fact::Module;
|
|
|
|
#[derive(Arbitrary, Debug)]
|
|
struct GenAdapterModule {
|
|
debug: bool,
|
|
adapters: Vec<GenAdapter>,
|
|
}
|
|
|
|
#[derive(Arbitrary, Debug)]
|
|
struct GenAdapter {
|
|
ty: FuncType,
|
|
post_return: bool,
|
|
lift_memory64: bool,
|
|
lower_memory64: bool,
|
|
lift_encoding: GenStringEncoding,
|
|
lower_encoding: GenStringEncoding,
|
|
}
|
|
|
|
#[derive(Arbitrary, Debug)]
|
|
struct FuncType {
|
|
params: Vec<ValType>,
|
|
result: ValType,
|
|
}
|
|
|
|
#[derive(Copy, Clone, Arbitrary, Debug)]
|
|
enum GenStringEncoding {
|
|
Utf8,
|
|
Utf16,
|
|
CompactUtf16,
|
|
}
|
|
|
|
fuzz_target!(|module: GenAdapterModule| { drop(target(module)) });
|
|
|
|
fn target(module: GenAdapterModule) -> Result<(), ()> {
|
|
drop(env_logger::try_init());
|
|
|
|
let mut types = ComponentTypesBuilder::default();
|
|
|
|
// Manufactures a unique `CoreDef` so all function imports get unique
|
|
// function imports.
|
|
let mut next_def = 0;
|
|
let mut dummy_def = || {
|
|
next_def += 1;
|
|
CoreDef::Adapter(AdapterIndex::from_u32(next_def))
|
|
};
|
|
|
|
// Manufactures a `CoreExport` for a memory with the shape specified. Note
|
|
// that we can't import as many memories as functions so these are
|
|
// intentionally limited. Once a handful of memories are generated of each
|
|
// type then they start getting reused.
|
|
let mut next_memory = 0;
|
|
let mut memories32 = Vec::new();
|
|
let mut memories64 = Vec::new();
|
|
let mut dummy_memory = |memory64: bool| {
|
|
let dst = if memory64 {
|
|
&mut memories64
|
|
} else {
|
|
&mut memories32
|
|
};
|
|
let idx = if dst.len() < 5 {
|
|
next_memory += 1;
|
|
dst.push(next_memory - 1);
|
|
next_memory - 1
|
|
} else {
|
|
dst[0]
|
|
};
|
|
CoreExport {
|
|
instance: RuntimeInstanceIndex::from_u32(idx),
|
|
item: ExportItem::Name(String::new()),
|
|
}
|
|
};
|
|
|
|
let mut adapters = Vec::new();
|
|
for adapter in module.adapters.iter() {
|
|
let mut params = Vec::new();
|
|
for param in adapter.ty.params.iter() {
|
|
params.push((None, intern(&mut types, param)?));
|
|
}
|
|
let result = intern(&mut types, &adapter.ty.result)?;
|
|
let signature = types.add_func_type(TypeFunc {
|
|
params: params.into(),
|
|
result,
|
|
});
|
|
adapters.push(Adapter {
|
|
lift_ty: signature,
|
|
lower_ty: signature,
|
|
lower_options: AdapterOptions {
|
|
instance: RuntimeComponentInstanceIndex::from_u32(0),
|
|
string_encoding: adapter.lower_encoding.into(),
|
|
memory64: adapter.lower_memory64,
|
|
// Pessimistically assume that memory/realloc are going to be
|
|
// required for this trampoline and provide it. Avoids doing
|
|
// calculations to figure out whether they're necessary and
|
|
// simplifies the fuzzer here without reducing coverage within FACT
|
|
// itself.
|
|
memory: Some(dummy_memory(adapter.lower_memory64)),
|
|
realloc: Some(dummy_def()),
|
|
// Lowering never allows `post-return`
|
|
post_return: None,
|
|
},
|
|
lift_options: AdapterOptions {
|
|
instance: RuntimeComponentInstanceIndex::from_u32(1),
|
|
string_encoding: adapter.lift_encoding.into(),
|
|
memory64: adapter.lift_memory64,
|
|
memory: Some(dummy_memory(adapter.lift_memory64)),
|
|
realloc: Some(dummy_def()),
|
|
post_return: if adapter.post_return {
|
|
Some(dummy_def())
|
|
} else {
|
|
None
|
|
},
|
|
},
|
|
func: dummy_def(),
|
|
});
|
|
}
|
|
let types = types.finish();
|
|
let mut fact_module = Module::new(&types, module.debug);
|
|
for (i, adapter) in adapters.iter().enumerate() {
|
|
fact_module.adapt(&format!("adapter{i}"), adapter);
|
|
}
|
|
let wasm = fact_module.encode();
|
|
let result = Validator::new_with_features(WasmFeatures {
|
|
multi_memory: true,
|
|
memory64: true,
|
|
..WasmFeatures::default()
|
|
})
|
|
.validate_all(&wasm);
|
|
|
|
let err = match result {
|
|
Ok(_) => return Ok(()),
|
|
Err(e) => e,
|
|
};
|
|
eprintln!("invalid wasm module: {err:?}");
|
|
for adapter in module.adapters.iter() {
|
|
eprintln!("adapter: {adapter:?}");
|
|
}
|
|
std::fs::write("invalid.wasm", &wasm).unwrap();
|
|
match wasmprinter::print_bytes(&wasm) {
|
|
Ok(s) => std::fs::write("invalid.wat", &s).unwrap(),
|
|
Err(_) => drop(std::fs::remove_file("invalid.wat")),
|
|
}
|
|
|
|
panic!()
|
|
}
|
|
|
|
fn intern(types: &mut ComponentTypesBuilder, ty: &ValType) -> Result<InterfaceType, ()> {
|
|
Ok(match ty {
|
|
ValType::Unit => InterfaceType::Unit,
|
|
ValType::Bool => InterfaceType::Bool,
|
|
ValType::U8 => InterfaceType::U8,
|
|
ValType::S8 => InterfaceType::S8,
|
|
ValType::U16 => InterfaceType::U16,
|
|
ValType::S16 => InterfaceType::S16,
|
|
ValType::U32 => InterfaceType::U32,
|
|
ValType::S32 => InterfaceType::S32,
|
|
ValType::U64 => InterfaceType::U64,
|
|
ValType::S64 => InterfaceType::S64,
|
|
ValType::Float32 => InterfaceType::Float32,
|
|
ValType::Float64 => InterfaceType::Float64,
|
|
ValType::Char => InterfaceType::Char,
|
|
ValType::List(ty) => {
|
|
let ty = intern(types, ty)?;
|
|
InterfaceType::List(types.add_interface_type(ty))
|
|
}
|
|
ValType::Record(tys) => {
|
|
let ty = TypeRecord {
|
|
fields: tys
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, ty)| {
|
|
Ok(RecordField {
|
|
name: format!("f{i}"),
|
|
ty: intern(types, ty)?,
|
|
})
|
|
})
|
|
.collect::<Result<_, _>>()?,
|
|
};
|
|
InterfaceType::Record(types.add_record_type(ty))
|
|
}
|
|
ValType::Flags(size) => {
|
|
let ty = TypeFlags {
|
|
names: (0..size.as_usize()).map(|i| format!("f{i}")).collect(),
|
|
};
|
|
InterfaceType::Flags(types.add_flags_type(ty))
|
|
}
|
|
ValType::Tuple(tys) => {
|
|
let ty = TypeTuple {
|
|
types: tys
|
|
.iter()
|
|
.map(|ty| intern(types, ty))
|
|
.collect::<Result<_, _>>()?,
|
|
};
|
|
InterfaceType::Tuple(types.add_tuple_type(ty))
|
|
}
|
|
ValType::Variant(cases) => {
|
|
let ty = TypeVariant {
|
|
cases: cases
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, ty)| {
|
|
Ok(VariantCase {
|
|
name: format!("c{i}"),
|
|
ty: intern(types, ty)?,
|
|
})
|
|
})
|
|
.collect::<Result<_, _>>()?,
|
|
};
|
|
InterfaceType::Variant(types.add_variant_type(ty))
|
|
}
|
|
ValType::Union(tys) => {
|
|
let ty = TypeUnion {
|
|
types: tys
|
|
.iter()
|
|
.map(|ty| intern(types, ty))
|
|
.collect::<Result<_, _>>()?,
|
|
};
|
|
InterfaceType::Union(types.add_union_type(ty))
|
|
}
|
|
ValType::Enum(size) => {
|
|
let ty = TypeEnum {
|
|
names: (0..size.as_usize()).map(|i| format!("c{i}")).collect(),
|
|
};
|
|
InterfaceType::Enum(types.add_enum_type(ty))
|
|
}
|
|
ValType::Option(ty) => {
|
|
let ty = intern(types, ty)?;
|
|
InterfaceType::Option(types.add_interface_type(ty))
|
|
}
|
|
ValType::Expected { ok, err } => {
|
|
let ok = intern(types, ok)?;
|
|
let err = intern(types, err)?;
|
|
InterfaceType::Expected(types.add_expected_type(TypeExpected { ok, err }))
|
|
}
|
|
ValType::String => return Err(()),
|
|
})
|
|
}
|
|
|
|
impl From<GenStringEncoding> for StringEncoding {
|
|
fn from(gen: GenStringEncoding) -> StringEncoding {
|
|
match gen {
|
|
GenStringEncoding::Utf8 => StringEncoding::Utf8,
|
|
GenStringEncoding::Utf16 => StringEncoding::Utf16,
|
|
GenStringEncoding::CompactUtf16 => StringEncoding::CompactUtf16,
|
|
}
|
|
}
|
|
}
|