From f974c5d3ad71f48411308a0ef6b771eb9368cc27 Mon Sep 17 00:00:00 2001 From: Khagan Karimov Date: Thu, 9 Apr 2026 02:28:31 +0000 Subject: [PATCH] Add cast types --- crates/fuzzing/src/generators/gc_ops/ops.rs | 77 ++++ crates/fuzzing/src/generators/gc_ops/tests.rs | 340 +++++++++++++++--- crates/fuzzing/src/generators/gc_ops/types.rs | 100 ++++++ 3 files changed, 466 insertions(+), 51 deletions(-) diff --git a/crates/fuzzing/src/generators/gc_ops/ops.rs b/crates/fuzzing/src/generators/gc_ops/ops.rs index 761348c9e50f..cc5c96038a5b 100644 --- a/crates/fuzzing/src/generators/gc_ops/ops.rs +++ b/crates/fuzzing/src/generators/gc_ops/ops.rs @@ -368,6 +368,7 @@ impl GcOps { let Some(op) = op.fixup(&self.limits, num_types) else { continue; }; + let op = StackType::fixup_cast(op, &self.types, &encoding_order); debug_assert!(operand_types.is_empty()); op.operand_types(&mut operand_types); @@ -584,6 +585,22 @@ macro_rules! for_each_gc_op { type_index = type_index.checked_rem(num_types)?; })] 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)?; + })] + 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)?; + })] + RefCastDownward { sub_type_index: u32, super_type_index: u32 }, } }; } @@ -890,6 +907,66 @@ impl GcOp { encoding_bases.typed_table_base + type_index, )); } + Self::RefCastUpward { + sub_type_index: _, + super_type_index, + } => { + // The value on the stack is already the subtype, so this + // cast always succeeds. + let heap_type = wasm_encoder::HeapType::Concrete( + encoding_bases.struct_type_base + super_type_index, + ); + func.instruction(&Instruction::RefCastNullable(heap_type)); + } + Self::RefCastDownward { + sub_type_index, + super_type_index, + } => { + // Fallible downcast that never traps: + // + // local.tee $my_temp + // ;; Test if the downcast will succeed. + // ref.test ... + // if (result (ref null $my_sub)) + // ;; The downcast will succeed, do a downcast-or-trap + // ;; operation which we know will not trap. + // local.get $my_temp + // ref.cast ... + // else + // ;; The downcast would fail, so just create a null + // ;; reference instead. + // ref.null ... + // end + let sub_wasm_type = encoding_bases.struct_type_base + sub_type_index; + let sub_heap_type = wasm_encoder::HeapType::Concrete(sub_wasm_type); + let temp_local = encoding_bases.typed_local_base + super_type_index; + + // Tee the supertype value into a temp local (saves and + // leaves the value on the stack for ref.test). + func.instruction(&Instruction::LocalTee(temp_local)); + + // Test if the downcast will succeed. + func.instruction(&Instruction::RefTestNullable(sub_heap_type)); + + // if (result (ref null $sub_type)) + func.instruction(&Instruction::If(wasm_encoder::BlockType::Result( + ValType::Ref(RefType { + nullable: true, + heap_type: sub_heap_type, + }), + ))); + + // The downcast will succeed; do the cast. + func.instruction(&Instruction::LocalGet(temp_local)); + func.instruction(&Instruction::RefCastNullable(sub_heap_type)); + + func.instruction(&Instruction::Else); + + // The downcast would fail; produce null instead. + func.instruction(&Instruction::RefNull(sub_heap_type)); + + 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 6c6548cadd5b..63583ab65505 100644 --- a/crates/fuzzing/src/generators/gc_ops/tests.rs +++ b/crates/fuzzing/src/generators/gc_ops/tests.rs @@ -112,45 +112,39 @@ fn test_ops(num_params: u32, num_globals: u32, table_size: u32) -> GcOps { t } +/// Validate that a GcOps produces a valid Wasm binary. +fn assert_valid_wasm(ops: &mut GcOps) { + let wasm = ops.to_wasm_binary(); + let feats = wasmparser::WasmFeatures::default(); + feats.reference_types(); + feats.gc(); + let mut validator = wasmparser::Validator::new_with_features(feats); + + if let Err(e) = validator.validate_all(&wasm) { + let wat = + wasmprinter::print_bytes(&wasm).unwrap_or_else(|e| format!("")); + panic!( + "Emitted Wasm binary is not valid!\n\n\ + === Validation Error ===\n\n\ + {e}\n\n\ + === GcOps ===\n\n\ + {ops:#?}\n\n\ + === Wat ===\n\n\ + {wat}" + ); + } +} + #[test] fn mutate_gc_ops_with_default_mutator() -> mutatis::Result<()> { let _ = env_logger::try_init(); - let mut features = wasmparser::WasmFeatures::default(); - features.insert(wasmparser::WasmFeatures::REFERENCE_TYPES); - features.insert(wasmparser::WasmFeatures::FUNCTION_REFERENCES); - features.insert(wasmparser::WasmFeatures::GC_TYPES); - features.insert(wasmparser::WasmFeatures::GC); - let mut ops = test_ops(5, 5, 5); let mut session = mutatis::Session::new(); for _ in 0..2048 { session.mutate(&mut ops)?; - - let wasm = ops.to_wasm_binary(); - crate::oracles::log_wasm(&wasm); - - let mut validator = wasmparser::Validator::new_with_features(features); - if let Err(e) = validator.validate_all(&wasm) { - let mut config = wasmprinter::Config::new(); - config.print_offsets(true); - config.print_operand_stack(true); - let mut wat = String::new(); - let wat = match config.print(&wasm, &mut wasmprinter::PrintFmtWrite(&mut wat)) { - Ok(()) => wat, - Err(e) => format!(""), - }; - panic!( - "Emitted Wasm binary is not valid!\n\n\ - === Validation Error ===\n\n\ - {e}\n\n\ - === GcOps ===\n\n\ - {ops:#?}\n\n\ - === Wat ===\n\n\ - {wat}" - ); - } + assert_valid_wasm(&mut ops); } Ok(()) } @@ -242,18 +236,9 @@ fn emits_rec_groups_and_validates() -> mutatis::Result<()> { let _ = env_logger::try_init(); let mut ops = test_ops(5, 5, 5); + assert_valid_wasm(&mut ops); let wasm = ops.to_wasm_binary(); - - 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 failed" - ); - let wat = wasmprinter::print_bytes(&wasm).expect("to WAT"); let recs = wat.matches("(rec").count(); let structs = wat.matches("(struct)").count(); @@ -327,17 +312,7 @@ fn fixup_check_types_and_indexes() -> mutatis::Result<()> { ); // Verify that we generate a valid Wasm binary after calling `fixup`. - 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" - ); + assert_valid_wasm(&mut ops); Ok(()) } @@ -782,3 +757,266 @@ fn stacktype_fixup_accepts_subtype_for_supertype_requirement() { assert_eq!(out, vec![GcOp::StructNew { type_index: 1 }]); assert_eq!(stack, vec![StackType::Struct(Some(0))]); } + +/// Helper: creates GcOps with a 3-type chain: TypeId(1) <- TypeId(2) <- TypeId(3). +/// +/// Dense encoding indices: +/// 0 -> TypeId(1) (root, no supertype) +/// 1 -> TypeId(2) (supertype = TypeId(1)) +/// 2 -> TypeId(3) (supertype = TypeId(2)) +fn cast_test_ops(ops: Vec) -> GcOps { + let mut t = GcOps { + limits: GcOpsLimits { + num_params: 0, + num_globals: 0, + table_size: 0, + max_rec_groups: 5, + max_types: 10, + }, + ops, + types: Types::new(), + }; + let g = RecGroupId(0); + t.types.insert_rec_group(g); + t.types.insert_empty_struct(TypeId(1), g, false, None); + t.types + .insert_empty_struct(TypeId(2), g, false, Some(TypeId(1))); + t.types + .insert_empty_struct(TypeId(3), g, false, Some(TypeId(2))); + t +} + +/// Helper: creates GcOps with two unrelated types (flat hierarchy, no supertypes). +/// +/// Dense encoding indices: +/// 0 -> TypeId(1) (no supertype) +/// 1 -> TypeId(2) (no supertype) +fn flat_cast_test_ops(ops: Vec) -> GcOps { + let mut t = GcOps { + limits: GcOpsLimits { + num_params: 0, + num_globals: 0, + table_size: 0, + max_rec_groups: 5, + max_types: 10, + }, + ops, + types: Types::new(), + }; + let g = RecGroupId(0); + t.types.insert_rec_group(g); + t.types.insert_empty_struct(TypeId(1), g, false, None); + t.types.insert_empty_struct(TypeId(2), g, false, None); + t +} + +// ---- RefCastUpward tests ---- + +/// Case 1: valid pair (sub <: super) — kept as-is. +#[test] +fn upcast_valid_pair() { + let _ = env_logger::try_init(); + // index 2 (TypeId(3)) <: index 0 (TypeId(1)) via chain 3 <: 2 <: 1 + let mut ops = cast_test_ops(vec![GcOp::RefCastUpward { + sub_type_index: 2, + super_type_index: 0, + }]); + ops.fixup(&mut Vec::new()); + + assert!( + ops.ops.iter().any(|op| matches!( + op, + GcOp::RefCastUpward { + sub_type_index: 2, + super_type_index: 0, + } + )), + "valid upcast pair should be preserved: {:#?}", + ops.ops + ); + assert_valid_wasm(&mut ops); +} + +/// Case 2: wrong pair — repaired to sub's direct supertype. +#[test] +fn upcast_wrong_pair_repaired_via_supertype() { + let _ = env_logger::try_init(); + // index 1 (TypeId(2)) is NOT a subtype of index 2 (TypeId(3)). + // TypeId(2)'s supertype is TypeId(1) at index 0. + // Should repair to { sub: 1, super: 0 }. + let mut ops = cast_test_ops(vec![GcOp::RefCastUpward { + sub_type_index: 1, + super_type_index: 2, + }]); + ops.fixup(&mut Vec::new()); + + assert!( + ops.ops.iter().any(|op| matches!( + op, + GcOp::RefCastUpward { + sub_type_index: 1, + super_type_index: 0, + } + )), + "upcast should be repaired to sub's direct supertype: {:#?}", + ops.ops + ); + assert_valid_wasm(&mut ops); +} + +/// Case 3: sub has no supertype — falls back to self-cast. +#[test] +fn upcast_no_supertype_self_cast() { + let _ = env_logger::try_init(); + // index 0 (TypeId(1)) has no supertype, so no valid super can be found. + // Falls back to self-cast { sub: 0, super: 0 }. + let mut ops = cast_test_ops(vec![GcOp::RefCastUpward { + sub_type_index: 0, + super_type_index: 2, + }]); + ops.fixup(&mut Vec::new()); + + assert!( + ops.ops.iter().any(|op| matches!( + op, + GcOp::RefCastUpward { + sub_type_index: 0, + super_type_index: 0, + } + )), + "upcast with no supertype should become self-cast: {:#?}", + ops.ops + ); + assert_valid_wasm(&mut ops); +} + +/// Case 4: flat hierarchy (no supertypes at all) — self-cast. +#[test] +fn upcast_flat_hierarchy_self_cast() { + let _ = env_logger::try_init(); + let mut ops = flat_cast_test_ops(vec![GcOp::RefCastUpward { + sub_type_index: 0, + super_type_index: 1, + }]); + ops.fixup(&mut Vec::new()); + + assert!( + ops.ops.iter().any(|op| matches!( + op, + GcOp::RefCastUpward { + sub_type_index, + super_type_index, + } if sub_type_index == super_type_index + )), + "upcast in flat hierarchy should become self-cast: {:#?}", + ops.ops + ); + assert_valid_wasm(&mut ops); +} + +// ---- RefCastDownward tests ---- + +/// Case 1: valid pair — kept as-is. +#[test] +fn downcast_valid_pair() { + let _ = env_logger::try_init(); + // index 2 (TypeId(3)) <: index 0 (TypeId(1)) — valid. + let mut ops = cast_test_ops(vec![GcOp::RefCastDownward { + sub_type_index: 2, + super_type_index: 0, + }]); + ops.fixup(&mut Vec::new()); + + assert!( + ops.ops.iter().any(|op| matches!( + op, + GcOp::RefCastDownward { + sub_type_index: 2, + super_type_index: 0, + } + )), + "valid downcast pair should be preserved: {:#?}", + ops.ops + ); + assert_valid_wasm(&mut ops); +} + +/// Case 2: wrong pair — super (operand) is kept, sub (result) is +/// repaired by finding a direct subtype of the super. +#[test] +fn downcast_wrong_pair_repaired_finds_subtype() { + let _ = env_logger::try_init(); + // sub=0 (TypeId(1)) is NOT a subtype of super=1 (TypeId(2)). + // super stays at 1 (it's the stack operand). + // Scan finds TypeId(3) at index 2 as a direct subtype of TypeId(2). + // Repaired to { sub: 2, super: 1 }. + let mut ops = cast_test_ops(vec![GcOp::RefCastDownward { + sub_type_index: 0, + super_type_index: 1, + }]); + ops.fixup(&mut Vec::new()); + + assert!( + ops.ops.iter().any(|op| matches!( + op, + GcOp::RefCastDownward { + sub_type_index: 2, + super_type_index: 1, + } + )), + "downcast should keep super and find a valid sub: {:#?}", + ops.ops + ); + assert_valid_wasm(&mut ops); +} + +/// Case 3: super has no subtypes — falls back to self-cast keeping +/// the super (operand) fixed. +#[test] +fn downcast_no_subtype_self_cast() { + let _ = env_logger::try_init(); + // super=2 (TypeId(3)) is the leaf — no type has it as a supertype. + // Self-cast: { sub: 2, super: 2 }. + let mut ops = cast_test_ops(vec![GcOp::RefCastDownward { + sub_type_index: 1, + super_type_index: 2, + }]); + ops.fixup(&mut Vec::new()); + + assert!( + ops.ops.iter().any(|op| matches!( + op, + GcOp::RefCastDownward { + sub_type_index: 2, + super_type_index: 2, + } + )), + "downcast with no subtypes should self-cast keeping super: {:#?}", + ops.ops + ); + assert_valid_wasm(&mut ops); +} + +/// Case 4: flat hierarchy — self-cast. +#[test] +fn downcast_flat_hierarchy_self_cast() { + let _ = env_logger::try_init(); + let mut ops = flat_cast_test_ops(vec![GcOp::RefCastDownward { + sub_type_index: 0, + super_type_index: 1, + }]); + ops.fixup(&mut Vec::new()); + + assert!( + ops.ops.iter().any(|op| matches!( + op, + GcOp::RefCastDownward { + sub_type_index, + super_type_index, + } if sub_type_index == super_type_index + )), + "downcast in flat hierarchy should become self-cast: {:#?}", + ops.ops + ); + assert_valid_wasm(&mut ops); +} diff --git a/crates/fuzzing/src/generators/gc_ops/types.rs b/crates/fuzzing/src/generators/gc_ops/types.rs index 9b77250bbd9b..7e8ef3481354 100644 --- a/crates/fuzzing/src/generators/gc_ops/types.rs +++ b/crates/fuzzing/src/generators/gc_ops/types.rs @@ -708,6 +708,106 @@ impl StackType { log::trace!("[StackType::emit] leave stack={stack:?}"); } + /// Fixup for cast ops: ensures the sub/super type relationship actually + /// holds. Always repairs the op rather than dropping it. + /// + /// For upcast the operand (sub) is on the stack, so we keep sub fixed + /// and adjust super. For downcast the operand (super) is on the stack, + /// so we keep super fixed and adjust sub. + pub fn fixup_cast(op: GcOp, types: &Types, encoding_order: &[TypeId]) -> GcOp { + match op { + GcOp::RefCastUpward { + sub_type_index, + super_type_index, + } => { + // Operand is sub (on the stack) — keep it, fix super. + let super_type_index = Self::find_supertype_of( + sub_type_index, + super_type_index, + types, + encoding_order, + ); + GcOp::RefCastUpward { + sub_type_index, + super_type_index, + } + } + GcOp::RefCastDownward { + sub_type_index, + super_type_index, + } => { + // Operand is super (on the stack) — keep it, fix sub. + let sub_type_index = + Self::find_subtype_of(super_type_index, sub_type_index, types, encoding_order); + GcOp::RefCastDownward { + sub_type_index, + super_type_index, + } + } + other => other, + } + } + + /// Given a sub type on the stack, find a valid super_type_index such + /// that sub <: super. Keeps sub fixed. Falls back to self-cast. + fn find_supertype_of( + sub_type_index: u32, + super_type_index: u32, + types: &Types, + encoding_order: &[TypeId], + ) -> u32 { + if let (Some(&sub_tid), Some(&super_tid)) = ( + encoding_order + .get(usize::try_from(sub_type_index).expect("sub_type_index is out of bounds")), + encoding_order + .get(usize::try_from(super_type_index).expect("super_type_index is out of bounds")), + ) { + // Already valid. + if types.is_subtype(sub_tid, super_tid) { + return super_type_index; + } + // Try sub's direct supertype. + if let Some(actual_super) = types.type_defs.get(&sub_tid).and_then(|d| d.supertype) { + if let Some(idx) = encoding_order.iter().position(|&t| t == actual_super) { + return u32::try_from(idx).expect("too many types for index"); + } + } + } + // Self-cast. + sub_type_index + } + + /// Given a super type on the stack, find a valid sub_type_index such + /// that sub <: super. Keeps super fixed. Falls back to self-cast. + fn find_subtype_of( + super_type_index: u32, + sub_type_index: u32, + types: &Types, + encoding_order: &[TypeId], + ) -> u32 { + if let (Some(&sub_tid), Some(&super_tid)) = ( + encoding_order + .get(usize::try_from(sub_type_index).expect("sub_type_index is out of bounds")), + encoding_order + .get(usize::try_from(super_type_index).expect("super_type_index is out of bounds")), + ) { + // Already valid. + if types.is_subtype(sub_tid, super_tid) { + return sub_type_index; + } + // Try to find any direct subtype of super. + for (idx, tid) in encoding_order.iter().enumerate() { + if let Some(def) = types.type_defs.get(tid) { + if def.supertype == Some(super_tid) { + return u32::try_from(idx).expect("too many types for index"); + } + } + } + } + // Self-cast. + super_type_index + } + /// Clamp a type index to the number of types. fn clamp(t: u32, n: u32) -> u32 { if n == 0 { 0 } else { t % n }