Skip to main content

wasmtime_fuzzing/oracles/
component_api.rs

1//! This module generates test cases for the Wasmtime component model function APIs,
2//! e.g. `wasmtime::component::func::Func` and `TypedFunc`.
3//!
4//! Each case includes a list of arbitrary interface types to use as parameters, plus another one to use as a
5//! result, and a component which exports a function and imports a function.  The exported function forwards its
6//! parameters to the imported one and forwards the result back to the caller.  This serves to exercise Wasmtime's
7//! lifting and lowering code and verify the values remain intact during both processes.
8
9use crate::block_on;
10use crate::generators::{self, CompilerStrategy, InstanceAllocationStrategy};
11use crate::oracles::log_wasm;
12use arbitrary::{Arbitrary, Unstructured};
13use std::any::Any;
14use std::fmt::Debug;
15use std::ops::ControlFlow;
16use wasmtime::component::{
17    self, Accessor, Component, ComponentNamedList, Lift, Linker, Lower, Val,
18};
19use wasmtime::{AsContextMut, Enabled, Engine, Result, Store, StoreContextMut};
20use wasmtime_test_util::component_fuzz::{
21    Declarations, EXPORT_FUNCTION, IMPORT_FUNCTION, MAX_TYPE_DEPTH, TestCase, Type,
22};
23
24/// Minimum length of an arbitrary list value generated for a test case
25const MIN_LIST_LENGTH: u32 = 0;
26
27/// Maximum length of an arbitrary list value generated for a test case
28const MAX_LIST_LENGTH: u32 = 10;
29
30/// Maximum number of invocations of one fuzz case.
31const MAX_ITERS: usize = 1_000;
32
33/// Generate an arbitrary instance of the specified type.
34fn arbitrary_val(
35    ty: &component::Type,
36    fuel: &mut u32,
37    input: &mut Unstructured,
38) -> arbitrary::Result<Val> {
39    use component::Type;
40
41    *fuel = fuel.saturating_sub(1);
42    Ok(match ty {
43        Type::Bool => Val::Bool(input.arbitrary()?),
44        Type::S8 => Val::S8(input.arbitrary()?),
45        Type::U8 => Val::U8(input.arbitrary()?),
46        Type::S16 => Val::S16(input.arbitrary()?),
47        Type::U16 => Val::U16(input.arbitrary()?),
48        Type::S32 => Val::S32(input.arbitrary()?),
49        Type::U32 => Val::U32(input.arbitrary()?),
50        Type::S64 => Val::S64(input.arbitrary()?),
51        Type::U64 => Val::U64(input.arbitrary()?),
52        Type::Float32 => Val::Float32(input.arbitrary()?),
53        Type::Float64 => Val::Float64(input.arbitrary()?),
54        Type::Char => Val::Char(input.arbitrary()?),
55        Type::String => {
56            let string = if *fuel == 0 {
57                String::new()
58            } else {
59                input.arbitrary()?
60            };
61            *fuel = fuel.saturating_sub(string.len() as u32);
62            Val::String(string)
63        }
64        Type::List(list) => {
65            let mut values = Vec::new();
66            input.arbitrary_loop(Some(MIN_LIST_LENGTH), Some(MAX_LIST_LENGTH), |input| {
67                if *fuel == 0 {
68                    return Ok(ControlFlow::Break(()));
69                }
70                values.push(arbitrary_val(&list.ty(), fuel, input)?);
71
72                Ok(ControlFlow::Continue(()))
73            })?;
74
75            Val::List(values)
76        }
77        Type::Record(record) => Val::Record(
78            record
79                .fields()
80                .map(|field| {
81                    Ok((
82                        field.name.to_string(),
83                        arbitrary_val(&field.ty, fuel, input)?,
84                    ))
85                })
86                .collect::<arbitrary::Result<_>>()?,
87        ),
88        Type::Tuple(tuple) => Val::Tuple(
89            tuple
90                .types()
91                .map(|ty| arbitrary_val(&ty, fuel, input))
92                .collect::<arbitrary::Result<_>>()?,
93        ),
94        Type::Variant(variant) => {
95            let cases = variant.cases().collect::<Vec<_>>();
96            let case = input.choose(&cases)?;
97            let payload = match &case.ty {
98                Some(ty) => Some(Box::new(arbitrary_val(ty, fuel, input)?)),
99                None => None,
100            };
101            Val::Variant(case.name.to_string(), payload)
102        }
103        Type::Enum(en) => {
104            let names = en.names().collect::<Vec<_>>();
105            let name = input.choose(&names)?;
106            Val::Enum(name.to_string())
107        }
108        Type::Option(option) => {
109            let discriminant = input.int_in_range(0..=1)?;
110            Val::Option(match discriminant {
111                0 => None,
112                1 => Some(Box::new(arbitrary_val(&option.ty(), fuel, input)?)),
113                _ => unreachable!(),
114            })
115        }
116        Type::Result(result) => {
117            let discriminant = input.int_in_range(0..=1)?;
118            Val::Result(match discriminant {
119                0 => Ok(match result.ok() {
120                    Some(ty) => Some(Box::new(arbitrary_val(&ty, fuel, input)?)),
121                    None => None,
122                }),
123                1 => Err(match result.err() {
124                    Some(ty) => Some(Box::new(arbitrary_val(&ty, fuel, input)?)),
125                    None => None,
126                }),
127                _ => unreachable!(),
128            })
129        }
130        Type::Flags(flags) => Val::Flags(
131            flags
132                .names()
133                .filter_map(|name| {
134                    input
135                        .arbitrary()
136                        .map(|p| if p { Some(name.to_string()) } else { None })
137                        .transpose()
138                })
139                .collect::<arbitrary::Result<_>>()?,
140        ),
141        Type::FixedLengthList(list) => Val::FixedLengthList(
142            (0..list.len())
143                .map(|_| arbitrary_val(&list.ty(), fuel, input))
144                .collect::<arbitrary::Result<_>>()?,
145        ),
146
147        Type::Map(map) => {
148            let mut pairs = Vec::new();
149            input.arbitrary_loop(Some(MIN_LIST_LENGTH), Some(MAX_LIST_LENGTH), |input| {
150                if *fuel == 0 {
151                    return Ok(ControlFlow::Break(()));
152                }
153                let key = arbitrary_val(&map.key(), fuel, input)?;
154                let value = arbitrary_val(&map.value(), fuel, input)?;
155                pairs.push((key, value));
156                Ok(ControlFlow::Continue(()))
157            })?;
158            Val::Map(pairs)
159        }
160
161        // Resources, futures, streams, and error contexts aren't fuzzed at this time.
162        Type::Own(_) | Type::Borrow(_) | Type::Future(_) | Type::Stream(_) | Type::ErrorContext => {
163            unreachable!()
164        }
165    })
166}
167
168fn store<T>(input: &mut Unstructured<'_>, val: T) -> arbitrary::Result<Store<T>> {
169    crate::init_fuzzing();
170
171    let mut config = input.arbitrary::<generators::Config>()?;
172    config.enable_async(input)?;
173    config.module_config.config.multi_value_enabled = true;
174    config.module_config.config.bulk_memory_enabled = true;
175    config.module_config.config.reference_types_enabled = true;
176    config.module_config.config.max_memories = 2;
177    config.module_config.component_model_async = true;
178    config.module_config.component_model_async_stackful = true;
179    config.module_config.component_model_map = true;
180    if config.wasmtime.compiler_strategy == CompilerStrategy::Winch {
181        config.wasmtime.compiler_strategy = CompilerStrategy::CraneliftNative;
182    }
183
184    fn set_min<T>(a: &mut T, min: T)
185    where
186        T: Ord + Copy,
187    {
188        if *a < min {
189            *a = min;
190        }
191    }
192
193    fn set_opt_min<T>(a: &mut Option<T>, min: T)
194    where
195        T: Ord + Copy,
196    {
197        if let Some(a) = a {
198            set_min(a, min);
199        }
200    }
201
202    const MEMORY_NEEDED: usize = 100 << 20; // 100 MiB
203
204    if let InstanceAllocationStrategy::Pooling(p) = &mut config.wasmtime.strategy {
205        set_min(&mut p.total_component_instances, 5);
206        set_min(&mut p.total_core_instances, 5);
207        set_min(&mut p.total_memories, 2);
208        set_min(&mut p.total_stacks, 4);
209        set_min(&mut p.max_memories_per_component, 2);
210        set_min(&mut p.max_memories_per_module, 2);
211        set_min(&mut p.component_instance_size, 64 << 10);
212        set_min(&mut p.core_instance_size, 64 << 10);
213        p.memory_protection_keys = Enabled::No;
214        p.max_memory_size = MEMORY_NEEDED;
215
216        // The default reservation on 32-bit hosts is a bit too small, so ensure
217        // that the default isn't used.
218        if cfg!(target_pointer_width = "32") {
219            config
220                .wasmtime
221                .memory_config
222                .memory_reservation
223                .get_or_insert(MEMORY_NEEDED as u64);
224        }
225    }
226    set_opt_min(
227        &mut config.wasmtime.memory_config.memory_reservation,
228        MEMORY_NEEDED as u64,
229    );
230
231    // Re-enforce internal consistency after the adjustments above (e.g.
232    // memory_reservation may have been bumped without a corresponding bump
233    // to gc_heap_reservation).
234    config.wasmtime.make_internally_consistent();
235
236    let engine = Engine::new(
237        config
238            .to_wasmtime()
239            .debug_adapter_modules(input.arbitrary()?),
240    )
241    .unwrap();
242    let mut store = Store::new(&engine, val);
243    config.configure_store_epoch_and_fuel(&mut store);
244    Ok(store)
245}
246
247/// Generate zero or more sets of arbitrary argument and result values and execute the test using those
248/// values, asserting that they flow from host-to-guest and guest-to-host unchanged.
249pub fn static_api_test<'a, P, R>(
250    input: &mut Unstructured<'a>,
251    declarations: &Declarations,
252) -> arbitrary::Result<()>
253where
254    P: ComponentNamedList
255        + Lift
256        + Lower
257        + Clone
258        + PartialEq
259        + Debug
260        + Arbitrary<'a>
261        + Send
262        + Sync
263        + 'static,
264    R: ComponentNamedList
265        + Lift
266        + Lower
267        + Clone
268        + PartialEq
269        + Debug
270        + Arbitrary<'a>
271        + Send
272        + Sync
273        + 'static,
274{
275    crate::init_fuzzing();
276
277    let mut store = store::<Box<dyn Any + Send>>(input, Box::new(()))?;
278    let engine = store.engine();
279    let wat = declarations.make_component();
280    let wat = wat.as_bytes();
281    crate::oracles::log_wasm(wat);
282    let component = Component::new(&engine, wat).unwrap();
283    let mut linker = Linker::new(&engine);
284
285    fn host_function<P, R>(
286        cx: StoreContextMut<'_, Box<dyn Any + Send>>,
287        params: P,
288    ) -> wasmtime::Result<R>
289    where
290        P: Debug + PartialEq + 'static,
291        R: Debug + Clone + 'static,
292    {
293        log::trace!("received parameters {params:?}");
294        let data: &(P, R) = cx.data().downcast_ref().unwrap();
295        let (expected_params, result) = data;
296        assert_eq!(params, *expected_params);
297        log::trace!("returning result {result:?}");
298        Ok(result.clone())
299    }
300
301    if declarations.options.host_async {
302        linker
303            .root()
304            .func_wrap_concurrent(IMPORT_FUNCTION, |a, params| {
305                Box::pin(async move {
306                    a.with(|mut cx| host_function::<P, R>(cx.as_context_mut(), params))
307                })
308            })
309            .unwrap();
310    } else {
311        linker
312            .root()
313            .func_wrap(IMPORT_FUNCTION, |cx, params| {
314                host_function::<P, R>(cx, params)
315            })
316            .unwrap();
317    }
318
319    block_on(async {
320        let instance = linker
321            .instantiate_async(&mut store, &component)
322            .await
323            .unwrap();
324        let func = instance
325            .get_typed_func::<P, R>(&mut store, EXPORT_FUNCTION)
326            .unwrap();
327
328        let mut iters = 0..MAX_ITERS;
329        while iters.next().is_some() && input.arbitrary()? {
330            let params = input.arbitrary::<P>()?;
331            let result = input.arbitrary::<R>()?;
332            *store.data_mut() = Box::new((params.clone(), result.clone()));
333            log::trace!("passing in parameters {params:?}");
334            let actual = if declarations.options.guest_caller_async {
335                store
336                    .run_concurrent(async |a| func.call_concurrent(a, params).await.unwrap())
337                    .await
338                    .unwrap()
339            } else {
340                func.call_async(&mut store, params).await.unwrap()
341            };
342            log::trace!("got result {actual:?}");
343            assert_eq!(actual, result);
344        }
345
346        Ok(())
347    })
348}
349
350/// Generate and execute a `crate::generators::component_types::TestCase` using the specified `input` to create
351/// arbitrary types and values.
352pub fn dynamic_component_api_target(input: &mut arbitrary::Unstructured) -> arbitrary::Result<()> {
353    crate::init_fuzzing();
354
355    let mut types = Vec::new();
356    let mut type_fuel = 500;
357
358    for _ in 0..5 {
359        types.push(Type::generate(input, MAX_TYPE_DEPTH, &mut type_fuel)?);
360    }
361
362    let case = TestCase::generate(&types, input)?;
363
364    let mut store = store(input, (Vec::new(), None))?;
365    let engine = store.engine();
366    let wat = case.declarations().make_component();
367    let wat = wat.as_bytes();
368    log_wasm(wat);
369    let component = Component::new(&engine, wat).unwrap();
370    let mut linker = Linker::new(&engine);
371
372    fn host_function(
373        mut cx: StoreContextMut<'_, (Vec<Val>, Option<Vec<Val>>)>,
374        params: &[Val],
375        results: &mut [Val],
376    ) -> Result<()> {
377        log::trace!("received params {params:?}");
378        let (expected_args, expected_results) = cx.data_mut();
379        assert_eq!(params.len(), expected_args.len());
380        for (expected, actual) in expected_args.iter().zip(params) {
381            assert_eq!(expected, actual);
382        }
383        results.clone_from_slice(&expected_results.take().unwrap());
384        log::trace!("returning results {results:?}");
385        Ok(())
386    }
387
388    if case.options.host_async {
389        linker
390            .root()
391            .func_new_concurrent(IMPORT_FUNCTION, {
392                move |cx: &Accessor<_, _>, _, params: &[Val], results: &mut [Val]| {
393                    Box::pin(async move {
394                        cx.with(|mut store| host_function(store.as_context_mut(), params, results))
395                    })
396                }
397            })
398            .unwrap();
399    } else {
400        linker
401            .root()
402            .func_new(IMPORT_FUNCTION, {
403                move |cx, _, params, results| host_function(cx, params, results)
404            })
405            .unwrap();
406    }
407
408    block_on(async {
409        let instance = linker
410            .instantiate_async(&mut store, &component)
411            .await
412            .unwrap();
413        let func = instance.get_func(&mut store, EXPORT_FUNCTION).unwrap();
414        let ty = func.ty(&store);
415
416        let mut iters = 0..MAX_ITERS;
417        while iters.next().is_some() && input.arbitrary()? {
418            let mut fuel = 10_000;
419            let params = ty
420                .params()
421                .map(|(_, ty)| arbitrary_val(&ty, &mut fuel, input))
422                .collect::<arbitrary::Result<Vec<_>>>()?;
423            let results = ty
424                .results()
425                .map(|ty| arbitrary_val(&ty, &mut fuel, input))
426                .collect::<arbitrary::Result<Vec<_>>>()?;
427
428            *store.data_mut() = (params.clone(), Some(results.clone()));
429
430            log::trace!("passing params {params:?}");
431            let mut actual = vec![Val::Bool(false); results.len()];
432            if case.options.guest_caller_async {
433                store
434                    .run_concurrent(async |a| {
435                        func.call_concurrent(a, &params, &mut actual).await.unwrap();
436                    })
437                    .await
438                    .unwrap();
439            } else {
440                func.call_async(&mut store, &params, &mut actual)
441                    .await
442                    .unwrap();
443            }
444            log::trace!("received results {actual:?}");
445            assert_eq!(actual, results);
446        }
447        Ok(())
448    })
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454    use crate::test::test_n_times;
455    use wasmtime_test_util::component_fuzz::{TestCase, Type};
456
457    #[test]
458    fn dynamic_component_api_smoke_test() {
459        test_n_times(50, |(), u| super::dynamic_component_api_target(u));
460    }
461
462    #[test]
463    fn static_api_smoke_test() {
464        test_n_times(10, |(), u| {
465            let mut case = TestCase::generate(&[], u)?;
466            case.params = vec![&Type::S32, &Type::Bool, &Type::String];
467            case.result = Some(&Type::String);
468
469            let declarations = case.declarations();
470            static_api_test::<(i32, bool, String), (String,)>(u, &declarations)
471        });
472    }
473}