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