Path: blob/main/crates/fuzzing/src/generators/stacks.rs
1693 views
//! Generate a Wasm program that keeps track of its current stack frames.1//!2//! We can then compare the stack trace we observe in Wasmtime to what the Wasm3//! program believes its stack should be. Any discrepancies between the two4//! points to a bug in either this test case generator or Wasmtime's stack5//! walker.67use std::mem;89use arbitrary::{Arbitrary, Result, Unstructured};10use wasm_encoder::{Instruction, ValType};1112const MAX_FUNCS: u32 = 20;13const MAX_OPS: usize = 1_000;14const MAX_PARAMS: usize = 10;1516/// Generate a Wasm module that keeps track of its current call stack, to17/// compare to the host.18#[derive(Debug)]19pub struct Stacks {20funcs: Vec<Function>,21inputs: Vec<u8>,22}2324#[derive(Debug, Default)]25struct Function {26ops: Vec<Op>,27params: usize,28results: usize,29}3031#[derive(Debug, Clone, Copy)]32enum Op {33CheckStackInHost,34Call(u32),35CallThroughHost(u32),36ReturnCall(u32),37}3839impl<'a> Arbitrary<'a> for Stacks {40fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self> {41let funcs = Self::arbitrary_funcs(u)?;42let n = u.len().min(200);43let inputs = u.bytes(n)?.to_vec();44Ok(Stacks { funcs, inputs })45}46}4748impl Stacks {49fn arbitrary_funcs(u: &mut Unstructured) -> Result<Vec<Function>> {50// Generate a list of functions first with a number of parameters and51// results. Bodies are generated afterwards.52let nfuncs = u.int_in_range(1..=MAX_FUNCS)?;53let mut funcs = (0..nfuncs)54.map(|_| {55Ok(Function {56ops: Vec::new(), // generated later57params: u.int_in_range(0..=MAX_PARAMS)?,58results: u.int_in_range(0..=MAX_PARAMS)?,59})60})61.collect::<Result<Vec<_>>>()?;62let mut funcs_by_result = vec![Vec::new(); MAX_PARAMS + 1];63for (i, func) in funcs.iter().enumerate() {64funcs_by_result[func.results].push(i as u32);65}6667// Fill in each function body with various instructions/operations now68// that the set of functions is known.69for f in funcs.iter_mut() {70let funcs_with_same_results = &funcs_by_result[f.results];71for _ in 0..u.arbitrary_len::<usize>()?.min(MAX_OPS) {72let op = match u.int_in_range(0..=3)? {730 => Op::CheckStackInHost,741 => Op::Call(u.int_in_range(0..=nfuncs - 1)?),752 => Op::CallThroughHost(u.int_in_range(0..=nfuncs - 1)?),76// This only works if the target function has the same77// number of results, so choose from a different set here.783 => Op::ReturnCall(*u.choose(funcs_with_same_results)?),79_ => unreachable!(),80};81f.ops.push(op);82// once a `return_call` has been generated there's no need to83// generate any more instructions, so fall through to below.84if let Some(Op::ReturnCall(_)) = f.ops.last() {85break;86}87}88}8990Ok(funcs)91}9293/// Get the input values to run the Wasm module with.94pub fn inputs(&self) -> &[u8] {95&self.inputs96}9798/// Get this test case's Wasm module.99///100/// The Wasm module has the following imports:101///102/// * `host.check_stack: [] -> []`: The host can check the Wasm's103/// understanding of its own stack against the host's understanding of the104/// Wasm stack to find discrepancy bugs.105///106/// * `host.call_func: [funcref] -> []`: The host should call the given107/// `funcref`, creating a call stack with multiple sequences of contiguous108/// Wasm frames on the stack like `[..., wasm, host, wasm]`.109///110/// The Wasm module has the following exports:111///112/// * `run: [i32] -> []`: This function should be called with each of the113/// input values to run this generated test case.114///115/// * `get_stack: [] -> [i32 i32]`: Get the pointer and length of the `u32`116/// array of this Wasm's understanding of its stack. This is useful for117/// checking whether the host's view of the stack at a trap matches the118/// Wasm program's understanding.119pub fn wasm(&self) -> Vec<u8> {120let mut module = wasm_encoder::Module::new();121122let mut types = wasm_encoder::TypeSection::new();123124let run_type = types.len();125types126.ty()127.function(vec![wasm_encoder::ValType::I32], vec![]);128129let get_stack_type = types.len();130types.ty().function(131vec![],132vec![wasm_encoder::ValType::I32, wasm_encoder::ValType::I32],133);134135let call_func_type = types.len();136types137.ty()138.function(vec![wasm_encoder::ValType::FUNCREF], vec![]);139140let check_stack_type = types.len();141types.ty().function(vec![], vec![]);142143let func_types_start = types.len();144for func in self.funcs.iter() {145types.ty().function(146vec![ValType::I32; func.params],147vec![ValType::I32; func.results],148);149}150151section(&mut module, types);152153let mut imports = wasm_encoder::ImportSection::new();154let check_stack_func = 0;155imports.import(156"host",157"check_stack",158wasm_encoder::EntityType::Function(check_stack_type),159);160let call_func_func = 1;161imports.import(162"host",163"call_func",164wasm_encoder::EntityType::Function(call_func_type),165);166let num_imported_funcs = 2;167section(&mut module, imports);168169let mut funcs = wasm_encoder::FunctionSection::new();170for (i, _) in self.funcs.iter().enumerate() {171funcs.function(func_types_start + (i as u32));172}173let run_func = funcs.len() + num_imported_funcs;174funcs.function(run_type);175let get_stack_func = funcs.len() + num_imported_funcs;176funcs.function(get_stack_type);177section(&mut module, funcs);178179let mut mems = wasm_encoder::MemorySection::new();180let memory = mems.len();181mems.memory(wasm_encoder::MemoryType {182minimum: 1,183maximum: Some(1),184memory64: false,185shared: false,186page_size_log2: None,187});188section(&mut module, mems);189190let mut globals = wasm_encoder::GlobalSection::new();191let fuel_global = globals.len();192globals.global(193wasm_encoder::GlobalType {194val_type: wasm_encoder::ValType::I32,195mutable: true,196shared: false,197},198&wasm_encoder::ConstExpr::i32_const(0),199);200let stack_len_global = globals.len();201globals.global(202wasm_encoder::GlobalType {203val_type: wasm_encoder::ValType::I32,204mutable: true,205shared: false,206},207&wasm_encoder::ConstExpr::i32_const(0),208);209section(&mut module, globals);210211let mut exports = wasm_encoder::ExportSection::new();212exports.export("run", wasm_encoder::ExportKind::Func, run_func);213exports.export("get_stack", wasm_encoder::ExportKind::Func, get_stack_func);214exports.export("memory", wasm_encoder::ExportKind::Memory, memory);215exports.export("fuel", wasm_encoder::ExportKind::Global, fuel_global);216section(&mut module, exports);217218let mut elems = wasm_encoder::ElementSection::new();219elems.declared(wasm_encoder::Elements::Functions(220(0..num_imported_funcs + u32::try_from(self.funcs.len()).unwrap())221.collect::<Vec<_>>()222.into(),223));224section(&mut module, elems);225226let check_fuel = |body: &mut wasm_encoder::Function| {227// Trap if we are out of fuel.228body.instruction(&Instruction::GlobalGet(fuel_global))229.instruction(&Instruction::I32Eqz)230.instruction(&Instruction::If(wasm_encoder::BlockType::Empty))231.instruction(&Instruction::Unreachable)232.instruction(&Instruction::End);233234// Decrement fuel.235body.instruction(&Instruction::GlobalGet(fuel_global))236.instruction(&Instruction::I32Const(1))237.instruction(&Instruction::I32Sub)238.instruction(&Instruction::GlobalSet(fuel_global));239};240241let push_func_to_stack = |body: &mut wasm_encoder::Function, func: u32| {242// Add this function to our internal stack.243//244// Note that we know our `stack_len_global` can't go beyond memory245// bounds because we limit fuel to at most `u8::MAX` and each stack246// entry is an `i32` and `u8::MAX * size_of(i32)` still fits in one247// Wasm page.248body.instruction(&Instruction::GlobalGet(stack_len_global))249.instruction(&Instruction::I32Const(func as i32))250.instruction(&Instruction::I32Store(wasm_encoder::MemArg {251offset: 0,252align: 0,253memory_index: memory,254}))255.instruction(&Instruction::GlobalGet(stack_len_global))256.instruction(&Instruction::I32Const(mem::size_of::<i32>() as i32))257.instruction(&Instruction::I32Add)258.instruction(&Instruction::GlobalSet(stack_len_global));259};260261let pop_func_from_stack = |body: &mut wasm_encoder::Function| {262// Remove this function from our internal stack.263body.instruction(&Instruction::GlobalGet(stack_len_global))264.instruction(&Instruction::I32Const(mem::size_of::<i32>() as i32))265.instruction(&Instruction::I32Sub)266.instruction(&Instruction::GlobalSet(stack_len_global));267};268269let push_params = |body: &mut wasm_encoder::Function, func: u32| {270let func = &self.funcs[func as usize];271for _ in 0..func.params {272body.instruction(&Instruction::I32Const(0));273}274};275let pop_results = |body: &mut wasm_encoder::Function, func: u32| {276let func = &self.funcs[func as usize];277for _ in 0..func.results {278body.instruction(&Instruction::Drop);279}280};281let push_results = |body: &mut wasm_encoder::Function, func: u32| {282let func = &self.funcs[func as usize];283for _ in 0..func.results {284body.instruction(&Instruction::I32Const(0));285}286};287288let mut code = wasm_encoder::CodeSection::new();289for (func_index, func) in self.funcs.iter().enumerate() {290let mut body = wasm_encoder::Function::new(vec![]);291292push_func_to_stack(293&mut body,294num_imported_funcs + u32::try_from(func_index).unwrap(),295);296check_fuel(&mut body);297298let mut check_fuel_and_pop_at_end = true;299300// Perform our specified operations.301for op in &func.ops {302assert!(check_fuel_and_pop_at_end);303match op {304Op::CheckStackInHost => {305body.instruction(&Instruction::Call(check_stack_func));306}307Op::Call(f) => {308push_params(&mut body, *f);309body.instruction(&Instruction::Call(f + num_imported_funcs));310pop_results(&mut body, *f);311}312Op::CallThroughHost(f) => {313body.instruction(&Instruction::RefFunc(f + num_imported_funcs))314.instruction(&Instruction::Call(call_func_func));315}316317// For a `return_call` preemptively check fuel to possibly318// trap and then pop our function from the in-wasm managed319// stack. After that execute the `return_call` itself.320Op::ReturnCall(f) => {321push_params(&mut body, *f);322check_fuel(&mut body);323pop_func_from_stack(&mut body);324check_fuel_and_pop_at_end = false;325body.instruction(&Instruction::ReturnCall(f + num_imported_funcs));326}327}328}329330// Potentially trap at the end of our function as well, so that we331// exercise the scenario where the Wasm-to-host trampoline332// initialized `last_wasm_exit_sp` et al when calling out to a host333// function, but then we returned back to Wasm and then trapped334// while `last_wasm_exit_sp` et al are still initialized from that335// previous host call.336if check_fuel_and_pop_at_end {337check_fuel(&mut body);338pop_func_from_stack(&mut body);339push_results(&mut body, func_index as u32);340}341342function(&mut code, body);343}344345let mut run_body = wasm_encoder::Function::new(vec![]);346347// Reset the bump pointer for the internal stack (this allows us to348// reuse an instance in the oracle, rather than re-instantiate).349run_body350.instruction(&Instruction::I32Const(0))351.instruction(&Instruction::GlobalSet(stack_len_global));352353// Initialize the fuel global.354run_body355.instruction(&Instruction::LocalGet(0))356.instruction(&Instruction::GlobalSet(fuel_global));357358push_func_to_stack(&mut run_body, run_func);359360// Make sure to check for out-of-fuel in the `run` function as well, so361// that we also capture stack traces with only one frame, not just `run`362// followed by the first locally-defined function and then zero or more363// extra frames.364check_fuel(&mut run_body);365366// Call the first locally defined function.367push_params(&mut run_body, 0);368run_body.instruction(&Instruction::Call(num_imported_funcs));369pop_results(&mut run_body, 0);370371check_fuel(&mut run_body);372pop_func_from_stack(&mut run_body);373374function(&mut code, run_body);375376let mut get_stack_body = wasm_encoder::Function::new(vec![]);377get_stack_body378.instruction(&Instruction::I32Const(0))379.instruction(&Instruction::GlobalGet(stack_len_global));380function(&mut code, get_stack_body);381382section(&mut module, code);383384return module.finish();385386// Helper that defines a section in the module and takes ownership of it387// so that it is dropped and its memory reclaimed after adding it to the388// module.389fn section(module: &mut wasm_encoder::Module, section: impl wasm_encoder::Section) {390module.section(§ion);391}392393// Helper that defines a function body in the code section and takes394// ownership of it so that it is dropped and its memory reclaimed after395// adding it to the module.396fn function(code: &mut wasm_encoder::CodeSection, mut func: wasm_encoder::Function) {397func.instruction(&Instruction::End);398code.function(&func);399}400}401}402403#[cfg(test)]404mod tests {405use super::*;406use rand::prelude::*;407use wasmparser::Validator;408409#[test]410fn stacks_generates_valid_wasm_modules() {411let mut rng = SmallRng::seed_from_u64(0);412let mut buf = vec![0; 2048];413for _ in 0..1024 {414rng.fill_bytes(&mut buf);415let u = Unstructured::new(&buf);416if let Ok(stacks) = Stacks::arbitrary_take_rest(u) {417let wasm = stacks.wasm();418validate(&wasm);419}420}421}422423fn validate(wasm: &[u8]) {424let mut validator = Validator::new();425let err = match validator.validate_all(wasm) {426Ok(_) => return,427Err(e) => e,428};429drop(std::fs::write("test.wasm", wasm));430if let Ok(text) = wasmprinter::print_bytes(wasm) {431drop(std::fs::write("test.wat", &text));432}433panic!("wasm failed to validate: {err}");434}435}436437438