Skip to content

Commit 0b9ff9b

Browse files
authored
Add async to the component_api fuzzer (#12047)
This commit extends the preexisting `component_api` fuzzer in Wasmtime to support `async`. This required a fair bit of refactoring to how the fuzzer worked but the basic idea remains the same. This fuzzer generates: * An arbitrary component-model type signature (minus resources/futures/streams) * A component which imports a function of this signature from the host and exports a function of this signature to the host. * Internally the export is implemented with a component `caller` that imports/exports this function signature. * The `caller` component is instantiated with a `callee` component that imports/exports this function signature. * The `callee` component is instantiated with the host. In essence a component model value is threaded into a component, to another composed component, then back out to the host. The return value then makes its way back through the same channel. The fuzz test ensures that the parameters received in the host import are the same as those passed to the export. Additionally the return value of the export is ensured to be the same as the one that the host import returned. In essence this is testing ABI handling in Wasmtime to ensure that composition works correctly in addition to lifting/lowering code. This fuzzer additionally has a "static" and "dynamic" mode where, at compile time, N components are generated with different signatures and use the "typed" APIs of Wasmtime. For "dynamic" `Val` is used to test and arbitrary components are generated at fuzz-time. The fuzzer finally executes this roundtrip in a loop multiple times in one fuzz test case to test different runtime-shapes of values in addition to compile-time-shapes of values. The main addition in this commit is to extend all of this with the async ABI. The following knobs were added and implemented: * The function type used for the root component export, the function between the composed components, and the function imported from the host all have the ability to be an `async` function type now. * The lifts/lowers, in all locations, can be configured with the various ABIs supported (e.g. sync, async-callback, or async-stackful for lifts, and sync/async for lowers). * Like before the string encoding can be varied between the components as well. This is intended to stress combining different flavors of ABI with different behaviors to ensure that all the various paths throughout Wasmtime and such are hit. The goal of this fuzzer is to stress ABI lifting/lowering, so this is not handling much related to async event scheduling (that's for a future fuzzer). In a follow-up commit I hope to extend this fuzzer with some async event scheduling nonetheless. For example the fuzzer will generate an async yield point before/after calling `task.return` or before/after calling the host import. That is intended to stress `first_poll`-vs-not behavior as it relates to ABI handling. None of that is yet implemented and for this fuzzer async calls are assumed to always succeed immediately for now.
1 parent e5aa998 commit 0b9ff9b

File tree

8 files changed

+1228
-269
lines changed

8 files changed

+1228
-269
lines changed

Cargo.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/environ/fuzz/fuzz_targets/fact-valid-module.rs

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ use wasmtime_environ::{ScopeVec, Tunables, component::*};
1515
use wasmtime_test_util::component_fuzz::{MAX_TYPE_DEPTH, TestCase, Type};
1616

1717
const TYPE_COUNT: usize = 50;
18-
const MAX_ARITY: u32 = 5;
1918

2019
#[derive(Debug)]
2120
struct GenAdapter<'a> {
@@ -48,21 +47,7 @@ fn target(data: &[u8]) -> arbitrary::Result<()> {
4847
}
4948

5049
// Next generate a static API test case driven by the above types.
51-
let mut params = Vec::new();
52-
let mut result = None;
53-
for _ in 0..u.int_in_range(0..=MAX_ARITY)? {
54-
params.push(u.choose(&types)?);
55-
}
56-
if u.arbitrary()? {
57-
result = Some(u.choose(&types)?);
58-
}
59-
60-
let test = TestCase {
61-
params,
62-
result,
63-
encoding1: u.arbitrary()?,
64-
encoding2: u.arbitrary()?,
65-
};
50+
let test = TestCase::generate(&types, &mut u)?;
6651
let adapter = GenAdapter { test };
6752

6853
let wat_decls = adapter.test.declarations();

crates/fuzzing/src/generators/component_types.rs

Lines changed: 72 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
//! parameters to the imported one and forwards the result back to the caller. This serves to exercise Wasmtime's
77
//! lifting and lowering code and verify the values remain intact during both processes.
88
9+
use crate::block_on;
910
use arbitrary::{Arbitrary, Unstructured};
1011
use std::any::Any;
1112
use std::fmt::Debug;
1213
use std::ops::ControlFlow;
1314
use wasmtime::component::{self, Component, ComponentNamedList, Lift, Linker, Lower, Val};
14-
use wasmtime::{Config, Engine, Store, StoreContextMut};
15+
use wasmtime::{AsContextMut, Config, Engine, Store, StoreContextMut};
1516
use wasmtime_test_util::component_fuzz::{Declarations, EXPORT_FUNCTION, IMPORT_FUNCTION};
1617

1718
/// Minimum length of an arbitrary list value generated for a test case
@@ -147,45 +148,82 @@ where
147148

148149
let mut config = Config::new();
149150
config.wasm_component_model(true);
151+
config.wasm_component_model_async(true);
152+
config.wasm_component_model_async_stackful(true);
153+
config.async_support(true);
150154
config.debug_adapter_modules(input.arbitrary()?);
151155
let engine = Engine::new(&config).unwrap();
152156
let wat = declarations.make_component();
153157
let wat = wat.as_bytes();
154158
crate::oracles::log_wasm(wat);
155159
let component = Component::new(&engine, wat).unwrap();
156160
let mut linker = Linker::new(&engine);
157-
linker
158-
.root()
159-
.func_wrap(
160-
IMPORT_FUNCTION,
161-
|cx: StoreContextMut<'_, Box<dyn Any + Send>>, params: P| {
162-
log::trace!("received parameters {params:?}");
163-
let data: &(P, R) = cx.data().downcast_ref().unwrap();
164-
let (expected_params, result) = data;
165-
assert_eq!(params, *expected_params);
166-
log::trace!("returning result {result:?}");
167-
Ok(result.clone())
168-
},
169-
)
170-
.unwrap();
171-
let mut store: Store<Box<dyn Any + Send>> = Store::new(&engine, Box::new(()));
172-
let instance = linker.instantiate(&mut store, &component).unwrap();
173-
let func = instance
174-
.get_typed_func::<P, R>(&mut store, EXPORT_FUNCTION)
175-
.unwrap();
176-
177-
while input.arbitrary()? {
178-
let params = input.arbitrary::<P>()?;
179-
let result = input.arbitrary::<R>()?;
180-
*store.data_mut() = Box::new((params.clone(), result.clone()));
181-
log::trace!("passing in parameters {params:?}");
182-
let actual = func.call(&mut store, params).unwrap();
183-
log::trace!("got result {actual:?}");
184-
assert_eq!(actual, result);
185-
func.post_return(&mut store).unwrap();
161+
162+
fn host_function<P, R>(
163+
cx: StoreContextMut<'_, Box<dyn Any + Send>>,
164+
params: P,
165+
) -> anyhow::Result<R>
166+
where
167+
P: Debug + PartialEq + 'static,
168+
R: Debug + Clone + 'static,
169+
{
170+
log::trace!("received parameters {params:?}");
171+
let data: &(P, R) = cx.data().downcast_ref().unwrap();
172+
let (expected_params, result) = data;
173+
assert_eq!(params, *expected_params);
174+
log::trace!("returning result {result:?}");
175+
Ok(result.clone())
176+
}
177+
178+
if declarations.options.host_async {
179+
linker
180+
.root()
181+
.func_wrap_concurrent(IMPORT_FUNCTION, |a, params| {
182+
Box::pin(async move {
183+
a.with(|mut cx| host_function::<P, R>(cx.as_context_mut(), params))
184+
})
185+
})
186+
.unwrap();
187+
} else {
188+
linker
189+
.root()
190+
.func_wrap(IMPORT_FUNCTION, |cx, params| {
191+
host_function::<P, R>(cx, params)
192+
})
193+
.unwrap();
186194
}
195+
let mut store: Store<Box<dyn Any + Send>> = Store::new(&engine, Box::new(()));
196+
197+
block_on(async {
198+
let instance = linker
199+
.instantiate_async(&mut store, &component)
200+
.await
201+
.unwrap();
202+
let func = instance
203+
.get_typed_func::<P, R>(&mut store, EXPORT_FUNCTION)
204+
.unwrap();
187205

188-
Ok(())
206+
while input.arbitrary()? {
207+
let params = input.arbitrary::<P>()?;
208+
let result = input.arbitrary::<R>()?;
209+
*store.data_mut() = Box::new((params.clone(), result.clone()));
210+
log::trace!("passing in parameters {params:?}");
211+
let actual = if declarations.options.guest_caller_async {
212+
store
213+
.run_concurrent(async |a| func.call_concurrent(a, params).await.unwrap().0)
214+
.await
215+
.unwrap()
216+
} else {
217+
let result = func.call_async(&mut store, params).await.unwrap();
218+
func.post_return_async(&mut store).await.unwrap();
219+
result
220+
};
221+
log::trace!("got result {actual:?}");
222+
assert_eq!(actual, result);
223+
}
224+
225+
Ok(())
226+
})
189227
}
190228

191229
#[cfg(test)]
@@ -197,12 +235,9 @@ mod tests {
197235
#[test]
198236
fn static_api_smoke_test() {
199237
test_n_times(10, |(), u| {
200-
let case = TestCase {
201-
params: vec![&Type::S32, &Type::Bool, &Type::String],
202-
result: Some(&Type::String),
203-
encoding1: u.arbitrary()?,
204-
encoding2: u.arbitrary()?,
205-
};
238+
let mut case = TestCase::generate(&[], u)?;
239+
case.params = vec![&Type::S32, &Type::Bool, &Type::String];
240+
case.result = Some(&Type::String);
206241

207242
let declarations = case.declarations();
208243
static_api_test::<(i32, bool, String), (String,)>(u, &declarations)

crates/fuzzing/src/lib.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
33
#![deny(missing_docs)]
44

5+
use std::task::{Context, Poll, Waker};
6+
57
pub use wasm_mutate;
68
pub use wasm_smith;
79
pub mod generators;
@@ -30,6 +32,17 @@ pub fn init_fuzzing() {
3032
});
3133
}
3234

35+
fn block_on<F: Future>(future: F) -> F::Output {
36+
let mut f = Box::pin(future);
37+
let mut cx = Context::from_waker(Waker::noop());
38+
loop {
39+
match f.as_mut().poll(&mut cx) {
40+
Poll::Ready(val) => break val,
41+
Poll::Pending => {}
42+
}
43+
}
44+
}
45+
3346
#[cfg(test)]
3447
mod test {
3548
use arbitrary::{Arbitrary, Unstructured};

0 commit comments

Comments
 (0)