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