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