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 64 impl Scope { 65 fn next_id(&mut self) -> usize { 66 let id = self.id_counter; 67 self.id_counter = id + 1; 68 id 69 } 70 } 71 72 /// A sequence of API calls. 73 #[derive(Debug)] 74 pub struct ApiCalls { 75 /// The API calls. 76 pub calls: Vec<ApiCall>, 77 } 78 79 impl Arbitrary for ApiCalls { 80 fn arbitrary(input: &mut Unstructured) -> arbitrary::Result<Self> { 81 crate::init_fuzzing(); 82 83 let swarm = Swarm::arbitrary(input)?; 84 let mut calls = vec![]; 85 86 arbitrary_config(input, &swarm, &mut calls)?; 87 calls.push(EngineNew); 88 calls.push(StoreNew); 89 90 let mut scope = Scope::default(); 91 let max_rss = 1 << 30; // 1GB 92 93 // Total limit on number of API calls we'll generate. This exists to 94 // avoid libFuzzer timeouts. 95 let max_calls = 100; 96 97 for _ in 0..input.arbitrary_len::<ApiCall>()? { 98 if calls.len() > max_calls { 99 break; 100 } 101 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 && !scope.instances.is_empty() { 141 choices.push(|input, scope| { 142 let instances: Vec<_> = scope.instances.keys().collect(); 143 let instance = **input.choose(&instances)?; 144 let nth = usize::arbitrary(input)?; 145 Ok(CallExportedFunc { instance, nth }) 146 }); 147 } 148 149 if choices.is_empty() { 150 break; 151 } 152 let c = input.choose(&choices)?; 153 calls.push(c(input, &mut scope)?); 154 } 155 156 Ok(ApiCalls { calls }) 157 } 158 159 fn size_hint(depth: usize) -> (usize, Option<usize>) { 160 arbitrary::size_hint::recursion_guard(depth, |depth| { 161 arbitrary::size_hint::or( 162 // This is the stuff we unconditionally need, which affects the 163 // minimum size. 164 arbitrary::size_hint::and( 165 <Swarm as Arbitrary>::size_hint(depth), 166 // `arbitrary_config` uses two bools when 167 // `swarm.config_debug_info` is true. 168 <(bool, bool) as Arbitrary>::size_hint(depth), 169 ), 170 // We can generate arbitrary `WasmOptTtf` instances, which have 171 // no upper bound on the number of bytes they consume. This sets 172 // the upper bound to `None`. 173 <super::WasmOptTtf as Arbitrary>::size_hint(depth), 174 ) 175 }) 176 } 177 } 178 179 fn arbitrary_config( 180 input: &mut Unstructured, 181 swarm: &Swarm, 182 calls: &mut Vec<ApiCall>, 183 ) -> arbitrary::Result<()> { 184 calls.push(ConfigNew); 185 186 if swarm.config_debug_info && bool::arbitrary(input)? { 187 calls.push(ConfigDebugInfo(bool::arbitrary(input)?)); 188 } 189 190 // TODO: flags, features, and compilation strategy. 191 192 Ok(()) 193 } 194 195 /// Attempt to heuristically predict how much rss instantiating the `wasm` 196 /// provided will take in wasmtime. 197 /// 198 /// The intention of this function is to prevent out-of-memory situations from 199 /// trivially instantiating a bunch of modules. We're basically taking any 200 /// random sequence of fuzz inputs and generating API calls, but if we 201 /// instantiate a million things we'd reasonably expect that to exceed the fuzz 202 /// limit of 2GB because, well, instantiation does take a bit of memory. 203 /// 204 /// This prediction will prevent new instances from being created once we've 205 /// created a bunch of instances. Once instances start being dropped, though, 206 /// it'll free up new slots to start making new instances. 207 fn predict_rss(wasm: &[u8]) -> Result<usize> { 208 let mut prediction = 0; 209 let mut reader = ModuleReader::new(wasm)?; 210 while !reader.eof() { 211 let section = reader.read()?; 212 match section.code { 213 // For each declared memory we'll have to map that all in, so add in 214 // the minimum amount of memory to our predicted rss. 215 SectionCode::Memory => { 216 for entry in section.get_memory_section_reader()? { 217 let initial = entry?.limits.initial as usize; 218 prediction += initial * 64 * 1024; 219 } 220 } 221 222 // We'll need to allocate tables and space for table elements, and 223 // currently this is 3 pointers per table entry. 224 SectionCode::Table => { 225 for entry in section.get_table_section_reader()? { 226 let initial = entry?.limits.initial as usize; 227 prediction += initial * 3 * mem::size_of::<usize>(); 228 } 229 } 230 231 // ... and for now nothing else is counted. If we run into issues 232 // with the fuzzers though we can always try to take into account 233 // more things 234 _ => {} 235 } 236 } 237 Ok(prediction) 238 } 239