1 //! Generate a Wasm program that keeps track of its current stack frames. 2 //! 3 //! We can then compare the stack trace we observe in Wasmtime to what the Wasm 4 //! program believes its stack should be. Any discrepancies between the two 5 //! points to a bug in either this test case generator or Wasmtime's stack 6 //! walker. 7 8 use std::mem; 9 use std::num::NonZeroUsize; 10 11 use arbitrary::{Arbitrary, Result, Unstructured}; 12 use wasm_encoder::{Instruction, ValType}; 13 14 const MAX_FUNCS: u32 = 20; 15 const MAX_OPS: usize = 1_000; 16 const MAX_PARAMS: usize = 10; 17 18 /// Generate a Wasm module that keeps track of its current call stack, to 19 /// compare to the host. 20 #[derive(Debug)] 21 pub struct Stacks { 22 funcs: Vec<Function>, 23 inputs: Vec<u8>, 24 /// The maximum number of backtrace frames to collect, or `None` to disable 25 /// backtrace collection. 26 pub limit: Option<NonZeroUsize>, 27 } 28 29 #[derive(Debug, Default)] 30 struct Function { 31 ops: Vec<Op>, 32 params: usize, 33 results: usize, 34 } 35 36 #[derive(Debug, Clone, Copy)] 37 enum Op { 38 CheckStackInHost, 39 Call(u32), 40 CallThroughHost(u32), 41 ReturnCall(u32), 42 } 43 44 impl<'a> Arbitrary<'a> for Stacks { arbitrary(u: &mut Unstructured<'a>) -> Result<Self>45 fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self> { 46 let funcs = Self::arbitrary_funcs(u)?; 47 let n = u.len().min(200); 48 let inputs = u.bytes(n)?.to_vec(); 49 let limit = NonZeroUsize::new(u.int_in_range(0..=256)?); 50 Ok(Stacks { 51 funcs, 52 inputs, 53 limit, 54 }) 55 } 56 } 57 58 impl Stacks { arbitrary_funcs(u: &mut Unstructured) -> Result<Vec<Function>>59 fn arbitrary_funcs(u: &mut Unstructured) -> Result<Vec<Function>> { 60 // Generate a list of functions first with a number of parameters and 61 // results. Bodies are generated afterwards. 62 let nfuncs = u.int_in_range(1..=MAX_FUNCS)?; 63 let mut funcs = (0..nfuncs) 64 .map(|_| { 65 Ok(Function { 66 ops: Vec::new(), // generated later 67 params: u.int_in_range(0..=MAX_PARAMS)?, 68 results: u.int_in_range(0..=MAX_PARAMS)?, 69 }) 70 }) 71 .collect::<Result<Vec<_>>>()?; 72 let mut funcs_by_result = vec![Vec::new(); MAX_PARAMS + 1]; 73 for (i, func) in funcs.iter().enumerate() { 74 funcs_by_result[func.results].push(i as u32); 75 } 76 77 // Fill in each function body with various instructions/operations now 78 // that the set of functions is known. 79 for f in funcs.iter_mut() { 80 let funcs_with_same_results = &funcs_by_result[f.results]; 81 for _ in 0..u.arbitrary_len::<usize>()?.min(MAX_OPS) { 82 let op = match u.int_in_range(0..=3)? { 83 0 => Op::CheckStackInHost, 84 1 => Op::Call(u.int_in_range(0..=nfuncs - 1)?), 85 2 => Op::CallThroughHost(u.int_in_range(0..=nfuncs - 1)?), 86 // This only works if the target function has the same 87 // number of results, so choose from a different set here. 88 3 => Op::ReturnCall(*u.choose(funcs_with_same_results)?), 89 _ => unreachable!(), 90 }; 91 f.ops.push(op); 92 // once a `return_call` has been generated there's no need to 93 // generate any more instructions, so fall through to below. 94 if let Some(Op::ReturnCall(_)) = f.ops.last() { 95 break; 96 } 97 } 98 } 99 100 Ok(funcs) 101 } 102 103 /// Get the input values to run the Wasm module with. inputs(&self) -> &[u8]104 pub fn inputs(&self) -> &[u8] { 105 &self.inputs 106 } 107 108 /// Get this test case's Wasm module. 109 /// 110 /// The Wasm module has the following imports: 111 /// 112 /// * `host.check_stack: [] -> []`: The host can check the Wasm's 113 /// understanding of its own stack against the host's understanding of the 114 /// Wasm stack to find discrepancy bugs. 115 /// 116 /// * `host.call_func: [funcref] -> []`: The host should call the given 117 /// `funcref`, creating a call stack with multiple sequences of contiguous 118 /// Wasm frames on the stack like `[..., wasm, host, wasm]`. 119 /// 120 /// The Wasm module has the following exports: 121 /// 122 /// * `run: [i32] -> []`: This function should be called with each of the 123 /// input values to run this generated test case. 124 /// 125 /// * `get_stack: [] -> [i32 i32]`: Get the pointer and length of the `u32` 126 /// array of this Wasm's understanding of its stack. This is useful for 127 /// checking whether the host's view of the stack at a trap matches the 128 /// Wasm program's understanding. wasm(&self) -> Vec<u8>129 pub fn wasm(&self) -> Vec<u8> { 130 let mut module = wasm_encoder::Module::new(); 131 132 let mut types = wasm_encoder::TypeSection::new(); 133 134 let run_type = types.len(); 135 types 136 .ty() 137 .function(vec![wasm_encoder::ValType::I32], vec![]); 138 139 let get_stack_type = types.len(); 140 types.ty().function( 141 vec![], 142 vec![wasm_encoder::ValType::I32, wasm_encoder::ValType::I32], 143 ); 144 145 let call_func_type = types.len(); 146 types 147 .ty() 148 .function(vec![wasm_encoder::ValType::FUNCREF], vec![]); 149 150 let check_stack_type = types.len(); 151 types.ty().function(vec![], vec![]); 152 153 let func_types_start = types.len(); 154 for func in self.funcs.iter() { 155 types.ty().function( 156 vec![ValType::I32; func.params], 157 vec![ValType::I32; func.results], 158 ); 159 } 160 161 section(&mut module, types); 162 163 let mut imports = wasm_encoder::ImportSection::new(); 164 let check_stack_func = 0; 165 imports.import( 166 "host", 167 "check_stack", 168 wasm_encoder::EntityType::Function(check_stack_type), 169 ); 170 let call_func_func = 1; 171 imports.import( 172 "host", 173 "call_func", 174 wasm_encoder::EntityType::Function(call_func_type), 175 ); 176 let num_imported_funcs = 2; 177 section(&mut module, imports); 178 179 let mut funcs = wasm_encoder::FunctionSection::new(); 180 for (i, _) in self.funcs.iter().enumerate() { 181 funcs.function(func_types_start + (i as u32)); 182 } 183 let run_func = funcs.len() + num_imported_funcs; 184 funcs.function(run_type); 185 let get_stack_func = funcs.len() + num_imported_funcs; 186 funcs.function(get_stack_type); 187 section(&mut module, funcs); 188 189 let mut mems = wasm_encoder::MemorySection::new(); 190 let memory = mems.len(); 191 mems.memory(wasm_encoder::MemoryType { 192 minimum: 1, 193 maximum: Some(1), 194 memory64: false, 195 shared: false, 196 page_size_log2: None, 197 }); 198 section(&mut module, mems); 199 200 let mut globals = wasm_encoder::GlobalSection::new(); 201 let fuel_global = globals.len(); 202 globals.global( 203 wasm_encoder::GlobalType { 204 val_type: wasm_encoder::ValType::I32, 205 mutable: true, 206 shared: false, 207 }, 208 &wasm_encoder::ConstExpr::i32_const(0), 209 ); 210 let stack_len_global = globals.len(); 211 globals.global( 212 wasm_encoder::GlobalType { 213 val_type: wasm_encoder::ValType::I32, 214 mutable: true, 215 shared: false, 216 }, 217 &wasm_encoder::ConstExpr::i32_const(0), 218 ); 219 section(&mut module, globals); 220 221 let mut exports = wasm_encoder::ExportSection::new(); 222 exports.export("run", wasm_encoder::ExportKind::Func, run_func); 223 exports.export("get_stack", wasm_encoder::ExportKind::Func, get_stack_func); 224 exports.export("memory", wasm_encoder::ExportKind::Memory, memory); 225 exports.export("fuel", wasm_encoder::ExportKind::Global, fuel_global); 226 section(&mut module, exports); 227 228 let mut elems = wasm_encoder::ElementSection::new(); 229 elems.declared(wasm_encoder::Elements::Functions( 230 (0..num_imported_funcs + u32::try_from(self.funcs.len()).unwrap()) 231 .collect::<Vec<_>>() 232 .into(), 233 )); 234 section(&mut module, elems); 235 236 let check_fuel = |body: &mut wasm_encoder::Function| { 237 // Trap if we are out of fuel. 238 body.instruction(&Instruction::GlobalGet(fuel_global)) 239 .instruction(&Instruction::I32Eqz) 240 .instruction(&Instruction::If(wasm_encoder::BlockType::Empty)) 241 .instruction(&Instruction::Unreachable) 242 .instruction(&Instruction::End); 243 244 // Decrement fuel. 245 body.instruction(&Instruction::GlobalGet(fuel_global)) 246 .instruction(&Instruction::I32Const(1)) 247 .instruction(&Instruction::I32Sub) 248 .instruction(&Instruction::GlobalSet(fuel_global)); 249 }; 250 251 let push_func_to_stack = |body: &mut wasm_encoder::Function, func: u32| { 252 // Add this function to our internal stack. 253 // 254 // Note that we know our `stack_len_global` can't go beyond memory 255 // bounds because we limit fuel to at most `u8::MAX` and each stack 256 // entry is an `i32` and `u8::MAX * size_of(i32)` still fits in one 257 // Wasm page. 258 body.instruction(&Instruction::GlobalGet(stack_len_global)) 259 .instruction(&Instruction::I32Const(func as i32)) 260 .instruction(&Instruction::I32Store(wasm_encoder::MemArg { 261 offset: 0, 262 align: 0, 263 memory_index: memory, 264 })) 265 .instruction(&Instruction::GlobalGet(stack_len_global)) 266 .instruction(&Instruction::I32Const(mem::size_of::<i32>() as i32)) 267 .instruction(&Instruction::I32Add) 268 .instruction(&Instruction::GlobalSet(stack_len_global)); 269 }; 270 271 let pop_func_from_stack = |body: &mut wasm_encoder::Function| { 272 // Remove this function from our internal stack. 273 body.instruction(&Instruction::GlobalGet(stack_len_global)) 274 .instruction(&Instruction::I32Const(mem::size_of::<i32>() as i32)) 275 .instruction(&Instruction::I32Sub) 276 .instruction(&Instruction::GlobalSet(stack_len_global)); 277 }; 278 279 let push_params = |body: &mut wasm_encoder::Function, func: u32| { 280 let func = &self.funcs[func as usize]; 281 for _ in 0..func.params { 282 body.instruction(&Instruction::I32Const(0)); 283 } 284 }; 285 let pop_results = |body: &mut wasm_encoder::Function, func: u32| { 286 let func = &self.funcs[func as usize]; 287 for _ in 0..func.results { 288 body.instruction(&Instruction::Drop); 289 } 290 }; 291 let push_results = |body: &mut wasm_encoder::Function, func: u32| { 292 let func = &self.funcs[func as usize]; 293 for _ in 0..func.results { 294 body.instruction(&Instruction::I32Const(0)); 295 } 296 }; 297 298 let mut code = wasm_encoder::CodeSection::new(); 299 for (func_index, func) in self.funcs.iter().enumerate() { 300 let mut body = wasm_encoder::Function::new(vec![]); 301 302 push_func_to_stack( 303 &mut body, 304 num_imported_funcs + u32::try_from(func_index).unwrap(), 305 ); 306 check_fuel(&mut body); 307 308 let mut check_fuel_and_pop_at_end = true; 309 310 // Perform our specified operations. 311 for op in &func.ops { 312 assert!(check_fuel_and_pop_at_end); 313 match op { 314 Op::CheckStackInHost => { 315 body.instruction(&Instruction::Call(check_stack_func)); 316 } 317 Op::Call(f) => { 318 push_params(&mut body, *f); 319 body.instruction(&Instruction::Call(f + num_imported_funcs)); 320 pop_results(&mut body, *f); 321 } 322 Op::CallThroughHost(f) => { 323 body.instruction(&Instruction::RefFunc(f + num_imported_funcs)) 324 .instruction(&Instruction::Call(call_func_func)); 325 } 326 327 // For a `return_call` preemptively check fuel to possibly 328 // trap and then pop our function from the in-wasm managed 329 // stack. After that execute the `return_call` itself. 330 Op::ReturnCall(f) => { 331 push_params(&mut body, *f); 332 check_fuel(&mut body); 333 pop_func_from_stack(&mut body); 334 check_fuel_and_pop_at_end = false; 335 body.instruction(&Instruction::ReturnCall(f + num_imported_funcs)); 336 } 337 } 338 } 339 340 // Potentially trap at the end of our function as well, so that we 341 // exercise the scenario where the Wasm-to-host trampoline 342 // initialized `last_wasm_exit_sp` et al when calling out to a host 343 // function, but then we returned back to Wasm and then trapped 344 // while `last_wasm_exit_sp` et al are still initialized from that 345 // previous host call. 346 if check_fuel_and_pop_at_end { 347 check_fuel(&mut body); 348 pop_func_from_stack(&mut body); 349 push_results(&mut body, func_index as u32); 350 } 351 352 function(&mut code, body); 353 } 354 355 let mut run_body = wasm_encoder::Function::new(vec![]); 356 357 // Reset the bump pointer for the internal stack (this allows us to 358 // reuse an instance in the oracle, rather than re-instantiate). 359 run_body 360 .instruction(&Instruction::I32Const(0)) 361 .instruction(&Instruction::GlobalSet(stack_len_global)); 362 363 // Initialize the fuel global. 364 run_body 365 .instruction(&Instruction::LocalGet(0)) 366 .instruction(&Instruction::GlobalSet(fuel_global)); 367 368 push_func_to_stack(&mut run_body, run_func); 369 370 // Make sure to check for out-of-fuel in the `run` function as well, so 371 // that we also capture stack traces with only one frame, not just `run` 372 // followed by the first locally-defined function and then zero or more 373 // extra frames. 374 check_fuel(&mut run_body); 375 376 // Call the first locally defined function. 377 push_params(&mut run_body, 0); 378 run_body.instruction(&Instruction::Call(num_imported_funcs)); 379 pop_results(&mut run_body, 0); 380 381 check_fuel(&mut run_body); 382 pop_func_from_stack(&mut run_body); 383 384 function(&mut code, run_body); 385 386 let mut get_stack_body = wasm_encoder::Function::new(vec![]); 387 get_stack_body 388 .instruction(&Instruction::I32Const(0)) 389 .instruction(&Instruction::GlobalGet(stack_len_global)); 390 function(&mut code, get_stack_body); 391 392 section(&mut module, code); 393 394 return module.finish(); 395 396 // Helper that defines a section in the module and takes ownership of it 397 // so that it is dropped and its memory reclaimed after adding it to the 398 // module. 399 fn section(module: &mut wasm_encoder::Module, section: impl wasm_encoder::Section) { 400 module.section(§ion); 401 } 402 403 // Helper that defines a function body in the code section and takes 404 // ownership of it so that it is dropped and its memory reclaimed after 405 // adding it to the module. 406 fn function(code: &mut wasm_encoder::CodeSection, mut func: wasm_encoder::Function) { 407 func.instruction(&Instruction::End); 408 code.function(&func); 409 } 410 } 411 } 412 413 #[cfg(test)] 414 mod tests { 415 use super::*; 416 use wasmparser::Validator; 417 418 #[test] stacks_generates_valid_wasm_modules()419 fn stacks_generates_valid_wasm_modules() { 420 crate::test::test_n_times(10, |stacks: Stacks, _u| { 421 let wasm = stacks.wasm(); 422 validate(&wasm); 423 Ok(()) 424 }) 425 } 426 validate(wasm: &[u8])427 fn validate(wasm: &[u8]) { 428 let mut validator = Validator::new(); 429 let err = match validator.validate_all(wasm) { 430 Ok(_) => return, 431 Err(e) => e, 432 }; 433 drop(std::fs::write("test.wasm", wasm)); 434 if let Ok(text) = wasmprinter::print_bytes(wasm) { 435 drop(std::fs::write("test.wat", &text)); 436 } 437 panic!("wasm failed to validate: {err}"); 438 } 439 } 440