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