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     module_new: bool,
26     module_drop: bool,
27     instance_new: bool,
28     instance_drop: bool,
29     call_exported_func: bool,
30 }
31 
32 /// A call to one of Wasmtime's public APIs.
33 #[derive(Arbitrary, Clone, Debug)]
34 #[allow(missing_docs)]
35 pub enum ApiCall {
36     ConfigNew,
37     ConfigDebugInfo(bool),
38     EngineNew,
39     StoreNew,
40     ModuleNew { id: usize, wasm: super::WasmOptTtf },
41     ModuleDrop { id: usize },
42     InstanceNew { id: usize, module: usize },
43     InstanceDrop { id: usize },
44     CallExportedFunc { instance: usize, nth: usize },
45 }
46 use ApiCall::*;
47 
48 #[derive(Default)]
49 struct Scope {
50     id_counter: usize,
51 
52     /// Map from a module id to the predicted amount of rss it will take to
53     /// instantiate.
54     modules: BTreeMap<usize, usize>,
55 
56     /// Map from an instance id to the amount of rss it's expected to be using.
57     instances: BTreeMap<usize, usize>,
58 
59     /// The rough predicted maximum RSS of executing all of our generated API
60     /// calls thus far.
61     predicted_rss: usize,
62 
63     /// The number of calls of an exported function from an instance.
64     num_export_calls: 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 Arbitrary for ApiCalls {
83     fn arbitrary(input: &mut Unstructured) -> arbitrary::Result<Self> {
84         let swarm = Swarm::arbitrary(input)?;
85         let mut calls = vec![];
86 
87         arbitrary_config(input, &swarm, &mut calls)?;
88         calls.push(EngineNew);
89         calls.push(StoreNew);
90 
91         let mut scope = Scope::default();
92         let max_rss = 1 << 30; // 1GB
93 
94         // Calling an exported function of a `wasm-opt -ttf` module tends to
95         // take about 20ms. Limit their number to 100, or ~2s, so that we don't
96         // get too close to our 3s timeout.
97         let max_export_calls = 100;
98 
99         for _ in 0..input.arbitrary_len::<ApiCall>()? {
100             let mut choices: Vec<fn(_, &mut Scope) -> arbitrary::Result<ApiCall>> = vec![];
101 
102             if swarm.module_new {
103                 choices.push(|input, scope| {
104                     let id = scope.next_id();
105                     let wasm = super::WasmOptTtf::arbitrary(input)?;
106                     let predicted_rss = predict_rss(&wasm.wasm).unwrap_or(0);
107                     scope.modules.insert(id, predicted_rss);
108                     Ok(ModuleNew { id, wasm })
109                 });
110             }
111             if swarm.module_drop && !scope.modules.is_empty() {
112                 choices.push(|input, scope| {
113                     let modules: Vec<_> = scope.modules.keys().collect();
114                     let id = **input.choose(&modules)?;
115                     scope.modules.remove(&id);
116                     Ok(ModuleDrop { id })
117                 });
118             }
119             if swarm.instance_new && !scope.modules.is_empty() && scope.predicted_rss < max_rss {
120                 choices.push(|input, scope| {
121                     let modules: Vec<_> = scope.modules.iter().collect();
122                     let (&module, &predicted_rss) = *input.choose(&modules)?;
123                     let id = scope.next_id();
124                     scope.instances.insert(id, predicted_rss);
125                     scope.predicted_rss += predicted_rss;
126                     Ok(InstanceNew { id, module })
127                 });
128             }
129             if swarm.instance_drop && !scope.instances.is_empty() {
130                 choices.push(|input, scope| {
131                     let instances: Vec<_> = scope.instances.iter().collect();
132                     let (&id, &rss) = *input.choose(&instances)?;
133                     scope.instances.remove(&id);
134                     scope.predicted_rss -= rss;
135                     Ok(InstanceDrop { id })
136                 });
137             }
138             if swarm.call_exported_func
139                 && scope.num_export_calls < max_export_calls
140                 && !scope.instances.is_empty()
141             {
142                 choices.push(|input, scope| {
143                     scope.num_export_calls += 1;
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 two bools when
169                     // `swarm.config_debug_info` is true.
170                     <(bool, bool) as Arbitrary>::size_hint(depth),
171                 ),
172                 // We can generate arbitrary `WasmOptTtf` instances, which have
173                 // no upper bound on the number of bytes they consume. This sets
174                 // the upper bound to `None`.
175                 <super::WasmOptTtf as Arbitrary>::size_hint(depth),
176             )
177         })
178     }
179 }
180 
181 fn arbitrary_config(
182     input: &mut Unstructured,
183     swarm: &Swarm,
184     calls: &mut Vec<ApiCall>,
185 ) -> arbitrary::Result<()> {
186     calls.push(ConfigNew);
187 
188     if swarm.config_debug_info && bool::arbitrary(input)? {
189         calls.push(ConfigDebugInfo(bool::arbitrary(input)?));
190     }
191 
192     // TODO: flags, features, and compilation strategy.
193 
194     Ok(())
195 }
196 
197 /// Attempt to heuristically predict how much rss instantiating the `wasm`
198 /// provided will take in wasmtime.
199 ///
200 /// The intention of this function is to prevent out-of-memory situations from
201 /// trivially instantiating a bunch of modules. We're basically taking any
202 /// random sequence of fuzz inputs and generating API calls, but if we
203 /// instantiate a million things we'd reasonably expect that to exceed the fuzz
204 /// limit of 2GB because, well, instantiation does take a bit of memory.
205 ///
206 /// This prediction will prevent new instances from being created once we've
207 /// created a bunch of instances. Once instances start being dropped, though,
208 /// it'll free up new slots to start making new instances.
209 fn predict_rss(wasm: &[u8]) -> Result<usize> {
210     let mut prediction = 0;
211     let mut reader = ModuleReader::new(wasm)?;
212     while !reader.eof() {
213         let section = reader.read()?;
214         match section.code {
215             // For each declared memory we'll have to map that all in, so add in
216             // the minimum amount of memory to our predicted rss.
217             SectionCode::Memory => {
218                 for entry in section.get_memory_section_reader()? {
219                     let initial = entry?.limits.initial as usize;
220                     prediction += initial * 64 * 1024;
221                 }
222             }
223 
224             // We'll need to allocate tables and space for table elements, and
225             // currently this is 3 pointers per table entry.
226             SectionCode::Table => {
227                 for entry in section.get_table_section_reader()? {
228                     let initial = entry?.limits.initial as usize;
229                     prediction += initial * 3 * mem::size_of::<usize>();
230                 }
231             }
232 
233             // ... and for now nothing else is counted. If we run into issues
234             // with the fuzzers though we can always try to take into account
235             // more things
236             _ => {}
237         }
238     }
239     Ok(prediction)
240 }
241