Skip to content

Commit

Permalink
Add tail-calls, params, and results to stacks fuzzer
Browse files Browse the repository at this point in the history
This commit extends the preexisting `stacks` fuzzer with a few new
features:

* `return_call` instructions are now generated
* functions may have both params/results to exercise logic around stack
  adjustments and how that might affect a stack trace
  • Loading branch information
alexcrichton committed Oct 9, 2024
1 parent e16a6b4 commit 92fa04f
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 39 deletions.
136 changes: 98 additions & 38 deletions crates/fuzzing/src/generators/stacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
use std::mem;

use arbitrary::{Arbitrary, Result, Unstructured};
use wasm_encoder::Instruction;
use wasm_encoder::{Instruction, ValType};

const MAX_FUNCS: usize = 20;
const MAX_FUNCS: u32 = 20;
const MAX_OPS: usize = 1_000;
const MAX_PARAMS: usize = 10;

/// Generate a Wasm module that keeps track of its current call stack, to
/// compare to the host.
Expand All @@ -24,13 +25,16 @@ pub struct Stacks {
#[derive(Debug, Default)]
struct Function {
ops: Vec<Op>,
params: usize,
results: usize,
}

#[derive(Arbitrary, Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy)]
enum Op {
CheckStackInHost,
Call(u32),
CallThroughHost(u32),
ReturnCall(u32),
}

impl<'a> Arbitrary<'a> for Stacks {
Expand All @@ -44,35 +48,44 @@ impl<'a> Arbitrary<'a> for Stacks {

impl Stacks {
fn arbitrary_funcs(u: &mut Unstructured) -> Result<Vec<Function>> {
let mut funcs = vec![Function::default()];

// The indices of functions within `funcs` that we still need to
// generate.
let mut work_list = vec![0];
// Generate a list of functions first with a number of parameters and
// results. Bodies are generated afterwards.
let nfuncs = u.int_in_range(1..=MAX_FUNCS)?;
let mut funcs = (0..nfuncs)
.map(|_| {
Ok(Function {
ops: Vec::new(), // generated later
params: u.int_in_range(0..=MAX_PARAMS)?,
results: u.int_in_range(0..=MAX_PARAMS)?,
})
})
.collect::<Result<Vec<_>>>()?;
let mut funcs_by_result = vec![Vec::new(); MAX_PARAMS + 1];
for (i, func) in funcs.iter().enumerate() {
funcs_by_result[func.results].push(i as u32);
}

while let Some(f) = work_list.pop() {
let mut ops = Vec::with_capacity(u.arbitrary_len::<Op>()?.min(MAX_OPS));
for _ in 0..ops.capacity() {
ops.push(u.arbitrary()?);
}
for op in &mut ops {
match op {
Op::CallThroughHost(idx) | Op::Call(idx) => {
if u.is_empty() || funcs.len() >= MAX_FUNCS || u.ratio(4, 5)? {
// Call an existing function.
*idx = *idx % u32::try_from(funcs.len()).unwrap();
} else {
// Call a new function...
*idx = u32::try_from(funcs.len()).unwrap();
// ...which means we also need to eventually define it.
work_list.push(funcs.len());
funcs.push(Function::default());
}
}
Op::CheckStackInHost => {}
// Fill in each function body with various instructions/operations now
// that the set of functions is known.
for f in funcs.iter_mut() {
let funcs_with_same_results = &funcs_by_result[f.results];
for _ in 0..u.arbitrary_len::<usize>()?.min(MAX_OPS) {
let op = match u.int_in_range(0..=3)? {
0 => Op::CheckStackInHost,
1 => Op::Call(u.int_in_range(0..=nfuncs - 1)?),
2 => Op::CallThroughHost(u.int_in_range(0..=nfuncs - 1)?),
// This only works if the target function has the same
// number of results, so choose from a different set here.
3 => Op::ReturnCall(*u.choose(funcs_with_same_results)?),
_ => unreachable!(),
};
f.ops.push(op);
// once a `return_call` has been generated there's no need to
// generate any more instructions, so fall through to below.
if let Some(Op::ReturnCall(_)) = f.ops.last() {
break;
}
}
funcs[f].ops = ops;
}

Ok(funcs)
Expand Down Expand Up @@ -120,22 +133,30 @@ impl Stacks {
vec![wasm_encoder::ValType::I32, wasm_encoder::ValType::I32],
);

let null_type = types.len();
types.ty().function(vec![], vec![]);

let call_func_type = types.len();
types
.ty()
.function(vec![wasm_encoder::ValType::FUNCREF], vec![]);

let check_stack_type = types.len();
types.ty().function(vec![], vec![]);

let func_types_start = types.len();
for func in self.funcs.iter() {
types.ty().function(
vec![ValType::I32; func.params],
vec![ValType::I32; func.results],
);
}

section(&mut module, types);

let mut imports = wasm_encoder::ImportSection::new();
let check_stack_func = 0;
imports.import(
"host",
"check_stack",
wasm_encoder::EntityType::Function(null_type),
wasm_encoder::EntityType::Function(check_stack_type),
);
let call_func_func = 1;
imports.import(
Expand All @@ -147,8 +168,8 @@ impl Stacks {
section(&mut module, imports);

let mut funcs = wasm_encoder::FunctionSection::new();
for _ in &self.funcs {
funcs.function(null_type);
for (i, _) in self.funcs.iter().enumerate() {
funcs.function(func_types_start + (i as u32));
}
let run_func = funcs.len() + num_imported_funcs;
funcs.function(run_type);
Expand Down Expand Up @@ -246,6 +267,25 @@ impl Stacks {
.instruction(&Instruction::GlobalSet(stack_len_global));
};

let push_params = |body: &mut wasm_encoder::Function, func: u32| {
let func = &self.funcs[func as usize];
for _ in 0..func.params {
body.instruction(&Instruction::I32Const(0));
}
};
let pop_results = |body: &mut wasm_encoder::Function, func: u32| {
let func = &self.funcs[func as usize];
for _ in 0..func.results {
body.instruction(&Instruction::Drop);
}
};
let push_results = |body: &mut wasm_encoder::Function, func: u32| {
let func = &self.funcs[func as usize];
for _ in 0..func.results {
body.instruction(&Instruction::I32Const(0));
}
};

let mut code = wasm_encoder::CodeSection::new();
for (func_index, func) in self.funcs.iter().enumerate() {
let mut body = wasm_encoder::Function::new(vec![]);
Expand All @@ -256,19 +296,35 @@ impl Stacks {
);
check_fuel(&mut body);

let mut check_fuel_and_pop_at_end = true;

// Perform our specified operations.
for op in &func.ops {
assert!(check_fuel_and_pop_at_end);
match op {
Op::CheckStackInHost => {
body.instruction(&Instruction::Call(check_stack_func));
}
Op::Call(f) => {
push_params(&mut body, *f);
body.instruction(&Instruction::Call(f + num_imported_funcs));
pop_results(&mut body, *f);
}
Op::CallThroughHost(f) => {
body.instruction(&Instruction::RefFunc(f + num_imported_funcs))
.instruction(&Instruction::Call(call_func_func));
}

// For a `return_call` preemptively check fuel to possibly
// trap and then pop our function from the in-wasm managed
// stack. After that execute the `return_call` itself.
Op::ReturnCall(f) => {
push_params(&mut body, *f);
check_fuel(&mut body);
pop_func_from_stack(&mut body);
check_fuel_and_pop_at_end = false;
body.instruction(&Instruction::ReturnCall(f + num_imported_funcs));
}
}
}

Expand All @@ -278,9 +334,11 @@ impl Stacks {
// function, but then we returned back to Wasm and then trapped
// while `last_wasm_exit_sp` et al are still initialized from that
// previous host call.
check_fuel(&mut body);

pop_func_from_stack(&mut body);
if check_fuel_and_pop_at_end {
check_fuel(&mut body);
pop_func_from_stack(&mut body);
push_results(&mut body, func_index as u32);
}

function(&mut code, body);
}
Expand All @@ -307,7 +365,9 @@ impl Stacks {
check_fuel(&mut run_body);

// Call the first locally defined function.
push_params(&mut run_body, 0);
run_body.instruction(&Instruction::Call(num_imported_funcs));
pop_results(&mut run_body, 0);

check_fuel(&mut run_body);
pop_func_from_stack(&mut run_body);
Expand Down
5 changes: 4 additions & 1 deletion crates/fuzzing/src/oracles/stacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ pub fn check_stacks(stacks: Stacks) -> usize {
"call_func",
|mut caller: Caller<'_, ()>, f: Option<Func>| {
let f = f.unwrap();
f.call(&mut caller, &[], &mut [])?;
let ty = f.ty(&caller);
let params = vec![Val::I32(0); ty.params().len()];
let mut results = vec![Val::I32(0); ty.results().len()];
f.call(&mut caller, &params, &mut results)?;
Ok(())
},
)
Expand Down

0 comments on commit 92fa04f

Please sign in to comment.