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