1 //! Generating sequences of Wasmtime API calls.
2 //!
3 //! We only generate *valid* sequences of API calls. To do this, we keep track
4 //! of what objects we've already created in earlier API calls via the `Scope`
5 //! struct.
6 //!
7 //! To generate even-more-pathological sequences of API calls, we use [swarm
8 //! testing]:
9 //!
10 //! > In swarm testing, the usual practice of potentially including all features
11 //! > in every test case is abandoned. Rather, a large “swarm” of randomly
12 //! > generated configurations, each of which omits some features, is used, with
13 //! > configurations receiving equal resources.
14 //!
15 //! [swarm testing]: https://www.cs.utah.edu/~regehr/papers/swarm12.pdf
16 
17 use arbitrary::{Arbitrary, Unstructured};
18 use std::collections::BTreeMap;
19 use std::mem;
20 use wasmparser::*;
21 
22 #[derive(Arbitrary, Debug)]
23 struct Swarm {
24     config_debug_info: bool,
25     config_interruptable: bool,
26     module_new: bool,
27     module_drop: bool,
28     instance_new: bool,
29     instance_drop: bool,
30     call_exported_func: bool,
31 }
32 
33 /// A call to one of Wasmtime's public APIs.
34 #[derive(Arbitrary, Clone, Debug)]
35 #[allow(missing_docs)]
36 pub enum ApiCall {
37     ConfigNew,
38     ConfigDebugInfo(bool),
39     ConfigInterruptable(bool),
40     EngineNew,
41     StoreNew,
42     ModuleNew { id: usize, wasm: super::WasmOptTtf },
43     ModuleDrop { id: usize },
44     InstanceNew { id: usize, module: usize },
45     InstanceDrop { id: usize },
46     CallExportedFunc { instance: usize, nth: usize },
47 }
48 use ApiCall::*;
49 
50 #[derive(Default)]
51 struct Scope {
52     id_counter: usize,
53 
54     /// Map from a module id to the predicted amount of rss it will take to
55     /// instantiate.
56     modules: BTreeMap<usize, usize>,
57 
58     /// Map from an instance id to the amount of rss it's expected to be using.
59     instances: BTreeMap<usize, usize>,
60 
61     /// The rough predicted maximum RSS of executing all of our generated API
62     /// calls thus far.
63     predicted_rss: usize,
64 }
65 
66 impl Scope {
67     fn next_id(&mut self) -> usize {
68         let id = self.id_counter;
69         self.id_counter = id + 1;
70         id
71     }
72 }
73 
74 /// A sequence of API calls.
75 #[derive(Debug)]
76 pub struct ApiCalls {
77     /// The API calls.
78     pub calls: Vec<ApiCall>,
79 }
80 
81 impl Arbitrary for ApiCalls {
82     fn arbitrary(input: &mut Unstructured) -> arbitrary::Result<Self> {
83         crate::init_fuzzing();
84 
85         let swarm = Swarm::arbitrary(input)?;
86         let mut calls = vec![];
87 
88         arbitrary_config(input, &swarm, &mut calls)?;
89         calls.push(EngineNew);
90         calls.push(StoreNew);
91 
92         let mut scope = Scope::default();
93         let max_rss = 1 << 30; // 1GB
94 
95         // Total limit on number of API calls we'll generate. This exists to
96         // avoid libFuzzer timeouts.
97         let max_calls = 100;
98 
99         for _ in 0..input.arbitrary_len::<ApiCall>()? {
100             if calls.len() > max_calls {
101                 break;
102             }
103 
104             let mut choices: Vec<fn(_, &mut Scope) -> arbitrary::Result<ApiCall>> = vec![];
105 
106             if swarm.module_new {
107                 choices.push(|input, scope| {
108                     let id = scope.next_id();
109                     let wasm = super::WasmOptTtf::arbitrary(input)?;
110                     let predicted_rss = predict_rss(&wasm.wasm).unwrap_or(0);
111                     scope.modules.insert(id, predicted_rss);
112                     Ok(ModuleNew { id, wasm })
113                 });
114             }
115             if swarm.module_drop && !scope.modules.is_empty() {
116                 choices.push(|input, scope| {
117                     let modules: Vec<_> = scope.modules.keys().collect();
118                     let id = **input.choose(&modules)?;
119                     scope.modules.remove(&id);
120                     Ok(ModuleDrop { id })
121                 });
122             }
123             if swarm.instance_new && !scope.modules.is_empty() && scope.predicted_rss < max_rss {
124                 choices.push(|input, scope| {
125                     let modules: Vec<_> = scope.modules.iter().collect();
126                     let (&module, &predicted_rss) = *input.choose(&modules)?;
127                     let id = scope.next_id();
128                     scope.instances.insert(id, predicted_rss);
129                     scope.predicted_rss += predicted_rss;
130                     Ok(InstanceNew { id, module })
131                 });
132             }
133             if swarm.instance_drop && !scope.instances.is_empty() {
134                 choices.push(|input, scope| {
135                     let instances: Vec<_> = scope.instances.iter().collect();
136                     let (&id, &rss) = *input.choose(&instances)?;
137                     scope.instances.remove(&id);
138                     scope.predicted_rss -= rss;
139                     Ok(InstanceDrop { id })
140                 });
141             }
142             if swarm.call_exported_func && !scope.instances.is_empty() {
143                 choices.push(|input, scope| {
144                     let instances: Vec<_> = scope.instances.keys().collect();
145                     let instance = **input.choose(&instances)?;
146                     let nth = usize::arbitrary(input)?;
147                     Ok(CallExportedFunc { instance, nth })
148                 });
149             }
150 
151             if choices.is_empty() {
152                 break;
153             }
154             let c = input.choose(&choices)?;
155             calls.push(c(input, &mut scope)?);
156         }
157 
158         Ok(ApiCalls { calls })
159     }
160 
161     fn size_hint(depth: usize) -> (usize, Option<usize>) {
162         arbitrary::size_hint::recursion_guard(depth, |depth| {
163             arbitrary::size_hint::or(
164                 // This is the stuff we unconditionally need, which affects the
165                 // minimum size.
166                 arbitrary::size_hint::and(
167                     <Swarm as Arbitrary>::size_hint(depth),
168                     // `arbitrary_config` uses four bools:
169                     // 2 when `swarm.config_debug_info` is true
170                     // 2 when `swarm.config_interruptable` is true
171                     <(bool, bool, bool, bool) as Arbitrary>::size_hint(depth),
172                 ),
173                 // We can generate arbitrary `WasmOptTtf` instances, which have
174                 // no upper bound on the number of bytes they consume. This sets
175                 // the upper bound to `None`.
176                 <super::WasmOptTtf as Arbitrary>::size_hint(depth),
177             )
178         })
179     }
180 }
181 
182 fn arbitrary_config(
183     input: &mut Unstructured,
184     swarm: &Swarm,
185     calls: &mut Vec<ApiCall>,
186 ) -> arbitrary::Result<()> {
187     calls.push(ConfigNew);
188 
189     if swarm.config_debug_info && bool::arbitrary(input)? {
190         calls.push(ConfigDebugInfo(bool::arbitrary(input)?));
191     }
192 
193     if swarm.config_interruptable && bool::arbitrary(input)? {
194         calls.push(ConfigInterruptable(bool::arbitrary(input)?));
195     }
196 
197     // TODO: flags, features, and compilation strategy.
198 
199     Ok(())
200 }
201 
202 /// Attempt to heuristically predict how much rss instantiating the `wasm`
203 /// provided will take in wasmtime.
204 ///
205 /// The intention of this function is to prevent out-of-memory situations from
206 /// trivially instantiating a bunch of modules. We're basically taking any
207 /// random sequence of fuzz inputs and generating API calls, but if we
208 /// instantiate a million things we'd reasonably expect that to exceed the fuzz
209 /// limit of 2GB because, well, instantiation does take a bit of memory.
210 ///
211 /// This prediction will prevent new instances from being created once we've
212 /// created a bunch of instances. Once instances start being dropped, though,
213 /// it'll free up new slots to start making new instances.
214 fn predict_rss(wasm: &[u8]) -> Result<usize> {
215     let mut prediction = 0;
216     let mut reader = ModuleReader::new(wasm)?;
217     while !reader.eof() {
218         let section = reader.read()?;
219         match section.code {
220             // For each declared memory we'll have to map that all in, so add in
221             // the minimum amount of memory to our predicted rss.
222             SectionCode::Memory => {
223                 for entry in section.get_memory_section_reader()? {
224                     let initial = entry?.limits.initial as usize;
225                     prediction += initial * 64 * 1024;
226                 }
227             }
228 
229             // We'll need to allocate tables and space for table elements, and
230             // currently this is 3 pointers per table entry.
231             SectionCode::Table => {
232                 for entry in section.get_table_section_reader()? {
233                     let initial = entry?.limits.initial as usize;
234                     prediction += initial * 3 * mem::size_of::<usize>();
235                 }
236             }
237 
238             // ... and for now nothing else is counted. If we run into issues
239             // with the fuzzers though we can always try to take into account
240             // more things
241             _ => {}
242         }
243     }
244     Ok(prediction)
245 }
246