diff --git a/crates/fuzzing/src/generators/gc_ops/mutator.rs b/crates/fuzzing/src/generators/gc_ops/mutator.rs index 581408d38356..9ed63b003e39 100644 --- a/crates/fuzzing/src/generators/gc_ops/mutator.rs +++ b/crates/fuzzing/src/generators/gc_ops/mutator.rs @@ -1,15 +1,6 @@ //! Mutators for the `gc` operations. -use crate::generators::gc_ops::{ - limits::{ - GcOpsLimits, MAX_OPS, MAX_REC_GROUPS_RANGE, MAX_TYPES_RANGE, NUM_GLOBALS_RANGE, - NUM_PARAMS_RANGE, TABLE_SIZE_RANGE, - }, - ops::{GcOp, GcOps}, - types::{RecGroupId, TypeId, Types}, -}; - -use mutatis::mutators as m; +use crate::generators::gc_ops::ops::{GcOp, GcOps}; use mutatis::{Candidates, Context, DefaultMutate, Generate, Mutate, Result as MutResult}; /// A mutator for the gc ops @@ -21,8 +12,7 @@ impl Mutate for GcOpsMutator { if !c.shrink() { c.mutation(|ctx| { if let Some(idx) = ctx.rng().gen_index(ops.ops.len() + 1) { - let stack = ops.abstract_stack_depth(idx); - let (op, _new_stack_size) = GcOp::generate(ctx, &ops, stack)?; + let op = GcOp::generate(ctx, &ops)?; ops.ops.insert(idx, op); } Ok(()) @@ -63,64 +53,12 @@ impl<'a> arbitrary::Arbitrary<'a> for GcOps { } impl Generate for GcOpsMutator { - fn generate(&mut self, ctx: &mut Context) -> MutResult { - let num_params = m::range(NUM_PARAMS_RANGE).generate(ctx)?; - let num_globals = m::range(NUM_GLOBALS_RANGE).generate(ctx)?; - let table_size = m::range(TABLE_SIZE_RANGE).generate(ctx)?; - - let max_rec_groups = m::range(MAX_REC_GROUPS_RANGE).generate(ctx)?; - let max_types = m::range(MAX_TYPES_RANGE).generate(ctx)?; - - let mut ops = GcOps { - limits: GcOpsLimits { - num_params, - num_globals, - table_size, - max_rec_groups, - max_types, - }, - ops: { - let mut v = vec![GcOp::Null(), GcOp::Drop(), GcOp::Gc()]; - if num_params > 0 { - v.push(GcOp::LocalSet(0)); - v.push(GcOp::LocalGet(0)); - } - if num_globals > 0 { - v.push(GcOp::GlobalSet(0)); - v.push(GcOp::GlobalGet(0)); - } - if max_types > 0 { - v.push(GcOp::StructNew(0)); - } - v - }, - types: Types::new(), - }; - - for i in 0..ops.limits.max_rec_groups { - ops.types.insert_rec_group(RecGroupId(i)); - } - - if ops.limits.max_rec_groups > 0 { - for i in 0..ops.limits.max_types { - let tid = TypeId(i); - let gid = RecGroupId(m::range(0..=ops.limits.max_rec_groups - 1).generate(ctx)?); - - ops.types.insert_empty_struct(tid, gid); - } - } - - let mut stack: usize = 0; - - while ops.ops.len() < MAX_OPS { - let (op, new_stack_len) = GcOp::generate(ctx, &ops, stack)?; - ops.ops.push(op); - stack = new_stack_len; - } + fn generate(&mut self, _ctx: &mut Context) -> MutResult { + let mut ops = GcOps::default(); + let mut session = mutatis::Session::new(); - // Drop any leftover refs on the stack. - for _ in 0..stack { - ops.ops.push(GcOp::Drop()); + for _ in 0..64 { + session.mutate(&mut ops)?; } Ok(ops) diff --git a/crates/fuzzing/src/generators/gc_ops/ops.rs b/crates/fuzzing/src/generators/gc_ops/ops.rs index ecaefd376386..ef8cfa5a25ea 100644 --- a/crates/fuzzing/src/generators/gc_ops/ops.rs +++ b/crates/fuzzing/src/generators/gc_ops/ops.rs @@ -1,12 +1,12 @@ //! Operations for the `gc` operations. +use crate::generators::gc_ops::types::StackType; use crate::generators::gc_ops::{ limits::GcOpsLimits, types::{CompositeType, RecGroupId, StructType, TypeId, Types}, }; use mutatis::{Context, Generate, mutators as m}; use serde::{Deserialize, Serialize}; -use smallvec::SmallVec; use std::collections::BTreeMap; use wasm_encoder::{ CodeSection, ConstExpr, EntityType, ExportKind, ExportSection, Function, FunctionSection, @@ -218,19 +218,6 @@ impl GcOps { module.finish() } - /// Computes the abstract stack depth after executing all operations - pub fn abstract_stack_depth(&self, index: usize) -> usize { - debug_assert!(index <= self.ops.len()); - let mut stack: usize = 0; - for op in self.ops.iter().take(index) { - let pop = op.operands_len(); - let push = op.results_len(); - stack = stack.saturating_sub(pop); - stack += push; - } - stack - } - /// Fixes this test case such that it becomes valid. /// /// This is necessary because a random mutation (e.g. removing an op in the @@ -247,7 +234,11 @@ impl GcOps { self.types.fixup(&self.limits); let mut new_ops = Vec::with_capacity(self.ops.len()); - let mut stack = 0; + let mut stack: Vec = Vec::new(); + let num_types = u32::try_from(self.types.type_defs.len()) + .expect("types len should be within u32 range"); + + let mut operand_types = Vec::new(); for mut op in self.ops.iter().copied() { if self.limits.max_types == 0 @@ -268,26 +259,29 @@ impl GcOps { continue; } - op.fixup(&self.limits); + op.fixup(&self.limits, num_types); - let mut temp = SmallVec::<[_; 4]>::new(); + let op = if let Some(op) = op.fixup(&self.limits, num_types) { + op + } else { + continue; + }; - while stack < op.operands_len() { - temp.push(GcOp::Null()); - stack += 1; + operand_types.clear(); + op.operand_types(&mut operand_types); + for ty in &operand_types { + StackType::fixup(*ty, &mut stack, &mut new_ops, num_types); } - temp.push(op); - stack = stack - op.operands_len() + op.results_len(); - - new_ops.extend(temp); + // Finally, emit the op itself (updates stack abstractly) + let mut result_types = Vec::new(); + StackType::emit(op, &mut stack, &mut new_ops, num_types, &mut result_types); } - // Insert drops to balance the final stack state - for _ in 0..stack { + // Balance any leftovers with drops (works for any type) + for _ in 0..stack.len() { new_ops.push(GcOp::Drop()); } - self.ops = new_ops; } @@ -303,79 +297,58 @@ impl GcOps { macro_rules! define_gc_ops { ( $( - $op:ident $( ( $($limit_var:ident : $limit:expr => $ty:ty),* ) )? : $params:expr => $results:expr , + $op:ident + $( ( $($limit_var:ident : $limit:expr => $ty:ty),* ) )? + : [ $($operand:expr),* $(,)? ] => [ $($result:expr),* $(,)? ] , )* ) => { - /// The operations for the `gc` operations. + + /// The operations that can be performed by the `gc` function. #[derive(Copy, Clone, Debug, Serialize, Deserialize)] - pub(crate) enum GcOp { + pub enum GcOp { $( + #[allow(missing_docs, reason = "macro-generated code")] $op ( $( $($ty),* )? ), )* } - /// Names of the operations for testing purposes. #[cfg(test)] - pub const OP_NAMES: &'static[&'static str] = &[ - $( - stringify!($op), - )* + pub(crate) const OP_NAMES: &'static [&'static str] = &[ + $( stringify!($op), )* ]; impl GcOp { #[cfg(test)] - pub fn name(&self) -> &'static str { - match self { - $( - Self::$op (..) => stringify!($op), - )* - } + pub(crate) fn name(&self) -> &'static str { + match self { $( Self::$op(..) => stringify!($op), )* } } - pub fn operands_len(&self) -> usize { + #[allow(unreachable_patterns, reason = "macro-generated code")] + pub(crate) fn operand_types(&self, out: &mut Vec>) { + match self { + Self::TakeTypedStructCall(t) => { + out.push(Some(StackType::Struct(Some(*t)))); + } $( - Self::$op (..) => $params, - )* + Self::$op(..) => { + $( out.push($operand); )* + } + ),* } } - pub fn results_len(&self) -> usize { + #[allow(unreachable_patterns, reason = "macro-generated code")] + pub(crate) fn result_types(&self, out: &mut Vec) { match self { - $( - Self::$op (..) => $results, - )* + Self::StructNew(t) => { + out.push(StackType::Struct(Some(*t))); + } + $( Self::$op(..) => { $( out.push($result); )* }, )* } } - } - $( - #[allow(non_snake_case, reason = "macro-generated code")] - fn $op( - _ctx: &mut mutatis::Context, - _limits: &GcOpsLimits, - stack: usize, - ) -> mutatis::Result<(GcOp, usize)> { - #[allow(unused_comparisons, reason = "macro-generated code")] - { - debug_assert!(stack >= $params); - } - - let op = GcOp::$op( - $($({ - let limit_fn = $limit as fn(&GcOpsLimits) -> $ty; - let limit = (limit_fn)(_limits); - debug_assert!(limit > 0); - m::range(0..=limit - 1).generate(_ctx)? - }),*)? - ); - let new_stack = stack - $params + $results; - Ok((op, new_stack)) - } - )* - - impl GcOp { - fn fixup(&mut self, limits: &GcOpsLimits) { + pub(crate) fn fixup(&mut self, limits: &GcOpsLimits, num_types: u32) -> Option { match self { $( Self::$op( $( $( $limit_var ),* )? ) => { @@ -388,62 +361,102 @@ macro_rules! define_gc_ops { } )* } + match self { + Self::StructNew(t) + | Self::TakeStructCall(t) + | Self::TakeTypedStructCall(t) => { + if num_types == 0 { + return None; + } + *t %= num_types; + } + _ => {} + } + + Some(*self) } + /// Generate an arbitrary op without stack-depth awareness. + /// The fixup pass will make the sequence valid. pub(crate) fn generate( ctx: &mut mutatis::Context, ops: &GcOps, - stack: usize, - ) -> mutatis::Result<(GcOp, usize)> { + ) -> mutatis::Result { let mut valid_choices: Vec< - fn(&mut Context, &GcOpsLimits, usize) -> mutatis::Result<(GcOp, usize)> + fn(&mut Context, &GcOpsLimits) -> mutatis::Result > = vec![]; + $( - #[allow(unused_comparisons, reason = "macro-generated code")] - if stack >= $params $($( - && { - let limit_fn = $limit as fn(&GcOpsLimits) -> $ty; - let limit = (limit_fn)(&ops.limits); - limit > 0 - } - )*)? { - valid_choices.push($op); - } + valid_choices.push($op); )* let f = *ctx.rng() .choose(&valid_choices) .expect("should always have a valid op choice"); - - (f)(ctx, &ops.limits, stack) + (f)(ctx, &ops.limits) } } + + $( + #[allow(non_snake_case, reason = "macro-generated code")] + fn $op( + _ctx: &mut mutatis::Context, + _limits: &GcOpsLimits, + ) -> mutatis::Result { + let op = GcOp::$op( + $($({ + let limit_fn = $limit as fn(&GcOpsLimits) -> $ty; + let limit = (limit_fn)(_limits); + // Generate a value even if limit is 0; fixup will handle it + if limit > 0 { + m::range(0..=limit - 1).generate(_ctx)? + } else { + 0 + } + }),*)? + ); + Ok(op) + } + )* }; } define_gc_ops! { - Gc : 0 => 3, + Gc : [] => [StackType::ExternRef, StackType::ExternRef, StackType::ExternRef], - MakeRefs : 0 => 3, - TakeRefs : 3 => 0, + MakeRefs : [] => [StackType::ExternRef, StackType::ExternRef, StackType::ExternRef], + TakeRefs : [Some(StackType::ExternRef), Some(StackType::ExternRef), Some(StackType::ExternRef)] => [], // Add one to make sure that out of bounds table accesses are possible, but still rare. - TableGet(elem_index: |ops| ops.table_size + 1 => u32) : 0 => 1, - TableSet(elem_index: |ops| ops.table_size + 1 => u32) : 1 => 0, + TableGet(elem_index: |ops| ops.table_size + 1 => u32) + : [] => [StackType::ExternRef], + TableSet(elem_index: |ops| ops.table_size + 1 => u32) + : [Some(StackType::ExternRef)] => [], - GlobalGet(global_index: |ops| ops.num_globals => u32) : 0 => 1, - GlobalSet(global_index: |ops| ops.num_globals => u32) : 1 => 0, + GlobalGet(global_index: |ops| ops.num_globals => u32) + : [] => [StackType::ExternRef], + GlobalSet(global_index: |ops| ops.num_globals => u32) + : [Some(StackType::ExternRef)] => [], - LocalGet(local_index: |ops| ops.num_params => u32) : 0 => 1, - LocalSet(local_index: |ops| ops.num_params => u32) : 1 => 0, + LocalGet(local_index: |ops| ops.num_params => u32) + : [] => [StackType::ExternRef], + LocalSet(local_index: |ops| ops.num_params => u32) + : [Some(StackType::ExternRef)] => [], - StructNew(type_index: |ops| ops.max_types => u32) : 0 => 0, - TakeStructCall(type_index: |ops| ops.max_types => u32) : 1 => 0, - TakeTypedStructCall(type_index: |ops| ops.max_types => u32) : 1 => 0, + // `StructNew` result is special-cased to push `Struct(Some(t))`, so results list is empty. + StructNew(type_index: |ops| ops.max_types => u32) + : [] => [], - Drop : 1 => 0, + TakeStructCall(type_index: |ops| ops.max_types => u32) + : [Some(StackType::Struct(None))] => [], - Null : 0 => 1, + // `TakeTypedStructCall` operand is special-cased to require `Struct(Some(t))`, so operands list is empty. + TakeTypedStructCall(type_index: |ops| ops.max_types => u32) + : [] => [], + + Drop : [None] => [], + + Null : [] => [StackType ::ExternRef], } impl GcOp { @@ -499,16 +512,12 @@ impl GcOp { } Self::StructNew(x) => { func.instruction(&Instruction::StructNew(x + struct_type_base)); - func.instruction(&Instruction::Call(take_structref_idx)); } - Self::TakeStructCall(x) => { - func.instruction(&Instruction::StructNew(x + struct_type_base)); + Self::TakeStructCall(_x) => { func.instruction(&Instruction::Call(take_structref_idx)); } Self::TakeTypedStructCall(x) => { - let s = struct_type_base + x; let f = typed_first_func_index + x; - func.instruction(&Instruction::StructNew(s)); func.instruction(&Instruction::Call(f)); } } diff --git a/crates/fuzzing/src/generators/gc_ops/tests.rs b/crates/fuzzing/src/generators/gc_ops/tests.rs index b5907c0f84c6..f1911de86593 100644 --- a/crates/fuzzing/src/generators/gc_ops/tests.rs +++ b/crates/fuzzing/src/generators/gc_ops/tests.rs @@ -250,7 +250,7 @@ fn test_wat_string() -> mutatis::Result<()> { global.set 0 global.get 0 struct.new 5 - call 3 + drop drop drop drop @@ -297,3 +297,83 @@ fn emits_empty_rec_groups_and_validates() -> mutatis::Result<()> { Ok(()) } + +#[test] +fn fixup_check_types_and_indexes() -> mutatis::Result<()> { + let _ = env_logger::try_init(); + + // Create GcOps with 5 types so that 7 % 5 = 2 + let mut ops = test_ops(5, 5, 5); + ops.limits.max_types = 5; + + // We create max types 5 and out ouf bounds for the type index + ops.ops = vec![ + GcOp::TakeTypedStructCall(27), + GcOp::GlobalSet(0), + GcOp::StructNew(24), + GcOp::LocalSet(0), + ]; + + // Call fixup to resolve dependencies + // this should fix the types by inserting missing types + // also put the indexes in the correct bounds + ops.fixup(); + + // Verify that fixup() + // The expected sequence should be: + // 1. StructNew(_) - inserted by fixup to satisfy TakeTypedStructCall(_) + // 2. TakeTypedStructCall(_) - now has Struct(_) on stack + // 3. Null() - inserted by fixup to satisfy GlobalSet(0) + // 4. GlobalSet(0) - now has ExternRef on stack + // 5. StructNew(_) - produces Struct(_) + // 6. Null() - inserted by fixup to satisfy LocalSet(0) + // 7. LocalSet(0) - now has ExternRef on stack + // 8. Drop() - inserted by fixup to consume ExternRef before Drop() + + // This is the expected sequence in wat format: + // loop ;; label = @1 + // struct.new 7 + // call 6 + // ref.null extern + // global.set 0 + // struct.new 9 + // ref.null extern + // local.set 0 + // drop + // br 0 (;@1;) + // end + + // Find the index of TakeTypedStructCall(_) after fixup + let take_call_idx = ops + .ops + .iter() + .position(|op| matches!(op, GcOp::TakeTypedStructCall(_))) + .expect("TakeTypedStructCall(_) should be present after fixup"); + + // Verify that StructNew(_) appears before TakeTypedStructCall(_) + let struct_new_2_before = ops + .ops + .iter() + .take(take_call_idx) + .any(|op| matches!(op, GcOp::StructNew(_))); + + assert!( + struct_new_2_before, + "fixup should insert StructNew(_) before TakeTypedStructCall(_) to satisfy the dependency" + ); + + // Verify the sequence validates correctly + let wasm = ops.to_wasm_binary(); + let wat = wasmprinter::print_bytes(&wasm).unwrap(); + log::debug!("{wat}"); + let feats = wasmparser::WasmFeatures::default(); + feats.reference_types(); + feats.gc(); + let mut validator = wasmparser::Validator::new_with_features(feats); + assert!( + validator.validate_all(&wasm).is_ok(), + "GC validation should pass after fixup" + ); + + Ok(()) +} diff --git a/crates/fuzzing/src/generators/gc_ops/types.rs b/crates/fuzzing/src/generators/gc_ops/types.rs index 5cd44ae61863..fb51491950f4 100644 --- a/crates/fuzzing/src/generators/gc_ops/types.rs +++ b/crates/fuzzing/src/generators/gc_ops/types.rs @@ -1,6 +1,7 @@ //! Types for the `gc` operations. use crate::generators::gc_ops::limits::GcOpsLimits; +use crate::generators::gc_ops::ops::GcOp; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet}; @@ -88,3 +89,83 @@ impl Types { ); } } + +/// This is used to track the requirements for the operands of an operation. +#[derive(Copy, Clone, Debug)] +pub enum StackType { + /// `externref` + ExternRef, + /// `(ref $*)`; + Struct(Option), +} + +impl StackType { + /// Fixes the stack type to match the given requirement. + pub fn fixup( + req: Option, + stack: &mut Vec, + out: &mut Vec, + num_types: u32, + ) { + let mut result_types = Vec::new(); + match req { + None => { + if stack.is_empty() { + Self::emit(GcOp::Null(), stack, out, num_types, &mut result_types); + } + stack.pop(); // always consume exactly one value + } + Some(Self::ExternRef) => match stack.last() { + Some(Self::ExternRef) => { + stack.pop(); + } + _ => { + Self::emit(GcOp::Null(), stack, out, num_types, &mut result_types); + stack.pop(); // consume just-synthesized externref + } + }, + Some(Self::Struct(wanted)) => { + let ok = match (wanted, stack.last()) { + (Some(wanted), Some(Self::Struct(Some(s)))) => *s == wanted, + (None, Some(Self::Struct(_))) => true, + _ => false, + }; + + if ok { + stack.pop(); + } else { + // Ensure there *is* a struct to consume. + let t = match wanted { + Some(t) => Self::clamp(t, num_types), + None => Self::clamp(0, num_types), + }; + Self::emit(GcOp::StructNew(t), stack, out, num_types, &mut result_types); + stack.pop(); // consume the synthesized struct + } + } + } + } + + pub(crate) fn emit( + op: GcOp, + stack: &mut Vec, + out: &mut Vec, + num_types: u32, + result_types: &mut Vec, + ) { + out.push(op); + result_types.clear(); + op.result_types(result_types); + for ty in result_types { + let clamped_ty = match ty { + Self::Struct(Some(t)) => Self::Struct(Some(Self::clamp(*t, num_types))), + other => *other, + }; + stack.push(clamped_ty); + } + } + + fn clamp(t: u32, n: u32) -> u32 { + if n == 0 { 0 } else { t % n } + } +}