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 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 // Calling an exported function of a `wasm-opt -ttf` module tends to 97 // take about 20ms. Limit their number to 100, or ~2s, so that we don't 98 // get too close to our 3s timeout. 99 let max_export_calls = 100; 100 101 for _ in 0..input.arbitrary_len::<ApiCall>()? { 102 let mut choices: Vec<fn(_, &mut Scope) -> arbitrary::Result<ApiCall>> = vec![]; 103 104 if swarm.module_new { 105 choices.push(|input, scope| { 106 let id = scope.next_id(); 107 let wasm = super::WasmOptTtf::arbitrary(input)?; 108 let predicted_rss = predict_rss(&wasm.wasm).unwrap_or(0); 109 scope.modules.insert(id, predicted_rss); 110 Ok(ModuleNew { id, wasm }) 111 }); 112 } 113 if swarm.module_drop && !scope.modules.is_empty() { 114 choices.push(|input, scope| { 115 let modules: Vec<_> = scope.modules.keys().collect(); 116 let id = **input.choose(&modules)?; 117 scope.modules.remove(&id); 118 Ok(ModuleDrop { id }) 119 }); 120 } 121 if swarm.instance_new && !scope.modules.is_empty() && scope.predicted_rss < max_rss { 122 choices.push(|input, scope| { 123 let modules: Vec<_> = scope.modules.iter().collect(); 124 let (&module, &predicted_rss) = *input.choose(&modules)?; 125 let id = scope.next_id(); 126 scope.instances.insert(id, predicted_rss); 127 scope.predicted_rss += predicted_rss; 128 Ok(InstanceNew { id, module }) 129 }); 130 } 131 if swarm.instance_drop && !scope.instances.is_empty() { 132 choices.push(|input, scope| { 133 let instances: Vec<_> = scope.instances.iter().collect(); 134 let (&id, &rss) = *input.choose(&instances)?; 135 scope.instances.remove(&id); 136 scope.predicted_rss -= rss; 137 Ok(InstanceDrop { id }) 138 }); 139 } 140 if swarm.call_exported_func 141 && scope.num_export_calls < max_export_calls 142 && !scope.instances.is_empty() 143 { 144 choices.push(|input, scope| { 145 scope.num_export_calls += 1; 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 two bools when 171 // `swarm.config_debug_info` is true. 172 <(bool, bool) as Arbitrary>::size_hint(depth), 173 ), 174 // We can generate arbitrary `WasmOptTtf` instances, which have 175 // no upper bound on the number of bytes they consume. This sets 176 // the upper bound to `None`. 177 <super::WasmOptTtf as Arbitrary>::size_hint(depth), 178 ) 179 }) 180 } 181 } 182 183 fn arbitrary_config( 184 input: &mut Unstructured, 185 swarm: &Swarm, 186 calls: &mut Vec<ApiCall>, 187 ) -> arbitrary::Result<()> { 188 calls.push(ConfigNew); 189 190 if swarm.config_debug_info && bool::arbitrary(input)? { 191 calls.push(ConfigDebugInfo(bool::arbitrary(input)?)); 192 } 193 194 // TODO: flags, features, and compilation strategy. 195 196 Ok(()) 197 } 198 199 /// Attempt to heuristically predict how much rss instantiating the `wasm` 200 /// provided will take in wasmtime. 201 /// 202 /// The intention of this function is to prevent out-of-memory situations from 203 /// trivially instantiating a bunch of modules. We're basically taking any 204 /// random sequence of fuzz inputs and generating API calls, but if we 205 /// instantiate a million things we'd reasonably expect that to exceed the fuzz 206 /// limit of 2GB because, well, instantiation does take a bit of memory. 207 /// 208 /// This prediction will prevent new instances from being created once we've 209 /// created a bunch of instances. Once instances start being dropped, though, 210 /// it'll free up new slots to start making new instances. 211 fn predict_rss(wasm: &[u8]) -> Result<usize> { 212 let mut prediction = 0; 213 let mut reader = ModuleReader::new(wasm)?; 214 while !reader.eof() { 215 let section = reader.read()?; 216 match section.code { 217 // For each declared memory we'll have to map that all in, so add in 218 // the minimum amount of memory to our predicted rss. 219 SectionCode::Memory => { 220 for entry in section.get_memory_section_reader()? { 221 let initial = entry?.limits.initial as usize; 222 prediction += initial * 64 * 1024; 223 } 224 } 225 226 // We'll need to allocate tables and space for table elements, and 227 // currently this is 3 pointers per table entry. 228 SectionCode::Table => { 229 for entry in section.get_table_section_reader()? { 230 let initial = entry?.limits.initial as usize; 231 prediction += initial * 3 * mem::size_of::<usize>(); 232 } 233 } 234 235 // ... and for now nothing else is counted. If we run into issues 236 // with the fuzzers though we can always try to take into account 237 // more things 238 _ => {} 239 } 240 } 241 Ok(prediction) 242 } 243