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, Clone, Debug)] 35 #[allow(missing_docs)] 36 pub enum ApiCall { 37 ConfigNew, 38 ConfigDebugInfo(bool), 39 ConfigInterruptable(bool), 40 EngineNew, 41 StoreNew, 42 ModuleNew { id: usize, wasm: super::WasmOptTtf }, 43 ModuleDrop { id: usize }, 44 InstanceNew { id: usize, module: usize }, 45 InstanceDrop { id: usize }, 46 CallExportedFunc { instance: usize, nth: usize }, 47 } 48 use ApiCall::*; 49 50 #[derive(Default)] 51 struct Scope { 52 id_counter: usize, 53 54 /// Map from a module id to the predicted amount of rss it will take to 55 /// instantiate. 56 modules: BTreeMap<usize, usize>, 57 58 /// Map from an instance id to the amount of rss it's expected to be using. 59 instances: BTreeMap<usize, usize>, 60 61 /// The rough predicted maximum RSS of executing all of our generated API 62 /// calls thus far. 63 predicted_rss: usize, 64 } 65 66 impl Scope { 67 fn next_id(&mut self) -> usize { 68 let id = self.id_counter; 69 self.id_counter = id + 1; 70 id 71 } 72 } 73 74 /// A sequence of API calls. 75 #[derive(Debug)] 76 pub struct ApiCalls { 77 /// The API calls. 78 pub calls: Vec<ApiCall>, 79 } 80 81 impl Arbitrary for ApiCalls { 82 fn arbitrary(input: &mut Unstructured) -> arbitrary::Result<Self> { 83 crate::init_fuzzing(); 84 85 let swarm = Swarm::arbitrary(input)?; 86 let mut calls = vec![]; 87 88 arbitrary_config(input, &swarm, &mut calls)?; 89 calls.push(EngineNew); 90 calls.push(StoreNew); 91 92 let mut scope = Scope::default(); 93 let max_rss = 1 << 30; // 1GB 94 95 // Total limit on number of API calls we'll generate. This exists to 96 // avoid libFuzzer timeouts. 97 let max_calls = 100; 98 99 for _ in 0..input.arbitrary_len::<ApiCall>()? { 100 if calls.len() > max_calls { 101 break; 102 } 103 104 let mut choices: Vec<fn(_, &mut Scope) -> arbitrary::Result<ApiCall>> = vec![]; 105 106 if swarm.module_new { 107 choices.push(|input, scope| { 108 let id = scope.next_id(); 109 let wasm = super::WasmOptTtf::arbitrary(input)?; 110 let predicted_rss = predict_rss(&wasm.wasm).unwrap_or(0); 111 scope.modules.insert(id, predicted_rss); 112 Ok(ModuleNew { id, wasm }) 113 }); 114 } 115 if swarm.module_drop && !scope.modules.is_empty() { 116 choices.push(|input, scope| { 117 let modules: Vec<_> = scope.modules.keys().collect(); 118 let id = **input.choose(&modules)?; 119 scope.modules.remove(&id); 120 Ok(ModuleDrop { id }) 121 }); 122 } 123 if swarm.instance_new && !scope.modules.is_empty() && scope.predicted_rss < max_rss { 124 choices.push(|input, scope| { 125 let modules: Vec<_> = scope.modules.iter().collect(); 126 let (&module, &predicted_rss) = *input.choose(&modules)?; 127 let id = scope.next_id(); 128 scope.instances.insert(id, predicted_rss); 129 scope.predicted_rss += predicted_rss; 130 Ok(InstanceNew { id, module }) 131 }); 132 } 133 if swarm.instance_drop && !scope.instances.is_empty() { 134 choices.push(|input, scope| { 135 let instances: Vec<_> = scope.instances.iter().collect(); 136 let (&id, &rss) = *input.choose(&instances)?; 137 scope.instances.remove(&id); 138 scope.predicted_rss -= rss; 139 Ok(InstanceDrop { id }) 140 }); 141 } 142 if swarm.call_exported_func && !scope.instances.is_empty() { 143 choices.push(|input, scope| { 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 four bools: 169 // 2 when `swarm.config_debug_info` is true 170 // 2 when `swarm.config_interruptable` is true 171 <(bool, bool, bool, bool) as Arbitrary>::size_hint(depth), 172 ), 173 // We can generate arbitrary `WasmOptTtf` instances, which have 174 // no upper bound on the number of bytes they consume. This sets 175 // the upper bound to `None`. 176 <super::WasmOptTtf as Arbitrary>::size_hint(depth), 177 ) 178 }) 179 } 180 } 181 182 fn arbitrary_config( 183 input: &mut Unstructured, 184 swarm: &Swarm, 185 calls: &mut Vec<ApiCall>, 186 ) -> arbitrary::Result<()> { 187 calls.push(ConfigNew); 188 189 if swarm.config_debug_info && bool::arbitrary(input)? { 190 calls.push(ConfigDebugInfo(bool::arbitrary(input)?)); 191 } 192 193 if swarm.config_interruptable && bool::arbitrary(input)? { 194 calls.push(ConfigInterruptable(bool::arbitrary(input)?)); 195 } 196 197 // TODO: flags, features, and compilation strategy. 198 199 Ok(()) 200 } 201 202 /// Attempt to heuristically predict how much rss instantiating the `wasm` 203 /// provided will take in wasmtime. 204 /// 205 /// The intention of this function is to prevent out-of-memory situations from 206 /// trivially instantiating a bunch of modules. We're basically taking any 207 /// random sequence of fuzz inputs and generating API calls, but if we 208 /// instantiate a million things we'd reasonably expect that to exceed the fuzz 209 /// limit of 2GB because, well, instantiation does take a bit of memory. 210 /// 211 /// This prediction will prevent new instances from being created once we've 212 /// created a bunch of instances. Once instances start being dropped, though, 213 /// it'll free up new slots to start making new instances. 214 fn predict_rss(wasm: &[u8]) -> Result<usize> { 215 let mut prediction = 0; 216 let mut reader = ModuleReader::new(wasm)?; 217 while !reader.eof() { 218 let section = reader.read()?; 219 match section.code { 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 SectionCode::Memory => { 223 for entry in section.get_memory_section_reader()? { 224 let initial = entry?.limits.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 SectionCode::Table => { 232 for entry in section.get_table_section_reader()? { 233 let initial = entry?.limits.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