From 54aa7205065955fcf89da0ae8dabb5314f7d3056 Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Mon, 11 Apr 2022 14:33:27 -0700 Subject: [PATCH] fuzzing: Refactor `TableOps` fuzz generator to allow GC with refs on the stack (#4016) This makes the generator more similar to `wasm-smith` where it is keeping track of what is on the stack and making choices about what instructions are valid to generate given the current stack state. This should in theory allow the generator to emit GC calls while there are live refs on the stack. Fixes #3917 --- Cargo.lock | 1 + crates/fuzzing/Cargo.toml | 1 + crates/fuzzing/src/generators/table_ops.rs | 389 +++++++++------------ crates/fuzzing/src/oracles.rs | 8 +- 4 files changed, 176 insertions(+), 223 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4031fdc0bf..ba662af8df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3613,6 +3613,7 @@ dependencies = [ "arbitrary", "env_logger 0.8.4", "log", + "rand 0.8.5", "rayon", "target-lexicon", "tempfile", diff --git a/crates/fuzzing/Cargo.toml b/crates/fuzzing/Cargo.toml index 6ddc06c84f..560572c5fa 100644 --- a/crates/fuzzing/Cargo.toml +++ b/crates/fuzzing/Cargo.toml @@ -34,6 +34,7 @@ v8 = "0.41" [dev-dependencies] wat = "1.0.37" +rand = { version = "0.8.0", features = ["small_rng"] } # Only enable the `build-libinterpret` feature when fuzzing is enabled, enabling # commands like `cargo test --workspace` or similar to not need an ocaml diff --git a/crates/fuzzing/src/generators/table_ops.rs b/crates/fuzzing/src/generators/table_ops.rs index 6bea0f8ae1..e2c833889f 100644 --- a/crates/fuzzing/src/generators/table_ops.rs +++ b/crates/fuzzing/src/generators/table_ops.rs @@ -1,7 +1,7 @@ //! Generating series of `table.get` and `table.set` operations. -use arbitrary::Arbitrary; -use std::ops::Range; +use arbitrary::{Arbitrary, Result, Unstructured}; +use std::ops::RangeInclusive; use wasm_encoder::{ CodeSection, EntityType, Export, ExportSection, Function, FunctionSection, GlobalSection, ImportSection, Instruction, Module, TableSection, TableType, TypeSection, ValType, @@ -9,41 +9,20 @@ use wasm_encoder::{ /// A description of a Wasm module that makes a series of `externref` table /// operations. -#[derive(Arbitrary, Debug)] +#[derive(Debug)] pub struct TableOps { - num_params: u8, - num_globals: u8, - table_size: u32, + pub(crate) num_params: u8, + pub(crate) num_globals: u8, + pub(crate) table_size: u32, ops: Vec, } -const NUM_PARAMS_RANGE: Range = 1..10; -const NUM_GLOBALS_RANGE: Range = 1..10; -const TABLE_SIZE_RANGE: Range = 1..100; +const NUM_PARAMS_RANGE: RangeInclusive = 1..=10; +const NUM_GLOBALS_RANGE: RangeInclusive = 1..=10; +const TABLE_SIZE_RANGE: RangeInclusive = 1..=100; const MAX_OPS: usize = 100; impl TableOps { - /// Get the number of parameters this module's "run" function takes. - pub fn num_params(&self) -> u8 { - let num_params = std::cmp::max(self.num_params, NUM_PARAMS_RANGE.start); - let num_params = std::cmp::min(num_params, NUM_PARAMS_RANGE.end); - num_params - } - - /// Get the number of globals this module has. - pub fn num_globals(&self) -> u8 { - let num_globals = std::cmp::max(self.num_globals, NUM_GLOBALS_RANGE.start); - let num_globals = std::cmp::min(num_globals, NUM_GLOBALS_RANGE.end); - num_globals - } - - /// Get the size of the table that this module uses. - pub fn table_size(&self) -> u32 { - let table_size = std::cmp::max(self.table_size, TABLE_SIZE_RANGE.start); - let table_size = std::cmp::min(table_size, TABLE_SIZE_RANGE.end); - table_size - } - /// Serialize this module into a Wasm binary. /// /// The module requires a single import: `(import "" "gc" (func))`. This @@ -74,8 +53,8 @@ impl TableOps { ); // 1: "run" - let mut params: Vec = Vec::with_capacity(self.num_params() as usize); - for _i in 0..self.num_params() { + let mut params: Vec = Vec::with_capacity(self.num_params as usize); + for _i in 0..self.num_params { params.push(ValType::ExternRef); } let results = vec![]; @@ -103,13 +82,13 @@ impl TableOps { let mut tables = TableSection::new(); tables.table(TableType { element_type: ValType::ExternRef, - minimum: self.table_size(), + minimum: self.table_size, maximum: None, }); // Define our globals. let mut globals = GlobalSection::new(); - for _ in 0..self.num_globals() { + for _ in 0..self.num_globals { globals.global( wasm_encoder::GlobalType { val_type: wasm_encoder::ValType::ExternRef, @@ -131,12 +110,12 @@ impl TableOps { let mut func = Function::new(vec![(1, ValType::ExternRef)]); func.instruction(&Instruction::Loop(wasm_encoder::BlockType::Empty)); - for op in self.ops.iter().take(MAX_OPS) { + for op in &self.ops { op.insert( &mut func, - self.num_params() as u32, - self.table_size(), - self.num_globals() as u32, + self.num_params as u32, + self.table_size, + self.num_globals as u32, ); } func.instruction(&Instruction::Br(0)); @@ -159,52 +138,96 @@ impl TableOps { } } -#[derive(Arbitrary, Copy, Clone, Debug)] -pub(crate) enum TableOp { - // `call $gc; drop; drop; drop;` - Gc, +impl<'a> Arbitrary<'a> for TableOps { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let num_params = u.int_in_range(NUM_PARAMS_RANGE)?; + let num_globals = u.int_in_range(NUM_GLOBALS_RANGE)?; + let table_size = u.int_in_range(TABLE_SIZE_RANGE)?; - // `(drop (table.get x))` - Get(i32), + let mut stack = 0; + let mut ops = vec![]; + let mut choices = vec![]; + loop { + let keep_going = ops.len() < MAX_OPS && u.arbitrary().unwrap_or(false); + if !keep_going { + break; + } - // `(drop (global.get i))` - GetGlobal(u32), + ops.push(TableOp::arbitrary(u, &mut stack, &mut choices)?); + } - // `(table.set x (local.get y))` - SetFromParam(i32, u32), + // Drop any extant refs on the stack. + for _ in 0..stack { + ops.push(TableOp::Drop); + } - // `(table.set x (table.get y))` - SetFromGet(i32, i32), + Ok(TableOps { + num_params, + num_globals, + table_size, + ops, + }) + } +} - // `call $make_refs; table.set x; table.set y; table.set z` - SetFromMake(i32, i32, i32), +macro_rules! define_table_ops { + ( + $( + $op:ident $( ( $($imm:ty),* $(,)* ) )? : $params:expr => $results:expr , + )* + ) => { + #[derive(Copy, Clone, Debug)] + pub(crate) enum TableOp { + $( + $op $( ( $($imm),* ) )? , + )* + } - // `(global.set x (local.get y))` - SetGlobalFromParam(u32, u32), + impl TableOp { + fn arbitrary( + u: &mut Unstructured, + stack: &mut u32, + choices: &mut Vec Result>, + ) -> Result { + choices.clear(); - // `(global.set x (table.get y))` - SetGlobalFromGet(u32, i32), + // Add all the choices of valid `TableOp`s we could generate. + $( + #[allow(unused_comparisons)] + if *stack >= $params { + choices.push(|_u, stack| { + *stack = *stack - $params + $results; + Ok(TableOp::$op $( ( $( <$imm>::arbitrary(_u)? ),* ) )? ) + }); + } + )* - // `call $make_refs; global.set x; global.set y; global.set z` - SetGlobalFromMake(u32, u32, u32), + // Choose a table op to insert. + let f = u.choose(&choices)?; + f(u, stack) + } + } + }; +} - // `call $make_refs; drop; drop; drop;` - Make, +define_table_ops! { + Gc : 0 => 3, - // `local.get x; local.get y; local.get z; call $take_refs` - TakeFromParams(u32, u32, u32), + MakeRefs : 0 => 3, + TakeRefs : 3 => 0, - // `table.get x; table.get y; table.get z; call $take_refs` - TakeFromGet(i32, i32, i32), + TableGet(i32) : 0 => 1, + TableSet(i32) : 1 => 0, - // `global.get x; global.get y; global.get z; call $take_refs` - TakeFromGlobalGet(u32, u32, u32), + GlobalGet(u32) : 0 => 1, + GlobalSet(u32) : 1 => 0, - // `call $make_refs; call $take_refs` - TakeFromMake, + LocalGet(u32) : 0 => 1, + LocalSet(u32) : 1 => 0, - // `call $gc; call $take_refs` - TakeFromGc, + Drop : 1 => 0, + + Null : 0 => 1, } impl TableOp { @@ -220,103 +243,45 @@ impl TableOp { let take_refs_func_idx = 1; let make_refs_func_idx = 2; + let scratch_local = num_params; + match self { Self::Gc => { func.instruction(&Instruction::Call(gc_func_idx)); - func.instruction(&Instruction::Drop); - func.instruction(&Instruction::Drop); - func.instruction(&Instruction::Drop); } - Self::Get(x) => { - func.instruction(&Instruction::I32Const(x % table_mod)); - func.instruction(&Instruction::TableGet { table: 0 }); - func.instruction(&Instruction::Drop); - } - Self::SetFromParam(x, y) => { - func.instruction(&Instruction::I32Const(x % table_mod)); - func.instruction(&Instruction::LocalGet(y % num_params)); - func.instruction(&Instruction::TableSet { table: 0 }); - } - Self::SetFromGet(x, y) => { - func.instruction(&Instruction::I32Const(x % table_mod)); - func.instruction(&Instruction::I32Const(y % table_mod)); - func.instruction(&Instruction::TableGet { table: 0 }); - func.instruction(&Instruction::TableSet { table: 0 }); - } - Self::SetFromMake(x, y, z) => { + Self::MakeRefs => { func.instruction(&Instruction::Call(make_refs_func_idx)); - - func.instruction(&Instruction::LocalSet(num_params)); - func.instruction(&Instruction::I32Const(x % table_mod)); - func.instruction(&Instruction::LocalGet(num_params)); - func.instruction(&Instruction::TableSet { table: 0 }); - - func.instruction(&Instruction::LocalSet(num_params)); - func.instruction(&Instruction::I32Const(y % table_mod)); - func.instruction(&Instruction::LocalGet(num_params)); - func.instruction(&Instruction::TableSet { table: 0 }); - - func.instruction(&Instruction::LocalSet(num_params)); - func.instruction(&Instruction::I32Const(z % table_mod)); - func.instruction(&Instruction::LocalGet(num_params)); - func.instruction(&Instruction::TableSet { table: 0 }); } - TableOp::Make => { - func.instruction(&Instruction::Call(make_refs_func_idx)); - func.instruction(&Instruction::Drop); - func.instruction(&Instruction::Drop); - func.instruction(&Instruction::Drop); - } - TableOp::TakeFromParams(x, y, z) => { - func.instruction(&Instruction::LocalGet(x % num_params)); - func.instruction(&Instruction::LocalGet(y % num_params)); - func.instruction(&Instruction::LocalGet(z % num_params)); + Self::TakeRefs => { func.instruction(&Instruction::Call(take_refs_func_idx)); } - TableOp::TakeFromGet(x, y, z) => { + Self::TableGet(x) => { func.instruction(&Instruction::I32Const(x % table_mod)); func.instruction(&Instruction::TableGet { table: 0 }); - - func.instruction(&Instruction::I32Const(y % table_mod)); - func.instruction(&Instruction::TableGet { table: 0 }); - - func.instruction(&Instruction::I32Const(z % table_mod)); - func.instruction(&Instruction::TableGet { table: 0 }); - - func.instruction(&Instruction::Call(take_refs_func_idx)); } - TableOp::TakeFromMake => { - func.instruction(&Instruction::Call(make_refs_func_idx)); - func.instruction(&Instruction::Call(take_refs_func_idx)); + Self::TableSet(x) => { + func.instruction(&Instruction::LocalSet(scratch_local)); + func.instruction(&Instruction::I32Const(x % table_mod)); + func.instruction(&Instruction::LocalGet(scratch_local)); + func.instruction(&Instruction::TableSet { table: 0 }); } - Self::TakeFromGc => { - func.instruction(&Instruction::Call(gc_func_idx)); - func.instruction(&Instruction::Call(take_refs_func_idx)); - } - TableOp::GetGlobal(x) => { + Self::GlobalGet(x) => { func.instruction(&Instruction::GlobalGet(x % num_globals)); - func.instruction(&Instruction::Drop); } - TableOp::SetGlobalFromParam(global, param) => { - func.instruction(&Instruction::LocalGet(param % num_params)); - func.instruction(&Instruction::GlobalSet(global % num_globals)); - } - TableOp::SetGlobalFromGet(global, x) => { - func.instruction(&Instruction::I32Const(x)); - func.instruction(&Instruction::TableGet { table: 0 }); - func.instruction(&Instruction::GlobalSet(global % num_globals)); - } - TableOp::SetGlobalFromMake(x, y, z) => { - func.instruction(&Instruction::Call(make_refs_func_idx)); + Self::GlobalSet(x) => { func.instruction(&Instruction::GlobalSet(x % num_globals)); - func.instruction(&Instruction::GlobalSet(y % num_globals)); - func.instruction(&Instruction::GlobalSet(z % num_globals)); } - TableOp::TakeFromGlobalGet(x, y, z) => { - func.instruction(&Instruction::GlobalGet(x % num_globals)); - func.instruction(&Instruction::GlobalGet(y % num_globals)); - func.instruction(&Instruction::GlobalGet(z % num_globals)); - func.instruction(&Instruction::Call(take_refs_func_idx)); + Self::LocalGet(x) => { + func.instruction(&Instruction::LocalGet(x % num_params)); + } + Self::LocalSet(x) => { + func.instruction(&Instruction::LocalSet(x % num_params)); + } + Self::Drop => { + func.instruction(&Instruction::Drop); + } + Self::Null => { + func.instruction(&Instruction::RefNull(wasm_encoder::ValType::ExternRef)); } } } @@ -325,107 +290,91 @@ impl TableOp { #[cfg(test)] mod tests { use super::*; + use rand::rngs::SmallRng; + use rand::{RngCore, SeedableRng}; + + #[test] + fn test_valid() { + let mut rng = SmallRng::seed_from_u64(0); + let mut buf = vec![0; 2048]; + for _ in 0..1024 { + rng.fill_bytes(&mut buf); + let u = Unstructured::new(&buf); + if let Ok(ops) = TableOps::arbitrary_take_rest(u) { + let wasm = ops.to_wasm_binary(); + let mut validator = + wasmparser::Validator::new_with_features(wasmparser::WasmFeatures { + reference_types: true, + ..Default::default() + }); + let result = validator.validate_all(&wasm); + assert!(result.is_ok()); + } + } + } #[test] fn test_wat_string() { let ops = TableOps { - num_params: 5, - num_globals: 1, + num_params: 10, + num_globals: 10, table_size: 20, ops: vec![ TableOp::Gc, - TableOp::Get(0), - TableOp::SetFromParam(1, 2), - TableOp::SetFromGet(3, 4), - TableOp::SetFromMake(5, 6, 7), - TableOp::Make, - TableOp::TakeFromParams(8, 9, 10), - TableOp::TakeFromGet(11, 12, 13), - TableOp::TakeFromMake, - TableOp::GetGlobal(14), - TableOp::SetGlobalFromParam(15, 16), - TableOp::SetGlobalFromGet(17, 18), - TableOp::SetGlobalFromMake(19, 20, 21), - TableOp::TakeFromGlobalGet(22, 23, 24), + TableOp::MakeRefs, + TableOp::TakeRefs, + TableOp::TableGet(0), + TableOp::TableSet(1), + TableOp::GlobalGet(2), + TableOp::GlobalSet(3), + TableOp::LocalGet(4), + TableOp::LocalSet(5), + TableOp::Drop, + TableOp::Null, ], }; let expected = r#" (module (type (;0;) (func (result externref externref externref))) - (type (;1;) (func (param externref externref externref externref externref))) + (type (;1;) (func (param externref externref externref externref externref externref externref externref externref externref))) (type (;2;) (func (param externref externref externref))) (type (;3;) (func (result externref externref externref))) (import "" "gc" (func (;0;) (type 0))) (import "" "take_refs" (func (;1;) (type 2))) (import "" "make_refs" (func (;2;) (type 3))) - (func (;3;) (type 1) (param externref externref externref externref externref) + (func (;3;) (type 1) (param externref externref externref externref externref externref externref externref externref externref) (local externref) loop ;; label = @1 call 0 - drop - drop - drop + call 2 + call 1 i32.const 0 table.get 0 - drop + local.set 10 i32.const 1 - local.get 2 + local.get 10 table.set 0 - i32.const 3 - i32.const 4 - table.get 0 - table.set 0 - call 2 - local.set 5 - i32.const 5 - local.get 5 - table.set 0 - local.set 5 - i32.const 6 - local.get 5 - table.set 0 - local.set 5 - i32.const 7 - local.get 5 - table.set 0 - call 2 - drop - drop - drop - local.get 3 + global.get 2 + global.set 3 local.get 4 - local.get 0 - call 1 - i32.const 11 - table.get 0 - i32.const 12 - table.get 0 - i32.const 13 - table.get 0 - call 1 - call 2 - call 1 - global.get 0 + local.set 5 drop - local.get 1 - global.set 0 - i32.const 18 - table.get 0 - global.set 0 - call 2 - global.set 0 - global.set 0 - global.set 0 - global.get 0 - global.get 0 - global.get 0 - call 1 + ref.null extern br 0 (;@1;) end ) (table (;0;) 20 externref) (global (;0;) (mut externref) ref.null extern) + (global (;1;) (mut externref) ref.null extern) + (global (;2;) (mut externref) ref.null extern) + (global (;3;) (mut externref) ref.null extern) + (global (;4;) (mut externref) ref.null extern) + (global (;5;) (mut externref) ref.null extern) + (global (;6;) (mut externref) ref.null extern) + (global (;7;) (mut externref) ref.null extern) + (global (;8;) (mut externref) ref.null extern) + (global (;9;) (mut externref) ref.null extern) (export "run" (func 3)) ) "#; diff --git a/crates/fuzzing/src/oracles.rs b/crates/fuzzing/src/oracles.rs index 7d07117c8b..48c082bd0f 100644 --- a/crates/fuzzing/src/oracles.rs +++ b/crates/fuzzing/src/oracles.rs @@ -16,7 +16,6 @@ use crate::generators; use arbitrary::Arbitrary; use log::debug; use std::cell::Cell; -use std::convert::TryInto; use std::rc::Rc; use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; use std::sync::{Arc, Condvar, Mutex}; @@ -548,7 +547,7 @@ pub fn spectest(mut fuzz_config: generators::Config, test: generators::SpecTest) /// Execute a series of `table.get` and `table.set` operations. pub fn table_ops(mut fuzz_config: generators::Config, ops: generators::table_ops::TableOps) { - let expected_drops = Arc::new(AtomicUsize::new(ops.num_params() as usize)); + let expected_drops = Arc::new(AtomicUsize::new(ops.num_params as usize)); let num_dropped = Arc::new(AtomicUsize::new(0)); { @@ -592,6 +591,7 @@ pub fn table_ops(mut fuzz_config: generators::Config, ops: generators::table_ops let num_dropped = num_dropped.clone(); let expected_drops = expected_drops.clone(); move |mut caller: Caller<'_, StoreLimits>, _params, results| { + log::info!("table_ops: GC"); if num_gcs.fetch_add(1, SeqCst) < MAX_GCS { caller.gc(); } @@ -614,6 +614,7 @@ pub fn table_ops(mut fuzz_config: generators::Config, ops: generators::table_ops .func_wrap("", "take_refs", { let expected_drops = expected_drops.clone(); move |a: Option, b: Option, c: Option| { + log::info!("table_ops: take_refs"); // Do the assertion on each ref's inner data, even though it // all points to the same atomic, so that if we happen to // run into a use-after-free bug with one of these refs we @@ -651,6 +652,7 @@ pub fn table_ops(mut fuzz_config: generators::Config, ops: generators::table_ops let num_dropped = num_dropped.clone(); let expected_drops = expected_drops.clone(); move |_caller, _params, results| { + log::info!("table_ops: make_refs"); expected_drops.fetch_add(3, SeqCst); results[0] = Some(ExternRef::new(CountDrops(num_dropped.clone()))).into(); @@ -668,7 +670,7 @@ pub fn table_ops(mut fuzz_config: generators::Config, ops: generators::table_ops let instance = linker.instantiate(&mut store, &module).unwrap(); let run = instance.get_func(&mut store, "run").unwrap(); - let args: Vec<_> = (0..ops.num_params()) + let args: Vec<_> = (0..ops.num_params) .map(|_| Val::ExternRef(Some(ExternRef::new(CountDrops(num_dropped.clone()))))) .collect(); let _ = run.call(&mut store, &args, &mut []);