diff --git a/crates/fuzzing/src/generators/config.rs b/crates/fuzzing/src/generators/config.rs index 32e52e114468..cbfb19f1f572 100644 --- a/crates/fuzzing/src/generators/config.rs +++ b/crates/fuzzing/src/generators/config.rs @@ -881,10 +881,12 @@ impl WasmtimeConfig { // must not conflict. Set it to None so the default matches. mcfg.gc_heap_may_move = None; - // Don't let the initial size of a GC heap exceed the maximum - // allowed by the pooling allocator. + // The GC heap rounds its min size up to a wasm page, so clamp the + // initial size to `max_memory_size` rounded *down* to a page. + const WASM_PAGE_SIZE: u64 = 1 << 16; + let cap = (pcfg.max_memory_size as u64) / WASM_PAGE_SIZE * WASM_PAGE_SIZE; if let Some(amt) = mcfg.gc_heap_initial_size { - mcfg.gc_heap_initial_size = Some(amt.min(pcfg.max_memory_size as u64)); + mcfg.gc_heap_initial_size = Some(amt.min(cap)); } } diff --git a/crates/fuzzing/src/generators/gc_ops/limits.rs b/crates/fuzzing/src/generators/gc_ops/limits.rs index 12bdb069508a..79faddafd810 100644 --- a/crates/fuzzing/src/generators/gc_ops/limits.rs +++ b/crates/fuzzing/src/generators/gc_ops/limits.rs @@ -15,6 +15,8 @@ pub const TABLE_SIZE_RANGE: RangeInclusive = 0..=100; pub const MAX_REC_GROUPS_RANGE: RangeInclusive = 0..=10; /// Range for the maximum number of fields per struct type. pub const MAX_FIELDS_RANGE: RangeInclusive = 0..=8; +/// Range for the length of created arrays. +pub const ARRAY_LENGTH_RANGE: RangeInclusive = 1..=16; /// Maximum number of operations. pub const MAX_OPS: usize = 100; @@ -27,6 +29,7 @@ pub struct GcOpsLimits { pub(crate) max_rec_groups: u32, pub(crate) max_types: u32, pub(crate) max_fields: u32, + pub(crate) array_length: u32, } impl Default for GcOpsLimits { @@ -38,6 +41,7 @@ impl Default for GcOpsLimits { max_rec_groups: 5, max_types: 5, max_fields: 5, + array_length: 5, } } } @@ -52,6 +56,7 @@ impl GcOpsLimits { max_rec_groups, max_types, max_fields, + array_length, } = self; let clamp = |limit: &mut u32, range: RangeInclusive| { @@ -63,5 +68,6 @@ impl GcOpsLimits { clamp(max_rec_groups, MAX_REC_GROUPS_RANGE); clamp(max_types, MAX_TYPES_RANGE); clamp(max_fields, MAX_FIELDS_RANGE); + clamp(array_length, ARRAY_LENGTH_RANGE); } } diff --git a/crates/fuzzing/src/generators/gc_ops/mutator.rs b/crates/fuzzing/src/generators/gc_ops/mutator.rs index bf16b1b4a619..5da1634ad424 100644 --- a/crates/fuzzing/src/generators/gc_ops/mutator.rs +++ b/crates/fuzzing/src/generators/gc_ops/mutator.rs @@ -1,7 +1,9 @@ //! Mutators for the `gc` operations. use crate::generators::gc_ops::limits::GcOpsLimits; use crate::generators::gc_ops::ops::{GcOp, GcOps}; -use crate::generators::gc_ops::types::{CompositeType, FieldType, StructField, TypeId, Types}; +use crate::generators::gc_ops::types::{ + ArrayType, CompositeType, FieldType, StructField, TypeId, Types, +}; use mutatis::{ Candidates, Context, DefaultMutate, Generate, Mutate, Result as MutResult, mutators as m, }; @@ -50,7 +52,7 @@ impl TypesMutator { } else { None }; - // Add struct with no fields; fields can be added later by `mutate_struct_fields`. + // Add struct with no fields; fields can be added later by `mutate_type_members`. types.insert_struct(tid, gid, is_final, supertype, Vec::new()); log::debug!("Added struct {tid:?} to rec group {gid:?}"); Ok(()) @@ -58,6 +60,62 @@ impl TypesMutator { Ok(()) } + /// Add an array type with a random element to a random existing rec group, + /// or create a rec group when there are none (if `limits` allow). + fn add_array( + &mut self, + c: &mut Candidates<'_>, + types: &mut Types, + limits: &GcOpsLimits, + ) -> mutatis::Result<()> { + if c.shrink() || types.type_defs.len() >= usize::try_from(limits.max_types).unwrap() { + return Ok(()); + } + + let max_rec_groups = usize::try_from(limits.max_rec_groups).unwrap(); + if types.rec_groups.is_empty() && max_rec_groups == 0 { + return Ok(()); + } + + c.mutation(|ctx| { + let gid = if types.rec_groups.is_empty() { + let new_gid = types.fresh_rec_group_id(ctx.rng()); + types.insert_rec_group(new_gid); + new_gid + } else { + let Some(gid) = ctx.rng().choose(types.rec_groups.keys()).copied() else { + return Ok(()); + }; + gid + }; + + let tid = types.fresh_type_id(ctx.rng()); + let is_final = (ctx.rng().gen_u32() % 4) == 0; + let supertype = if (ctx.rng().gen_u32() % 4) == 0 { + ctx.rng().choose(types.type_defs.keys()).copied() + } else { + None + }; + + // Arrays always have exactly one element, so generate it now. + let candidates: Vec = types.type_defs.keys().copied().collect(); + let element = StructField { + field_type: FieldType::generate(ctx.rng(), &candidates), + mutable: (ctx.rng().gen_u32() % 2) == 0, + }; + types.insert_type( + tid, + gid, + is_final, + supertype, + CompositeType::Array(ArrayType { element }), + ); + log::debug!("Added array {tid:?} to rec group {gid:?}"); + Ok(()) + })?; + Ok(()) + } + /// Remove a random type from its rec group. fn remove_struct(&mut self, c: &mut Candidates<'_>, types: &mut Types) -> mutatis::Result<()> { if types.type_defs.is_empty() { @@ -213,14 +271,19 @@ impl TypesMutator { return Ok(()); } - // Collect (TypeId, is_final, supertype, fields) for members of the source group. - let members: SmallVec<[(TypeId, bool, Option, Vec); 32]> = + // Collect (TypeId, is_final, supertype, composite_type) for members + // of the source group (works for both structs and arrays). + let members: SmallVec<[(TypeId, bool, Option, CompositeType); 32]> = src_members .iter() .filter_map(|tid| { types.type_defs.get(tid).map(|def| { - let CompositeType::Struct(ref st) = def.composite_type; - (*tid, def.is_final, def.supertype, st.fields.clone()) + ( + *tid, + def.is_final, + def.supertype, + def.composite_type.clone(), + ) }) }) .collect(); @@ -240,10 +303,16 @@ impl TypesMutator { } // Insert duplicated defs, rewriting intra-group supertype edges to cloned ids. - for (old_tid, is_final, supertype, fields) in &members { + for (old_tid, is_final, supertype, composite_type) in &members { let new_tid = old_to_new[old_tid]; let mapped_super = supertype.map(|st| *old_to_new.get(&st).unwrap_or(&st)); - types.insert_struct(new_tid, new_gid, *is_final, mapped_super, fields.clone()); + types.insert_type( + new_tid, + new_gid, + *is_final, + mapped_super, + composite_type.clone(), + ); } log::debug!( @@ -385,8 +454,8 @@ impl TypesMutator { Ok(()) } - /// Mutate struct fields (add/remove/modify via `m::vec`). - fn mutate_struct_fields( + /// Mutate struct fields (add/remove/modify via `m::vec`) and array elements. + fn mutate_type_members( &mut self, c: &mut Candidates<'_>, types: &mut Types, @@ -394,11 +463,20 @@ impl TypesMutator { // Snapshot target types up front so fields can reference any type (incl. self) without borrowing `types`. let candidates: Vec = types.type_defs.keys().copied().collect(); for (_, def) in types.type_defs.iter_mut() { - let CompositeType::Struct(ref mut st) = def.composite_type; - m::vec(StructFieldMutator { - candidates: &candidates, - }) - .mutate(c, &mut st.fields)?; + match &mut def.composite_type { + CompositeType::Struct(st) => { + m::vec(StructFieldMutator { + candidates: &candidates, + }) + .mutate(c, &mut st.fields)?; + } + CompositeType::Array(at) => { + StructFieldMutator { + candidates: &candidates, + } + .mutate(c, &mut at.element)?; + } + } } Ok(()) } @@ -411,6 +489,7 @@ impl TypesMutator { limits: &GcOpsLimits, ) -> mutatis::Result<()> { self.add_struct(c, types, limits)?; + self.add_array(c, types, limits)?; self.remove_struct(c, types)?; self.swap_within_group(c, types)?; self.move_between_groups(c, types)?; @@ -418,7 +497,7 @@ impl TypesMutator { self.remove_group(c, types)?; self.merge_groups(c, types)?; self.split_group(c, types, limits)?; - self.mutate_struct_fields(c, types)?; + self.mutate_type_members(c, types)?; Ok(()) } diff --git a/crates/fuzzing/src/generators/gc_ops/ops.rs b/crates/fuzzing/src/generators/gc_ops/ops.rs index fd9257463dfe..44cbda1673c0 100644 --- a/crates/fuzzing/src/generators/gc_ops/ops.rs +++ b/crates/fuzzing/src/generators/gc_ops/ops.rs @@ -13,6 +13,16 @@ use wasm_encoder::{ TypeSection, ValType, }; +/// Pick a same-kind concrete type index for `raw`, or `None` when there are no +/// types of that kind (so the caller drops the op). +fn pick_type_index(indices: &[u32], raw: u32) -> Option { + if indices.is_empty() { + None + } else { + Some(indices[usize::try_from(raw).unwrap() % indices.len()]) + } +} + /// The base offsets and indices for various Wasm entities within /// their index spaces in the the encoded Wasm binary. #[derive(Clone, Copy)] @@ -22,15 +32,19 @@ struct WasmEncodingBases { struct_local_idx: u32, eq_local_idx: u32, i31_local_idx: u32, + array_local_idx: u32, typed_local_base: u32, struct_global_idx: u32, eq_global_idx: u32, i31_global_idx: u32, + array_global_idx: u32, typed_global_base: u32, struct_table_idx: u32, eq_table_idx: u32, i31_table_idx: u32, + array_table_idx: u32, typed_table_base: u32, + array_length: u32, } /// A description of a Wasm module that makes a series of `externref` table @@ -132,6 +146,19 @@ impl GcOps { vec![], ); + // 7: `take_array` + let take_array_type_idx = types.len(); + types.ty().function( + vec![ValType::Ref(RefType { + nullable: true, + heap_type: wasm_encoder::HeapType::Abstract { + shared: false, + ty: wasm_encoder::AbstractHeapType::Array, + }, + })], + vec![], + ); + let struct_type_base: u32 = types.len(); // Build the type-id-to-wasm-index map from the pre-computed @@ -146,9 +173,16 @@ impl GcOps { } } + // Flat encoding order (dense index -> TypeId), used below for naming the + // typed host imports and, later, for field lookups during encoding. + let encoding_order: Vec = encoding_order_grouped + .iter() + .flat_map(|(_, members)| members.iter().copied()) + .collect(); + let encode_ty_id = |ty_id: &TypeId| -> wasm_encoder::SubType { let def = &self.types.type_defs[ty_id]; - match &def.composite_type { + let inner = match &def.composite_type { CompositeType::Struct(st) => { let fields: Box<[wasm_encoder::FieldType]> = st .fields @@ -158,35 +192,41 @@ impl GcOps { mutable: f.mutable, }) .collect(); - wasm_encoder::SubType { - is_final: def.is_final, - supertype_idx: def.supertype.map(|st| type_ids_to_index[&st]), - composite_type: wasm_encoder::CompositeType { - inner: wasm_encoder::CompositeInnerType::Struct( - wasm_encoder::StructType { fields }, - ), - shared: false, - describes: None, - descriptor: None, - }, - } + wasm_encoder::CompositeInnerType::Struct(wasm_encoder::StructType { fields }) + } + CompositeType::Array(at) => { + let element = wasm_encoder::FieldType { + element_type: at.element.field_type.to_storage_type(&type_ids_to_index), + mutable: at.element.mutable, + }; + wasm_encoder::CompositeInnerType::Array(wasm_encoder::ArrayType(element)) } + }; + wasm_encoder::SubType { + is_final: def.is_final, + supertype_idx: def.supertype.map(|st| type_ids_to_index[&st]), + composite_type: wasm_encoder::CompositeType { + inner, + shared: false, + describes: None, + descriptor: None, + }, } }; - let mut struct_count = 0; + let mut concrete_count = 0; // Emit rec groups in the pre-computed order. for (_, group_members) in &encoding_order_grouped { let members: Vec = group_members.iter().map(encode_ty_id).collect(); types.ty().rec(members); - struct_count += u32::try_from(group_members.len()).unwrap(); + concrete_count += u32::try_from(group_members.len()).unwrap(); } - let typed_fn_type_base: u32 = struct_type_base + struct_count; + let typed_fn_type_base: u32 = struct_type_base + concrete_count; - for i in 0..struct_count { + for i in 0..concrete_count { let concrete = struct_type_base + i; types.ty().function( vec![ValType::Ref(RefType { @@ -205,14 +245,26 @@ impl GcOps { imports.import("", "take_struct", EntityType::Function(4)); imports.import("", "take_eq", EntityType::Function(5)); imports.import("", "take_i31", EntityType::Function(take_i31_type_idx)); + imports.import("", "take_array", EntityType::Function(take_array_type_idx)); - // For each of our concrete struct types, define a function - // import that takes an argument of that concrete type. + // For each of our concrete struct/array types, define a function + // import that takes an argument of that concrete type. The import name + // records the kind so the host can define it appropriately. let typed_first_func_index: u32 = imports.len(); - for i in 0..struct_count { + for i in 0..concrete_count { let ty_idx = typed_fn_type_base + i; - let name = format!("take_struct_{}", struct_type_base + i); + let wasm_idx = struct_type_base + i; + let is_array = encoding_order + .get(usize::try_from(i).unwrap()) + .and_then(|tid| self.types.type_defs.get(tid)) + .map(|def| def.composite_type.is_array()) + .unwrap_or(false); + let name = if is_array { + format!("take_array_{wasm_idx}") + } else { + format!("take_struct_{wasm_idx}") + }; imports.import("", &name, EntityType::Function(ty_idx)); } @@ -259,8 +311,23 @@ impl GcOps { shared: false, }); + let array_table_idx = tables.len(); + tables.table(TableType { + element_type: RefType { + nullable: true, + heap_type: wasm_encoder::HeapType::Abstract { + shared: false, + ty: wasm_encoder::AbstractHeapType::Array, + }, + }, + minimum: u64::from(self.limits.table_size), + maximum: None, + table64: false, + shared: false, + }); + let typed_table_base = tables.len(); - for i in 0..struct_count { + for i in 0..concrete_count { let concrete = struct_type_base + i; tables.table(TableType { element_type: RefType { @@ -332,9 +399,29 @@ impl GcOps { &ConstExpr::ref_null(wasm_encoder::HeapType::I31), ); - // Add one typed (ref ) global per struct type. + // Add exactly one (ref.null array) global. + let array_global_idx = globals.len(); + globals.global( + wasm_encoder::GlobalType { + val_type: ValType::Ref(RefType { + nullable: true, + heap_type: wasm_encoder::HeapType::Abstract { + shared: false, + ty: wasm_encoder::AbstractHeapType::Array, + }, + }), + mutable: true, + shared: false, + }, + &ConstExpr::ref_null(wasm_encoder::HeapType::Abstract { + shared: false, + ty: wasm_encoder::AbstractHeapType::Array, + }), + ); + + // Add one typed (ref ) global per struct/array type. let typed_global_base = globals.len(); - for i in 0..struct_count { + for i in 0..concrete_count { let concrete = struct_type_base + i; globals.global( wasm_encoder::GlobalType { @@ -381,8 +468,20 @@ impl GcOps { let i31_local_idx = eq_local_idx + 1; local_decls.push((1, ValType::Ref(RefType::I31REF))); - let typed_local_base: u32 = i31_local_idx + 1; - for i in 0..struct_count { + let array_local_idx = i31_local_idx + 1; + local_decls.push(( + 1, + ValType::Ref(RefType { + nullable: true, + heap_type: wasm_encoder::HeapType::Abstract { + shared: false, + ty: wasm_encoder::AbstractHeapType::Array, + }, + }), + )); + + let typed_local_base: u32 = array_local_idx + 1; + for i in 0..concrete_count { let concrete = struct_type_base + i; local_decls.push(( 1, @@ -399,23 +498,21 @@ impl GcOps { struct_local_idx, eq_local_idx, i31_local_idx, + array_local_idx, typed_local_base, struct_global_idx, eq_global_idx, i31_global_idx, + array_global_idx, typed_global_base, struct_table_idx, eq_table_idx, i31_table_idx, + array_table_idx, typed_table_base, + array_length: self.limits.array_length, }; - // Build a flat encoding order for field lookups during encoding. - let encoding_order: Vec = encoding_order_grouped - .iter() - .flat_map(|(_, members)| members.iter().copied()) - .collect(); - let mut func = Function::new(local_decls); func.instruction(&Instruction::Loop(wasm_encoder::BlockType::Empty)); for op in &self.ops { @@ -470,9 +567,27 @@ impl GcOps { let mut stack: Vec = Vec::new(); let num_types = u32::try_from(self.types.type_defs.len()).unwrap(); + // Concrete encoding indices split by kind, so typed ops can be remapped + // onto a same-kind type (struct ops never point at an array, etc.). + let mut struct_type_indices = Vec::new(); + let mut array_type_indices = Vec::new(); + for (i, tid) in encoding_order.iter().enumerate() { + let i = u32::try_from(i).unwrap(); + match self.types.type_defs.get(tid) { + Some(def) if def.composite_type.is_array() => array_type_indices.push(i), + Some(_) => struct_type_indices.push(i), + None => {} + } + } + let mut operand_types = Vec::new(); for op in &self.ops { - let Some(op) = op.fixup(&self.limits, num_types) else { + let Some(op) = op.fixup( + &self.limits, + num_types, + &struct_type_indices, + &array_type_indices, + ) else { continue; }; let op = StackType::fixup_cast(op, &self.types, &encoding_order); @@ -530,7 +645,7 @@ macro_rules! for_each_gc_op { #[operands([])] #[results([ExternRef])] - #[fixup(|limits, _num_types| { + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { // Add one to make sure that out-of-bounds table accesses are // possible, but still rare. elem_index = elem_index % (limits.table_size + 1); @@ -539,7 +654,7 @@ macro_rules! for_each_gc_op { #[operands([Some(ExternRef)])] #[results([])] - #[fixup(|limits, _num_types| { + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { // Add one to make sure that out-of-bounds table accesses are // possible, but still rare. elem_index = elem_index % (limits.table_size + 1); @@ -548,36 +663,36 @@ macro_rules! for_each_gc_op { #[operands([])] #[results([ExternRef])] - #[fixup(|limits, _num_types| { + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { global_index = global_index.checked_rem(limits.num_globals)?; })] GlobalGet { global_index: u32 }, #[operands([Some(ExternRef)])] #[results([])] - #[fixup(|limits, _num_types| { + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { global_index = global_index.checked_rem(limits.num_globals)?; })] GlobalSet { global_index: u32 }, #[operands([])] #[results([ExternRef])] - #[fixup(|limits, _num_types| { + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { local_index = local_index.checked_rem(limits.num_params)?; })] LocalGet { local_index: u32 }, #[operands([Some(ExternRef)])] #[results([])] - #[fixup(|limits, _num_types| { + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { local_index = local_index.checked_rem(limits.num_params)?; })] LocalSet { local_index: u32 }, #[operands([])] #[results([Struct(Some(type_index))])] - #[fixup(|_limits, num_types| { - type_index = type_index.checked_rem(num_types)?; + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + type_index = pick_type_index(struct_type_indices, type_index)?; })] StructNew { type_index: u32 }, @@ -587,8 +702,8 @@ macro_rules! for_each_gc_op { #[operands([Some(Struct(Some(type_index)))])] #[results([])] - #[fixup(|_limits, num_types| { - type_index = type_index.checked_rem(num_types)?; + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + type_index = pick_type_index(struct_type_indices, type_index)?; })] TakeTypedStructCall { type_index: u32 }, @@ -602,15 +717,15 @@ macro_rules! for_each_gc_op { #[operands([Some(Struct(Some(type_index)))])] #[results([])] - #[fixup(|_limits, num_types| { - type_index = type_index.checked_rem(num_types)?; + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + type_index = pick_type_index(struct_type_indices, type_index)?; })] TypedStructLocalSet { type_index: u32 }, #[operands([])] #[results([Struct(Some(type_index))])] - #[fixup(|_limits, num_types| { - type_index = type_index.checked_rem(num_types)?; + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + type_index = pick_type_index(struct_type_indices, type_index)?; })] TypedStructLocalGet { type_index: u32 }, @@ -624,21 +739,21 @@ macro_rules! for_each_gc_op { #[operands([Some(Struct(Some(type_index)))])] #[results([])] - #[fixup(|_limits, num_types| { - type_index = type_index.checked_rem(num_types)?; + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + type_index = pick_type_index(struct_type_indices, type_index)?; })] TypedStructGlobalSet { type_index: u32 }, #[operands([])] #[results([Struct(Some(type_index))])] - #[fixup(|_limits, num_types| { - type_index = type_index.checked_rem(num_types)?; + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + type_index = pick_type_index(struct_type_indices, type_index)?; })] TypedStructGlobalGet { type_index: u32 }, #[operands([Some(Struct(None))])] #[results([])] - #[fixup(|limits, _num_types| { + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { // Add one to make sure that out-of-bounds table accesses are // possible, but still rare. elem_index = elem_index % (limits.table_size + 1); @@ -647,7 +762,7 @@ macro_rules! for_each_gc_op { #[operands([])] #[results([Struct(None)])] - #[fixup(|limits, _num_types| { + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { // Add one to make sure that out-of-bounds table accesses are // possible, but still rare. elem_index = elem_index % (limits.table_size + 1); @@ -656,21 +771,21 @@ macro_rules! for_each_gc_op { #[operands([Some(Struct(Some(type_index)))])] #[results([])] - #[fixup(|limits, num_types| { + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { // Add one to make sure that out-of-bounds table accesses are // possible, but still rare. elem_index = elem_index % (limits.table_size + 1); - type_index = type_index.checked_rem(num_types)?; + type_index = pick_type_index(struct_type_indices, type_index)?; })] TypedStructTableSet { elem_index: u32, type_index: u32 }, #[operands([])] #[results([Struct(Some(type_index))])] - #[fixup(|limits, num_types| { + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { // Add one to make sure that out-of-bounds table accesses are // possible, but still rare. elem_index = elem_index % (limits.table_size + 1); - type_index = type_index.checked_rem(num_types)?; + type_index = pick_type_index(struct_type_indices, type_index)?; })] TypedStructTableGet { elem_index: u32, type_index: u32 }, @@ -712,7 +827,7 @@ macro_rules! for_each_gc_op { #[operands([Some(Eq)])] #[results([])] - #[fixup(|limits, _num_types| { + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { // Add one to make sure that out-of-bounds table accesses are // possible, but still rare. elem_index = elem_index % (limits.table_size + 1); @@ -721,7 +836,7 @@ macro_rules! for_each_gc_op { #[operands([])] #[results([Eq])] - #[fixup(|limits, _num_types| { + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { // Add one to make sure that out-of-bounds table accesses are // possible, but still rare. elem_index = elem_index % (limits.table_size + 1); @@ -730,38 +845,38 @@ macro_rules! for_each_gc_op { #[operands([])] #[results([Struct(Some(type_index))])] - #[fixup(|_limits, num_types| { - type_index = type_index.checked_rem(num_types)?; + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + type_index = pick_type_index(struct_type_indices, type_index)?; })] NullTypedStruct { type_index: u32 }, #[operands([Some(Struct(Some(sub_type_index)))])] #[results([Struct(Some(super_type_index))])] - #[fixup(|_limits, num_types| { - sub_type_index = sub_type_index.checked_rem(num_types)?; - super_type_index = super_type_index.checked_rem(num_types)?; + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + sub_type_index = pick_type_index(struct_type_indices, sub_type_index)?; + super_type_index = pick_type_index(struct_type_indices, super_type_index)?; })] RefCastUpward { sub_type_index: u32, super_type_index: u32 }, #[operands([Some(Struct(Some(super_type_index)))])] #[results([Struct(Some(sub_type_index))])] - #[fixup(|_limits, num_types| { - sub_type_index = sub_type_index.checked_rem(num_types)?; - super_type_index = super_type_index.checked_rem(num_types)?; + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + sub_type_index = pick_type_index(struct_type_indices, sub_type_index)?; + super_type_index = pick_type_index(struct_type_indices, super_type_index)?; })] RefCastDownward { sub_type_index: u32, super_type_index: u32 }, #[operands([Some(Struct(Some(type_index)))])] #[results([])] - #[fixup(|_limits, num_types| { - type_index = type_index.checked_rem(num_types)?; + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + type_index = pick_type_index(struct_type_indices, type_index)?; })] StructGet { type_index: u32, field_index: u32 }, #[operands([Some(Struct(Some(type_index)))])] #[results([])] - #[fixup(|_limits, num_types| { - type_index = type_index.checked_rem(num_types)?; + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + type_index = pick_type_index(struct_type_indices, type_index)?; })] StructSet { type_index: u32, field_index: u32 }, @@ -771,7 +886,7 @@ macro_rules! for_each_gc_op { #[operands([])] #[results([I31])] - #[fixup(|_limits, _num_types| { + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { // Any `i32` is a valid operand to `ref.i31` (it wraps to 31 // bits), so no clamping is needed. })] @@ -795,7 +910,7 @@ macro_rules! for_each_gc_op { #[operands([Some(I31)])] #[results([])] - #[fixup(|limits, _num_types| { + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { // Add one to make sure that out-of-bounds table accesses are // possible, but still rare. elem_index = elem_index % (limits.table_size + 1); @@ -804,7 +919,7 @@ macro_rules! for_each_gc_op { #[operands([])] #[results([I31])] - #[fixup(|limits, _num_types| { + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { // Add one to make sure that out-of-bounds table accesses are // possible, but still rare. elem_index = elem_index % (limits.table_size + 1); @@ -829,14 +944,175 @@ macro_rules! for_each_gc_op { #[operands([Some(Struct(Some(type_index)))])] #[results([Eq])] - #[fixup(|_limits, num_types| { - type_index = type_index.checked_rem(num_types)?; + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + type_index = pick_type_index(struct_type_indices, type_index)?; })] TypedStructRefAsEq { type_index: u32 }, #[operands([Some(I31)])] #[results([Eq])] I31RefAsEq, + + #[operands([])] + #[results([Array(Some(type_index))])] + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + type_index = pick_type_index(array_type_indices, type_index)?; + })] + ArrayNew { type_index: u32 }, + + #[operands([])] + #[results([Array(None)])] + NullArray, + + #[operands([])] + #[results([Array(Some(type_index))])] + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + type_index = pick_type_index(array_type_indices, type_index)?; + })] + NullTypedArray { type_index: u32 }, + + #[operands([Some(Array(None))])] + #[results([])] + TakeArrayCall, + + #[operands([Some(Array(Some(type_index)))])] + #[results([])] + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + type_index = pick_type_index(array_type_indices, type_index)?; + })] + TakeTypedArrayCall { type_index: u32 }, + + #[operands([Some(Array(None))])] + #[results([])] + ArrayLocalSet, + + #[operands([])] + #[results([Array(None)])] + ArrayLocalGet, + + #[operands([Some(Array(Some(type_index)))])] + #[results([])] + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + type_index = pick_type_index(array_type_indices, type_index)?; + })] + TypedArrayLocalSet { type_index: u32 }, + + #[operands([])] + #[results([Array(Some(type_index))])] + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + type_index = pick_type_index(array_type_indices, type_index)?; + })] + TypedArrayLocalGet { type_index: u32 }, + + #[operands([Some(Array(None))])] + #[results([])] + ArrayGlobalSet, + + #[operands([])] + #[results([Array(None)])] + ArrayGlobalGet, + + #[operands([Some(Array(Some(type_index)))])] + #[results([])] + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + type_index = pick_type_index(array_type_indices, type_index)?; + })] + TypedArrayGlobalSet { type_index: u32 }, + + #[operands([])] + #[results([Array(Some(type_index))])] + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + type_index = pick_type_index(array_type_indices, type_index)?; + })] + TypedArrayGlobalGet { type_index: u32 }, + + #[operands([Some(Array(None))])] + #[results([])] + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + // Add one to make sure that out-of-bounds table accesses are + // possible, but still rare. + elem_index = elem_index % (limits.table_size + 1); + })] + ArrayTableSet { elem_index: u32 }, + + #[operands([])] + #[results([Array(None)])] + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + // Add one to make sure that out-of-bounds table accesses are + // possible, but still rare. + elem_index = elem_index % (limits.table_size + 1); + })] + ArrayTableGet { elem_index: u32 }, + + #[operands([Some(Array(Some(type_index)))])] + #[results([])] + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + // Add one to make sure that out-of-bounds table accesses are + // possible, but still rare. + elem_index = elem_index % (limits.table_size + 1); + type_index = pick_type_index(array_type_indices, type_index)?; + })] + TypedArrayTableSet { elem_index: u32, type_index: u32 }, + + #[operands([])] + #[results([Array(Some(type_index))])] + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + // Add one to make sure that out-of-bounds table accesses are + // possible, but still rare. + elem_index = elem_index % (limits.table_size + 1); + type_index = pick_type_index(array_type_indices, type_index)?; + })] + TypedArrayTableGet { elem_index: u32, type_index: u32 }, + + #[operands([Some(Array(Some(sub_type_index)))])] + #[results([Array(Some(super_type_index))])] + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + sub_type_index = pick_type_index(array_type_indices, sub_type_index)?; + super_type_index = pick_type_index(array_type_indices, super_type_index)?; + })] + ArrayRefCastUpward { sub_type_index: u32, super_type_index: u32 }, + + #[operands([Some(Array(Some(super_type_index)))])] + #[results([Array(Some(sub_type_index))])] + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + sub_type_index = pick_type_index(array_type_indices, sub_type_index)?; + super_type_index = pick_type_index(array_type_indices, super_type_index)?; + })] + ArrayRefCastDownward { sub_type_index: u32, super_type_index: u32 }, + + #[operands([Some(Array(Some(type_index)))])] + #[results([])] + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + // Keep most indices in-bounds: an array is exactly `array_length` + // long, so `% (array_length + 1)` is out-of-bounds only for one + // index value, making OOB traps possible but rare. + index = index % (limits.array_length + 1); + type_index = pick_type_index(array_type_indices, type_index)?; + })] + ArrayGet { type_index: u32, index: u32 }, + + #[operands([Some(Array(Some(type_index)))])] + #[results([])] + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + index = index % (limits.array_length + 1); + type_index = pick_type_index(array_type_indices, type_index)?; + })] + ArraySet { type_index: u32, index: u32 }, + + #[operands([Some(Array(None))])] + #[results([])] + ArrayLen, + + #[operands([Some(Array(None))])] + #[results([Eq])] + ArrayRefAsEq, + + #[operands([Some(Array(Some(type_index)))])] + #[results([Eq])] + #[fixup(|limits, num_types, struct_type_indices, array_type_indices| { + type_index = pick_type_index(array_type_indices, type_index)?; + })] + TypedArrayRefAsEq { type_index: u32 }, } }; } @@ -954,16 +1230,27 @@ impl GcOp { for_each_gc_op!(define_gc_op_result_types) } - pub(crate) fn fixup(&self, limits: &GcOpsLimits, num_types: u32) -> Option { + /// Fix up an op's immediates. `struct_type_indices` / `array_type_indices` + /// are the concrete encoding indices of each kind; a typed op remaps its + /// type index into the matching set so struct ops never point at an array + /// (or vice versa), and drops itself if no type of that kind exists. + pub(crate) fn fixup( + &self, + limits: &GcOpsLimits, + num_types: u32, + struct_type_indices: &[u32], + array_type_indices: &[u32], + ) -> Option { macro_rules! define_gc_op_fixup { ( $( #[operands($operands:expr)] #[results($results:expr)] - $( #[fixup(|$limits:ident, $num_types:ident| $fixup:expr)] )? + $( #[fixup(|$limits:ident, $num_types:ident, $structs:ident, $arrays:ident| $fixup:expr)] )? $op:ident $( { $( $field:ident : $field_ty:ty ),* } )? , )* ) => {{ + let _ = (limits, num_types, struct_type_indices, array_type_indices); match self { $( Self::$op $( { $($field),* } )? => { @@ -972,8 +1259,14 @@ impl GcOp { #[allow(unused_mut, unused_assignments, reason = "macro code")] let mut $field = *$field; )* + #[allow(unused_variables, reason = "macro code")] let $limits = limits; + #[allow(unused_variables, reason = "macro code")] let $num_types = num_types; + #[allow(unused_variables, reason = "macro code")] + let $structs = struct_type_indices; + #[allow(unused_variables, reason = "macro code")] + let $arrays = array_type_indices; $fixup; )? Some(Self::$op $( { $( $field ),* } )? ) @@ -1030,6 +1323,7 @@ impl GcOp { let take_structref_idx = 3; let take_eqref_idx = 4; let take_i31_idx = 5; + let take_arrayref_idx = 6; match *self { Self::Gc => { @@ -1107,15 +1401,18 @@ impl GcOp { func.instruction(&Instruction::LocalGet(encoding_bases.eq_local_idx)); func.instruction(&Instruction::TableSet(encoding_bases.eq_table_idx)); } - Self::NullTypedStruct { type_index } => { + // `NullTypedStruct` / `NullTypedArray` both produce a typed null; the + // concrete type index already resolves to the right kind. + Self::NullTypedStruct { type_index } | Self::NullTypedArray { type_index } => { func.instruction(&Instruction::RefNull(wasm_encoder::HeapType::Concrete( encoding_bases.struct_type_base + type_index, ))); } Self::StructNew { type_index: x } => { if let Some(tid) = encoding_order.get(usize::try_from(x).unwrap()) { - if let Some(def) = types.type_defs.get(tid) { - let CompositeType::Struct(ref st) = def.composite_type; + if let Some(CompositeType::Struct(st)) = + types.type_defs.get(tid).map(|def| &def.composite_type) + { for field in &st.fields { field.field_type.emit_default_const(func, type_ids_to_index); } @@ -1123,29 +1420,46 @@ impl GcOp { } func.instruction(&Instruction::StructNew(encoding_bases.struct_type_base + x)); } + Self::ArrayNew { type_index: x } => { + // Create a default-initialized array of a fixed length so most + // subsequent indexed accesses are in-bounds. + func.instruction(&Instruction::I32Const( + encoding_bases.array_length.cast_signed(), + )); + func.instruction(&Instruction::ArrayNewDefault( + encoding_bases.struct_type_base + x, + )); + } Self::TakeStructCall => { func.instruction(&Instruction::Call(take_structref_idx)); } - Self::TakeTypedStructCall { type_index: x } => { + // Typed struct/array calls and storage share identical encodings + // (they index the same kind-agnostic typed func/local/global/table + // slots); only their abstract stack type differs. + Self::TakeTypedStructCall { type_index: x } + | Self::TakeTypedArrayCall { type_index: x } => { let f = encoding_bases.typed_first_func_index + x; func.instruction(&Instruction::Call(f)); } Self::StructLocalGet => { func.instruction(&Instruction::LocalGet(encoding_bases.struct_local_idx)); } - Self::TypedStructLocalGet { type_index: x } => { + Self::TypedStructLocalGet { type_index: x } + | Self::TypedArrayLocalGet { type_index: x } => { func.instruction(&Instruction::LocalGet(encoding_bases.typed_local_base + x)); } Self::StructLocalSet => { func.instruction(&Instruction::LocalSet(encoding_bases.struct_local_idx)); } - Self::TypedStructLocalSet { type_index: x } => { + Self::TypedStructLocalSet { type_index: x } + | Self::TypedArrayLocalSet { type_index: x } => { func.instruction(&Instruction::LocalSet(encoding_bases.typed_local_base + x)); } Self::StructGlobalGet => { func.instruction(&Instruction::GlobalGet(encoding_bases.struct_global_idx)); } - Self::TypedStructGlobalGet { type_index: x } => { + Self::TypedStructGlobalGet { type_index: x } + | Self::TypedArrayGlobalGet { type_index: x } => { func.instruction(&Instruction::GlobalGet( encoding_bases.typed_global_base + x, )); @@ -1153,7 +1467,8 @@ impl GcOp { Self::StructGlobalSet => { func.instruction(&Instruction::GlobalSet(encoding_bases.struct_global_idx)); } - Self::TypedStructGlobalSet { type_index: x } => { + Self::TypedStructGlobalSet { type_index: x } + | Self::TypedArrayGlobalSet { type_index: x } => { func.instruction(&Instruction::GlobalSet( encoding_bases.typed_global_base + x, )); @@ -1165,6 +1480,10 @@ impl GcOp { Self::TypedStructTableGet { elem_index, type_index, + } + | Self::TypedArrayTableGet { + elem_index, + type_index, } => { func.instruction(&Instruction::I32Const(elem_index.cast_signed())); func.instruction(&Instruction::TableGet( @@ -1181,6 +1500,10 @@ impl GcOp { Self::TypedStructTableSet { elem_index, type_index, + } + | Self::TypedArrayTableSet { + elem_index, + type_index, } => { func.instruction(&Instruction::LocalSet( encoding_bases.typed_local_base + type_index, @@ -1196,6 +1519,10 @@ impl GcOp { Self::RefCastUpward { sub_type_index: _, super_type_index, + } + | Self::ArrayRefCastUpward { + sub_type_index: _, + super_type_index, } => { // The value on the stack is already the subtype, so this // cast always succeeds. @@ -1207,6 +1534,10 @@ impl GcOp { Self::RefCastDownward { sub_type_index, super_type_index, + } + | Self::ArrayRefCastDownward { + sub_type_index, + super_type_index, } => { // Fallible downcast that never traps: // @@ -1261,9 +1592,9 @@ impl GcOp { let fields = encoding_order .get(usize::try_from(type_index).unwrap()) .and_then(|tid| types.type_defs.get(tid)) - .map(|def| { - let CompositeType::Struct(ref st) = def.composite_type; - &st.fields[..] + .and_then(|def| match &def.composite_type { + CompositeType::Struct(st) => Some(&st.fields[..]), + CompositeType::Array(_) => None, }); match fields { @@ -1305,9 +1636,9 @@ impl GcOp { let fields = encoding_order .get(usize::try_from(type_index).unwrap()) .and_then(|tid| types.type_defs.get(tid)) - .map(|def| { - let CompositeType::Struct(ref st) = def.composite_type; - &st.fields[..] + .and_then(|def| match &def.composite_type { + CompositeType::Struct(st) => Some(&st.fields[..]), + CompositeType::Array(_) => None, }); match fields { @@ -1378,10 +1709,14 @@ impl GcOp { func.instruction(&Instruction::LocalGet(encoding_bases.i31_local_idx)); func.instruction(&Instruction::TableSet(encoding_bases.i31_table_idx)); } - Self::StructRefAsEq | Self::TypedStructRefAsEq { .. } | Self::I31RefAsEq => { - // Upcasting to `eqref` is implicit in Wasm subtyping: both - // `struct` and `i31` are subtypes of `eq`, so the value already - // on the stack is a valid `eqref` and no instruction is + Self::StructRefAsEq + | Self::TypedStructRefAsEq { .. } + | Self::ArrayRefAsEq + | Self::TypedArrayRefAsEq { .. } + | Self::I31RefAsEq => { + // Upcasting to `eqref` is implicit in Wasm subtyping: `struct`, + // `array`, and `i31` are all subtypes of `eq`, so the value + // already on the stack is a valid `eqref` and no instruction is // required. Only the abstract stack type changes (via // `result_types`). } @@ -1426,6 +1761,116 @@ impl GcOp { func.instruction(&Instruction::Call(take_i31_idx)); func.instruction(&Instruction::End); } + Self::NullArray => { + func.instruction(&Instruction::RefNull(wasm_encoder::HeapType::Abstract { + shared: false, + ty: wasm_encoder::AbstractHeapType::Array, + })); + } + Self::TakeArrayCall => { + func.instruction(&Instruction::Call(take_arrayref_idx)); + } + Self::ArrayLocalGet => { + func.instruction(&Instruction::LocalGet(encoding_bases.array_local_idx)); + } + Self::ArrayLocalSet => { + func.instruction(&Instruction::LocalSet(encoding_bases.array_local_idx)); + } + Self::ArrayGlobalGet => { + func.instruction(&Instruction::GlobalGet(encoding_bases.array_global_idx)); + } + Self::ArrayGlobalSet => { + func.instruction(&Instruction::GlobalSet(encoding_bases.array_global_idx)); + } + Self::ArrayTableGet { elem_index } => { + func.instruction(&Instruction::I32Const(elem_index.cast_signed())); + func.instruction(&Instruction::TableGet(encoding_bases.array_table_idx)); + } + Self::ArrayTableSet { elem_index } => { + // Use array_local_idx (arrayref) to temporarily store the value before table.set. + func.instruction(&Instruction::LocalSet(encoding_bases.array_local_idx)); + func.instruction(&Instruction::I32Const(elem_index.cast_signed())); + func.instruction(&Instruction::LocalGet(encoding_bases.array_local_idx)); + func.instruction(&Instruction::TableSet(encoding_bases.array_table_idx)); + } + Self::ArrayGet { type_index, index } => { + let wasm_type = encoding_bases.struct_type_base + type_index; + let typed_local = encoding_bases.typed_local_base + type_index; + let element = encoding_order + .get(usize::try_from(type_index).unwrap()) + .and_then(|tid| types.type_defs.get(tid)) + .and_then(|def| match &def.composite_type { + CompositeType::Array(at) => Some(&at.element), + CompositeType::Struct(_) => None, + }); + + match element { + Some(element) => { + // Null-guard: array.get traps on null, so skip if null. + // The index is kept mostly in-bounds by fixup. + func.instruction(&Instruction::LocalTee(typed_local)); + func.instruction(&Instruction::RefIsNull); + func.instruction(&Instruction::If(wasm_encoder::BlockType::Empty)); + func.instruction(&Instruction::Else); + func.instruction(&Instruction::LocalGet(typed_local)); + func.instruction(&Instruction::I32Const(index.cast_signed())); + if element.field_type.is_packed() { + func.instruction(&Instruction::ArrayGetS(wasm_type)); + } else { + func.instruction(&Instruction::ArrayGet(wasm_type)); + } + // The element value is not tracked on the abstract stack. + func.instruction(&Instruction::Drop); + func.instruction(&Instruction::End); + } + None => { + func.instruction(&Instruction::Drop); + } + } + } + Self::ArraySet { type_index, index } => { + let wasm_type = encoding_bases.struct_type_base + type_index; + let typed_local = encoding_bases.typed_local_base + type_index; + let element = encoding_order + .get(usize::try_from(type_index).unwrap()) + .and_then(|tid| types.type_defs.get(tid)) + .and_then(|def| match &def.composite_type { + CompositeType::Array(at) => Some(at.element.clone()), + CompositeType::Struct(_) => None, + }); + + match element { + Some(element) if element.mutable => { + // Null-guard: array.set traps on null, so skip if null. + func.instruction(&Instruction::LocalTee(typed_local)); + func.instruction(&Instruction::RefIsNull); + func.instruction(&Instruction::If(wasm_encoder::BlockType::Empty)); + func.instruction(&Instruction::Else); + func.instruction(&Instruction::LocalGet(typed_local)); + func.instruction(&Instruction::I32Const(index.cast_signed())); + element + .field_type + .emit_default_const(func, type_ids_to_index); + func.instruction(&Instruction::ArraySet(wasm_type)); + func.instruction(&Instruction::End); + } + // Immutable element or non-array: just drop the operand. + _ => { + func.instruction(&Instruction::Drop); + } + } + } + Self::ArrayLen => { + // array.len traps on null, so guard; the length is not tracked. + func.instruction(&Instruction::LocalTee(encoding_bases.array_local_idx)); + func.instruction(&Instruction::RefIsNull); + func.instruction(&Instruction::If(wasm_encoder::BlockType::Empty)); + func.instruction(&Instruction::Else); + func.instruction(&Instruction::LocalGet(encoding_bases.array_local_idx)); + func.instruction(&Instruction::ArrayLen); + func.instruction(&Instruction::Drop); + func.instruction(&Instruction::End); + } } } } diff --git a/crates/fuzzing/src/generators/gc_ops/tests.rs b/crates/fuzzing/src/generators/gc_ops/tests.rs index 4a44c42796c1..f7aabde517c9 100644 --- a/crates/fuzzing/src/generators/gc_ops/tests.rs +++ b/crates/fuzzing/src/generators/gc_ops/tests.rs @@ -63,6 +63,7 @@ fn empty_test_ops() -> GcOps { max_rec_groups: 5, max_types: 5, max_fields: 10, + array_length: 5, }, ops: vec![], types: Types::new(), @@ -83,6 +84,7 @@ fn test_ops(num_params: u32, num_globals: u32, table_size: u32) -> GcOps { max_rec_groups: 7, max_types: 10, max_fields: 10, + array_length: 5, }, ops: vec![ GcOp::NullExtern, @@ -418,6 +420,7 @@ fn fixup_preserves_subtyping_within_same_rec_group() { max_rec_groups: 10, max_types: 10, max_fields: 10, + array_length: 5, }; types.fixup(&limits, &mut Vec::new()); @@ -466,6 +469,7 @@ fn fixup_breaks_one_edge_in_multi_rec_group_type_cycle() { max_rec_groups: 10, max_types: 10, max_fields: 10, + array_length: 5, }; types.fixup(&limits, &mut Vec::new()); @@ -819,6 +823,7 @@ fn cast_test_ops(ops: Vec) -> GcOps { max_rec_groups: 5, max_types: 10, max_fields: 10, + array_length: 5, }, ops, types: Types::new(), @@ -847,6 +852,7 @@ fn flat_cast_test_ops(ops: Vec) -> GcOps { max_rec_groups: 5, max_types: 10, max_fields: 10, + array_length: 5, }, ops, types: Types::new(), diff --git a/crates/fuzzing/src/generators/gc_ops/types.rs b/crates/fuzzing/src/generators/gc_ops/types.rs index 46d86b13f03b..04889ba4027b 100644 --- a/crates/fuzzing/src/generators/gc_ops/types.rs +++ b/crates/fuzzing/src/generators/gc_ops/types.rs @@ -205,11 +205,44 @@ pub struct StructType { pub(crate) fields: Vec, } -/// A composite type: currently only structs. -#[derive(Debug, Serialize, Deserialize)] +/// An array type definition: a single element storage type plus mutability. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ArrayType { + /// The element storage type of this array type. + pub(crate) element: StructField, +} + +/// A composite type: either a struct or an array. +#[derive(Clone, Debug, Serialize, Deserialize)] pub enum CompositeType { /// A struct composite type. Struct(StructType), + /// An array composite type. + Array(ArrayType), +} + +impl CompositeType { + /// Returns `true` if this is an array composite type. + pub(crate) fn is_array(&self) -> bool { + matches!(self, CompositeType::Array(_)) + } + + /// The storage fields of this composite type. + /// All struct fields or the single array element. + pub(crate) fn fields(&self) -> &[StructField] { + match self { + CompositeType::Struct(st) => &st.fields, + CompositeType::Array(at) => std::slice::from_ref(&at.element), + } + } + + /// Mutable view of the storage fields; see [`CompositeType::fields`]. + pub(crate) fn fields_mut(&mut self) -> &mut [StructField] { + match self { + CompositeType::Struct(st) => &mut st.fields, + CompositeType::Array(at) => std::slice::from_mut(&mut at.element), + } + } } /// A sub-type definition (the per-type payload). @@ -289,11 +322,11 @@ impl Graph for RecGroupGraph<'_> { } } - // Field-reference edges: a concrete `(ref null $t)` field means - // `$t`'s group must encode first (references *within* a group are - // always legal and impose no ordering constraint). - let CompositeType::Struct(ref st) = def.composite_type; - for field in &st.fields { + // Field-reference edges: a concrete `(ref null $t)` field (or + // array element) means `$t`'s group must encode first (references + // *within* a group are always legal and impose no ordering + // constraint). + for field in def.composite_type.fields() { if let FieldType::Ref { type_id, .. } = field.field_type { if let Some(&ref_group) = self.type_to_group.get(&type_id) { if ref_group != group { @@ -391,16 +424,16 @@ impl Types { } } - /// Insert a struct type into the given rec group. + /// Insert a type with the given composite type into the given rec group. /// /// The rec group must already exist. - pub fn insert_struct( + pub fn insert_type( &mut self, id: TypeId, group: RecGroupId, is_final: bool, supertype: Option, - fields: Vec, + composite_type: CompositeType, ) { self.rec_groups .get_mut(&group) @@ -411,11 +444,31 @@ impl Types { SubType { is_final, supertype, - composite_type: CompositeType::Struct(StructType { fields }), + composite_type, }, ); } + /// Insert a struct type into the given rec group. + /// + /// The rec group must already exist. + pub fn insert_struct( + &mut self, + id: TypeId, + group: RecGroupId, + is_final: bool, + supertype: Option, + fields: Vec, + ) { + self.insert_type( + id, + group, + is_final, + supertype, + CompositeType::Struct(StructType { fields }), + ); + } + /// Remove a type from its rec group and from `type_defs`. pub fn remove_type(&mut self, id: TypeId) { self.type_defs.remove(&id); @@ -708,18 +761,34 @@ impl Types { } } - // 8. Trim fields to max_fields limit. + // 7b. A subtype must have the same composite kind as its supertype + // (a struct cannot subtype an array, or vice versa). + let kinds: BTreeMap = self + .type_defs + .iter() + .map(|(id, d)| (*id, d.composite_type.is_array())) + .collect(); + for (tid, def) in self.type_defs.iter_mut() { + if let Some(super_id) = def.supertype { + if kinds.get(&super_id) != kinds.get(tid) { + def.supertype = None; + } + } + } + + // 8. Trim struct fields to max_fields limit (arrays always have exactly + // one element). let max_fields = usize::try_from(limits.max_fields).unwrap(); for def in self.type_defs.values_mut() { - let CompositeType::Struct(ref mut st) = def.composite_type; - st.fields.truncate(max_fields); + if let CompositeType::Struct(ref mut st) = def.composite_type { + st.fields.truncate(max_fields); + } } - // 9. Normalize reference fields. + // 9. Normalize reference fields (struct fields and array elements alike). let valid_type_ids: BTreeSet = self.type_defs.keys().copied().collect(); for def in self.type_defs.values_mut() { - let CompositeType::Struct(ref mut st) = def.composite_type; - for field in &mut st.fields { + for field in def.composite_type.fields_mut() { match &mut field.field_type { FieldType::StructRef { nullable } => *nullable = true, FieldType::Ref { nullable, type_id } => { @@ -757,21 +826,36 @@ impl Types { let Some(super_def) = self.type_defs.get(&super_id) else { continue; }; - let CompositeType::Struct(ref super_st) = super_def.composite_type; - let super_fields = super_st.fields.clone(); - - let def = self.type_defs.get_mut(tid).unwrap(); - let CompositeType::Struct(ref mut sub_st) = def.composite_type; - - // Extend subtype fields if shorter than supertype. - while sub_st.fields.len() < super_fields.len() { - sub_st - .fields - .push(super_fields[sub_st.fields.len()].clone()); - } - // Overwrite prefix to match supertype fields exactly. - for (i, sf) in super_fields.iter().enumerate() { - sub_st.fields[i] = sf.clone(); + // Step 7b guarantees the subtype and supertype share a composite + // kind. so match on the supertype and repair the subtype to match. + match &super_def.composite_type { + CompositeType::Struct(super_st) => { + let super_fields = super_st.fields.clone(); + let def = self.type_defs.get_mut(tid).unwrap(); + let CompositeType::Struct(ref mut sub_st) = def.composite_type else { + continue; + }; + // Extend subtype fields if shorter than supertype. + while sub_st.fields.len() < super_fields.len() { + sub_st + .fields + .push(super_fields[sub_st.fields.len()].clone()); + } + // Overwrite prefix to match supertype fields exactly. + for (i, sf) in super_fields.iter().enumerate() { + sub_st.fields[i] = sf.clone(); + } + } + CompositeType::Array(super_at) => { + // Force the element to match exactly. This satisfies both the + // covariant (immutable) and invariant (mutable) subtyping rules. + let super_elem = super_at.element.clone(); + let def = self.type_defs.get_mut(tid).unwrap(); + let CompositeType::Array(ref mut sub_at) = def.composite_type else { + continue; + }; + sub_at.element = super_elem; + } } } @@ -814,19 +898,20 @@ impl Types { // Every supertype must exist and must not be final. let max_fields = usize::try_from(limits.max_fields).unwrap(); for (&tid, def) in &self.type_defs { - // Check field count limit. - let CompositeType::Struct(ref st) = def.composite_type; - if st.fields.len() > max_fields { + let fields = def.composite_type.fields(); + + // Check struct field count limit (arrays always have one element). + if !def.composite_type.is_array() && fields.len() > max_fields { log::debug!( "[-] Failed: type {tid:?} has {} fields > max_fields {max_fields}", - st.fields.len() + fields.len() ); return false; } // Reference fields must be nullable (non-nullable references are // deferred), and concrete references must target an existing type. - for field in &st.fields { + for field in fields { match field.field_type { FieldType::StructRef { nullable } | FieldType::Ref { nullable, .. } if !nullable => @@ -854,17 +939,26 @@ impl Types { log::debug!("[-] Failed: subtype {tid:?} has final supertype {super_id:?}"); return false; } + Some(super_def) + if super_def.composite_type.is_array() != def.composite_type.is_array() => + { + log::debug!( + "[-] Failed: subtype {tid:?} and supertype {super_id:?} have different composite kinds" + ); + return false; + } Some(super_def) => { - // Subtype fields must be prefix-compatible with supertype. - let CompositeType::Struct(ref super_st) = super_def.composite_type; - if st.fields.len() < super_st.fields.len() { + // Subtype fields must be prefix-compatible with supertype + // (for arrays, the single element must match exactly). + let super_fields = super_def.composite_type.fields(); + if fields.len() < super_fields.len() { log::debug!( "[-] Failed: subtype {tid:?} has fewer fields than supertype {super_id:?}" ); return false; } - for (i, sf) in super_st.fields.iter().enumerate() { - if st.fields[i] != *sf { + for (i, sf) in super_fields.iter().enumerate() { + if fields[i] != *sf { log::debug!( "[-] Failed: subtype {tid:?} field {i} differs from supertype {super_id:?}" ); @@ -890,6 +984,8 @@ pub enum StackType { I31, /// `(ref $*)` — optionally with a concrete type index. Struct(Option), + /// `(ref array)` or `(ref $t)` — optionally with a concrete type index. + Array(Option), } impl StackType { @@ -933,9 +1029,9 @@ impl StackType { } }, Some(Self::Eq) => match stack.last() { - // struct <: eq and i31 <: eq, so a struct or i31 on the stack - // satisfies an eqref requirement. - Some(Self::Eq) | Some(Self::Struct(_)) | Some(Self::I31) => { + // struct, array, and i31 are all subtypes of eq, so any of them + // on the stack satisfies an eqref requirement. + Some(Self::Eq) | Some(Self::Struct(_)) | Some(Self::Array(_)) | Some(Self::I31) => { log::trace!("[StackType::fixup] Eq: top ok -> pop"); stack.pop(); } @@ -1052,6 +1148,53 @@ impl StackType { } } } + Some(Self::Array(wanted)) => { + let ok = match (wanted, stack.last()) { + (Some(wanted), Some(Self::Array(Some(actual)))) => { + let sub = encoding_order + .get(usize::try_from(*actual).unwrap()) + .copied(); + let sup = encoding_order + .get(usize::try_from(wanted).unwrap()) + .copied(); + match (sub, sup) { + (Some(sub), Some(sup)) => types.is_subtype(sub, sup), + _ => false, + } + } + // Abstract arrayref requirement accepts any array on the stack. + (None, Some(Self::Array(_))) => true, + _ => false, + }; + + if ok { + stack.pop(); + } else { + match wanted { + // Abstract requirement: a null arrayref satisfies it. + None => { + Self::emit(GcOp::NullArray, stack, out, num_types, &mut result_types); + stack.pop(); + } + // Concrete requirement: synthesize a fresh array of that type. + Some(t) => { + debug_assert_ne!( + num_types, 0, + "typed array requirement with num_types == 0; op should have been removed" + ); + let t = Self::clamp(t, num_types); + Self::emit( + GcOp::ArrayNew { type_index: t }, + stack, + out, + num_types, + &mut result_types, + ); + stack.pop(); + } + } + } + } } log::trace!( "[StackType::fixup] leave stack_len={} stack={stack:?} out_len={}", @@ -1078,6 +1221,7 @@ impl StackType { for ty in result_types { let clamped_ty = match ty { Self::Struct(Some(t)) => Self::Struct(Some(Self::clamp(*t, num_types))), + Self::Array(Some(t)) => Self::Array(Some(Self::clamp(*t, num_types))), other => *other, }; log::trace!("[StackType::emit] push result {clamped_ty:?}"); @@ -1122,6 +1266,33 @@ impl StackType { super_type_index, } } + // Array casts use the same index repair (subtyping is kind-agnostic). + GcOp::ArrayRefCastUpward { + sub_type_index, + super_type_index, + } => { + let super_type_index = Self::find_supertype_of( + sub_type_index, + super_type_index, + types, + encoding_order, + ); + GcOp::ArrayRefCastUpward { + sub_type_index, + super_type_index, + } + } + GcOp::ArrayRefCastDownward { + sub_type_index, + super_type_index, + } => { + let sub_type_index = + Self::find_subtype_of(super_type_index, sub_type_index, types, encoding_order); + GcOp::ArrayRefCastDownward { + sub_type_index, + super_type_index, + } + } other => other, } } diff --git a/crates/fuzzing/src/oracles.rs b/crates/fuzzing/src/oracles.rs index f96ac01f125f..d6776a91fc26 100644 --- a/crates/fuzzing/src/oracles.rs +++ b/crates/fuzzing/src/oracles.rs @@ -862,6 +862,21 @@ pub fn gc_ops(mut fuzz_config: generators::Config, mut ops: GcOps) -> Result, _params, _results| { + log::info!("gc_ops: take_array()"); + Ok(()) + } + }); + + linker.define(&store, "", "take_array", func).unwrap(); + // `take_i31` receives an `i31ref` along with the guest's inline // `i31.get_s` and `i31.get_u` results, and asserts that the host's own // view of the i31 matches the values the Wasm instructions produced. @@ -893,12 +908,12 @@ pub fn gc_ops(mut fuzz_config: generators::Config, mut ops: GcOps) -> Result)"); + log::info!("gc_ops: {imp_name}()"); Ok(()) }); linker.define(&store, "", name, func).unwrap(); @@ -930,10 +945,11 @@ pub fn gc_ops(mut fuzz_config: generators::Config, mut ops: GcOps) -> Result>() || err.is::>() { @@ -943,7 +959,10 @@ pub fn gc_ops(mut fuzz_config: generators::Config, mut ops: GcOps) -> Result() .expect("if not GC oom, error should be a Wasm trap"); match trap { - Trap::TableOutOfBounds | Trap::OutOfFuel | Trap::AllocationTooLarge => {} + Trap::TableOutOfBounds + | Trap::ArrayOutOfBounds + | Trap::OutOfFuel + | Trap::AllocationTooLarge => {} _ => panic!("unexpected trap: {trap}"), } }