implement fuzzing for component types (#4537)

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>
This commit is contained in:
Joel Dice
2022-08-04 11:02:55 -06:00
committed by GitHub
parent ad223c5234
commit ed8908efcf
29 changed files with 1963 additions and 266 deletions

View File

@@ -1,8 +1,9 @@
use anyhow::Result;
use component_test_util::{engine, TypedFuncExt};
use std::fmt::Write;
use std::iter;
use wasmtime::component::{Component, ComponentParams, Lift, Lower, TypedFunc};
use wasmtime::{AsContextMut, Config, Engine};
use wasmtime::component::Component;
use wasmtime_component_util::REALLOC_AND_FREE;
mod dynamic;
mod func;
@@ -12,97 +13,6 @@ mod macros;
mod nested;
mod post_return;
trait TypedFuncExt<P, R> {
fn call_and_post_return(&self, store: impl AsContextMut, params: P) -> Result<R>;
}
impl<P, R> TypedFuncExt<P, R> for TypedFunc<P, R>
where
P: ComponentParams + Lower,
R: Lift,
{
fn call_and_post_return(&self, mut store: impl AsContextMut, params: P) -> Result<R> {
let result = self.call(&mut store, params)?;
self.post_return(&mut store)?;
Ok(result)
}
}
// A simple bump allocator which can be used with modules
const REALLOC_AND_FREE: &str = r#"
(global $last (mut i32) (i32.const 8))
(func $realloc (export "realloc")
(param $old_ptr i32)
(param $old_size i32)
(param $align i32)
(param $new_size i32)
(result i32)
;; Test if the old pointer is non-null
local.get $old_ptr
if
;; If the old size is bigger than the new size then
;; this is a shrink and transparently allow it
local.get $old_size
local.get $new_size
i32.gt_u
if
local.get $old_ptr
return
end
;; ... otherwise this is unimplemented
unreachable
end
;; align up `$last`
(global.set $last
(i32.and
(i32.add
(global.get $last)
(i32.add
(local.get $align)
(i32.const -1)))
(i32.xor
(i32.add
(local.get $align)
(i32.const -1))
(i32.const -1))))
;; save the current value of `$last` as the return value
global.get $last
;; ensure anything necessary is set to valid data by spraying a bit
;; pattern that is invalid
global.get $last
i32.const 0xde
local.get $new_size
memory.fill
;; bump our pointer
(global.set $last
(i32.add
(global.get $last)
(local.get $new_size)))
)
"#;
fn engine() -> Engine {
drop(env_logger::try_init());
let mut config = Config::new();
config.wasm_component_model(true);
// When pooling allocator tests are skipped it means we're in qemu. The
// component model tests create a disproportionate number of instances so
// try to cut down on virtual memory usage by avoiding 4G reservations.
if crate::skip_pooling_allocator_tests() {
config.static_memory_maximum_size(0);
config.dynamic_memory_guard_size(0);
}
Engine::new(&config).unwrap()
}
#[test]
fn components_importing_modules() -> Result<()> {
let engine = engine();
@@ -113,49 +23,49 @@ fn components_importing_modules() -> Result<()> {
Component::new(
&engine,
r#"
(component
(import "" (core module))
)
(component
(import "" (core module))
)
"#,
)?;
Component::new(
&engine,
r#"
(component
(import "" (core module $m1
(import "" "" (func))
(import "" "x" (global i32))
(component
(import "" (core module $m1
(import "" "" (func))
(import "" "x" (global i32))
(export "a" (table 1 funcref))
(export "b" (memory 1))
(export "c" (func (result f32)))
(export "d" (global i64))
))
(export "a" (table 1 funcref))
(export "b" (memory 1))
(export "c" (func (result f32)))
(export "d" (global i64))
))
(core module $m2
(func (export ""))
(global (export "x") i32 i32.const 0)
)
(core instance $i2 (instantiate (module $m2)))
(core instance $i1 (instantiate (module $m1) (with "" (instance $i2))))
(core module $m3
(import "mod" "1" (memory 1))
(import "mod" "2" (table 1 funcref))
(import "mod" "3" (global i64))
(import "mod" "4" (func (result f32)))
)
(core instance $i3 (instantiate (module $m3)
(with "mod" (instance
(export "1" (memory $i1 "b"))
(export "2" (table $i1 "a"))
(export "3" (global $i1 "d"))
(export "4" (func $i1 "c"))
))
))
(core module $m2
(func (export ""))
(global (export "x") i32 i32.const 0)
)
(core instance $i2 (instantiate (module $m2)))
(core instance $i1 (instantiate (module $m1) (with "" (instance $i2))))
(core module $m3
(import "mod" "1" (memory 1))
(import "mod" "2" (table 1 funcref))
(import "mod" "3" (global i64))
(import "mod" "4" (func (result f32)))
)
(core instance $i3 (instantiate (module $m3)
(with "mod" (instance
(export "1" (memory $i1 "b"))
(export "2" (table $i1 "a"))
(export "3" (global $i1 "d"))
(export "4" (func $i1 "c"))
))
))
)
"#,
)?;

View File

@@ -1,19 +1,8 @@
use super::{make_echo_component, make_echo_component_with_params, Param, Type};
use anyhow::Result;
use wasmtime::component::{self, Component, Func, Linker, Val};
use wasmtime::{AsContextMut, Store};
trait FuncExt {
fn call_and_post_return(&self, store: impl AsContextMut, args: &[Val]) -> Result<Val>;
}
impl FuncExt for Func {
fn call_and_post_return(&self, mut store: impl AsContextMut, args: &[Val]) -> Result<Val> {
let result = self.call(&mut store, args)?;
self.post_return(&mut store)?;
Ok(result)
}
}
use component_test_util::FuncExt;
use wasmtime::component::{self, Component, Linker, Val};
use wasmtime::Store;
#[test]
fn primitives() -> Result<()> {

View File

@@ -1,5 +1,6 @@
use super::REALLOC_AND_FREE;
use anyhow::Result;
use std::ops::Deref;
use wasmtime::component::*;
use wasmtime::{Store, StoreContextMut, Trap};
@@ -117,6 +118,12 @@ fn simple() -> Result<()> {
"#;
let engine = super::engine();
let component = Component::new(&engine, component)?;
let mut store = Store::new(&engine, None);
assert!(store.data().is_none());
// First, test the static API
let mut linker = Linker::new(&engine);
linker.root().func_wrap(
"",
@@ -127,15 +134,36 @@ fn simple() -> Result<()> {
Ok(())
},
)?;
let component = Component::new(&engine, component)?;
let mut store = Store::new(&engine, None);
let instance = linker.instantiate(&mut store, &component)?;
assert!(store.data().is_none());
instance
.get_typed_func::<(), (), _>(&mut store, "call")?
.call(&mut store, ())?;
assert_eq!(store.data().as_ref().unwrap(), "hello world");
// Next, test the dynamic API
*store.data_mut() = None;
let mut linker = Linker::new(&engine);
linker.root().func_new(
&component,
"",
|mut store: StoreContextMut<'_, Option<String>>, args| {
if let Val::String(s) = &args[0] {
assert!(store.data().is_none());
*store.data_mut() = Some(s.to_string());
Ok(Val::Unit)
} else {
panic!()
}
},
)?;
let instance = linker.instantiate(&mut store, &component)?;
instance
.get_func(&mut store, "call")
.unwrap()
.call(&mut store, &[])?;
assert_eq!(store.data().as_ref().unwrap(), "hello world");
Ok(())
}
@@ -299,15 +327,20 @@ fn attempt_to_reenter_during_host() -> Result<()> {
)
"#;
struct State {
let engine = super::engine();
let component = Component::new(&engine, component)?;
// First, test the static API
struct StaticState {
func: Option<TypedFunc<(), ()>>,
}
let engine = super::engine();
let mut store = Store::new(&engine, StaticState { func: None });
let mut linker = Linker::new(&engine);
linker.root().func_wrap(
"thunk",
|mut store: StoreContextMut<'_, State>| -> Result<()> {
|mut store: StoreContextMut<'_, StaticState>| -> Result<()> {
let func = store.data_mut().func.take().unwrap();
let trap = func.call(&mut store, ()).unwrap_err();
assert!(
@@ -319,12 +352,39 @@ fn attempt_to_reenter_during_host() -> Result<()> {
Ok(())
},
)?;
let component = Component::new(&engine, component)?;
let mut store = Store::new(&engine, State { func: None });
let instance = linker.instantiate(&mut store, &component)?;
let func = instance.get_typed_func::<(), (), _>(&mut store, "run")?;
store.data_mut().func = Some(func);
func.call(&mut store, ())?;
// Next, test the dynamic API
struct DynamicState {
func: Option<Func>,
}
let mut store = Store::new(&engine, DynamicState { func: None });
let mut linker = Linker::new(&engine);
linker.root().func_new(
&component,
"thunk",
|mut store: StoreContextMut<'_, DynamicState>, _| {
let func = store.data_mut().func.take().unwrap();
let trap = func.call(&mut store, &[]).unwrap_err();
assert!(
trap.to_string()
.contains("cannot reenter component instance"),
"bad trap: {}",
trap,
);
Ok(Val::Unit)
},
)?;
let instance = linker.instantiate(&mut store, &component)?;
let func = instance.get_func(&mut store, "run").unwrap();
store.data_mut().func = Some(func);
func.call(&mut store, &[])?;
Ok(())
}
@@ -466,6 +526,11 @@ fn stack_and_heap_args_and_rets() -> Result<()> {
);
let engine = super::engine();
let component = Component::new(&engine, component)?;
let mut store = Store::new(&engine, ());
// First, test the static API
let mut linker = Linker::new(&engine);
linker.root().func_wrap("f1", |x: u32| -> Result<u32> {
assert_eq!(x, 1);
@@ -515,12 +580,60 @@ fn stack_and_heap_args_and_rets() -> Result<()> {
Ok("xyz".to_string())
},
)?;
let component = Component::new(&engine, component)?;
let mut store = Store::new(&engine, ());
let instance = linker.instantiate(&mut store, &component)?;
instance
.get_typed_func::<(), (), _>(&mut store, "run")?
.call(&mut store, ())?;
// Next, test the dynamic API
let mut linker = Linker::new(&engine);
linker.root().func_new(&component, "f1", |_, args| {
if let Val::U32(x) = &args[0] {
assert_eq!(*x, 1);
Ok(Val::U32(2))
} else {
panic!()
}
})?;
linker.root().func_new(&component, "f2", |_, args| {
if let Val::Tuple(tuple) = &args[0] {
if let Val::String(s) = &tuple.values()[0] {
assert_eq!(s.deref(), "abc");
Ok(Val::U32(3))
} else {
panic!()
}
} else {
panic!()
}
})?;
linker.root().func_new(&component, "f3", |_, args| {
if let Val::U32(x) = &args[0] {
assert_eq!(*x, 8);
Ok(Val::String("xyz".into()))
} else {
panic!();
}
})?;
linker.root().func_new(&component, "f4", |_, args| {
if let Val::Tuple(tuple) = &args[0] {
if let Val::String(s) = &tuple.values()[0] {
assert_eq!(s.deref(), "abc");
Ok(Val::String("xyz".into()))
} else {
panic!()
}
} else {
panic!()
}
})?;
let instance = linker.instantiate(&mut store, &component)?;
instance
.get_func(&mut store, "run")
.unwrap()
.call(&mut store, &[])?;
Ok(())
}
@@ -648,6 +761,9 @@ fn no_actual_wasm_code() -> Result<()> {
let engine = super::engine();
let component = Component::new(&engine, component)?;
let mut store = Store::new(&engine, 0);
// First, test the static API
let mut linker = Linker::new(&engine);
linker
.root()
@@ -663,5 +779,23 @@ fn no_actual_wasm_code() -> Result<()> {
thunk.call(&mut store, ())?;
assert_eq!(*store.data(), 1);
// Next, test the dynamic API
*store.data_mut() = 0;
let mut linker = Linker::new(&engine);
linker
.root()
.func_new(&component, "f", |mut store: StoreContextMut<'_, u32>, _| {
*store.data_mut() += 1;
Ok(Val::Unit)
})?;
let instance = linker.instantiate(&mut store, &component)?;
let thunk = instance.get_func(&mut store, "thunk").unwrap();
assert_eq!(*store.data(), 0);
thunk.call(&mut store, &[])?;
assert_eq!(*store.data(), 1);
Ok(())
}