From f07c4416c3754b82327558cf5b4ea736e666e70b Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Tue, 1 Oct 2024 09:36:42 -0700 Subject: [PATCH] Add support for funcrefs inside GC objects (#9341) --- crates/cranelift/src/gc/enabled.rs | 93 ++++++++++++++++-- crates/cranelift/src/gc/enabled/drc.rs | 12 +-- crates/environ/src/builtin.rs | 37 ++++++- crates/wasmtime/src/runtime/vm/gc.rs | 7 ++ .../src/runtime/vm/gc/enabled/arrayref.rs | 46 +++++++-- crates/wasmtime/src/runtime/vm/gc/func_ref.rs | 96 +++++++++++++++++++ crates/wasmtime/src/runtime/vm/libcalls.rs | 54 +++++++++++ tests/disas/gc/funcref-in-gc-heap-get.wat | 40 ++++++++ tests/disas/gc/funcref-in-gc-heap-new.wat | 49 ++++++++++ tests/disas/gc/funcref-in-gc-heap-set.wat | 39 ++++++++ .../gc/func-refs-in-gc-heap.wast | 79 +++++++++++++++ 11 files changed, 526 insertions(+), 26 deletions(-) create mode 100644 crates/wasmtime/src/runtime/vm/gc/func_ref.rs create mode 100644 tests/disas/gc/funcref-in-gc-heap-get.wat create mode 100644 tests/disas/gc/funcref-in-gc-heap-new.wat create mode 100644 tests/disas/gc/funcref-in-gc-heap-set.wat create mode 100644 tests/misc_testsuite/gc/func-refs-in-gc-heap.wast diff --git a/crates/cranelift/src/gc/enabled.rs b/crates/cranelift/src/gc/enabled.rs index 42fb6bf10bb1..d5c5d8e2c99e 100644 --- a/crates/cranelift/src/gc/enabled.rs +++ b/crates/cranelift/src/gc/enabled.rs @@ -7,11 +7,12 @@ use cranelift_codegen::{ cursor::FuncCursor, ir::{self, condcodes::IntCC, InstBuilder}, }; +use cranelift_entity::packed_option::ReservedValue; use cranelift_frontend::FunctionBuilder; use wasmtime_environ::{ - wasm_unsupported, GcArrayLayout, GcLayout, GcStructLayout, ModuleInternedTypeIndex, PtrSize, - TypeIndex, WasmCompositeType, WasmHeapTopType, WasmHeapType, WasmRefType, WasmResult, - WasmStorageType, WasmValType, I31_DISCRIMINANT, NON_NULL_NON_I31_MASK, + GcArrayLayout, GcLayout, GcStructLayout, ModuleInternedTypeIndex, PtrSize, TypeIndex, + WasmCompositeType, WasmHeapTopType, WasmHeapType, WasmRefType, WasmResult, WasmStorageType, + WasmValType, I31_DISCRIMINANT, NON_NULL_NON_I31_MASK, }; mod drc; @@ -86,9 +87,40 @@ fn read_field_at_addr( WasmHeapTopType::Any | WasmHeapTopType::Extern => gc_compiler(func_env)? .translate_read_gc_reference(func_env, builder, r, addr, flags)?, WasmHeapTopType::Func => { - return Err(wasm_unsupported!( - "funcrefs inside the GC heap are not yet implemented" - )); + let expected_ty = match r.heap_type { + WasmHeapType::Func => ModuleInternedTypeIndex::reserved_value(), + WasmHeapType::ConcreteFunc(ty) => ty.unwrap_module_type_index(), + WasmHeapType::NoFunc => { + let null = builder.ins().iconst(func_env.pointer_type(), 0); + if !r.nullable { + // Because `nofunc` is uninhabited, and this + // reference is non-null, this is unreachable + // code. Unconditionally trap via conditional + // trap instructions to avoid inserting block + // terminators in the middle of this block. + builder + .ins() + .trapz(null, ir::TrapCode::User(DEBUG_ASSERT_TRAP_CODE)); + } + return Ok(null); + } + _ => unreachable!("not a function heap type"), + }; + let expected_ty = builder + .ins() + .iconst(ir::types::I32, i64::from(expected_ty.as_bits())); + + let vmctx = func_env.vmctx_val(&mut builder.cursor()); + + let func_ref_id = builder.ins().load(ir::types::I32, flags, addr, 0); + let get_interned_func_ref = func_env + .builtin_functions + .get_interned_func_ref(builder.func); + + let call_inst = builder + .ins() + .call(get_interned_func_ref, &[vmctx, func_ref_id, expected_ty]); + builder.func.dfg.first_result(call_inst) } }, }, @@ -103,6 +135,51 @@ fn read_field_at_addr( Ok(value) } +fn write_func_ref_at_addr( + func_env: &mut FuncEnvironment<'_>, + builder: &mut FunctionBuilder<'_>, + ref_type: WasmRefType, + flags: ir::MemFlags, + field_addr: ir::Value, + func_ref: ir::Value, +) -> WasmResult<()> { + assert_eq!(ref_type.heap_type.top(), WasmHeapTopType::Func); + + let vmctx = func_env.vmctx_val(&mut builder.cursor()); + + let intern_func_ref_for_gc_heap = func_env + .builtin_functions + .intern_func_ref_for_gc_heap(builder.func); + + let func_ref = if ref_type.heap_type == WasmHeapType::NoFunc { + let null = builder.ins().iconst(func_env.pointer_type(), 0); + if !ref_type.nullable { + // Because `nofunc` is uninhabited, and this reference is + // non-null, this is unreachable code. Unconditionally trap + // via conditional trap instructions to avoid inserting + // block terminators in the middle of this block. + builder + .ins() + .trapz(null, ir::TrapCode::User(DEBUG_ASSERT_TRAP_CODE)); + } + null + } else { + func_ref + }; + + // Convert the raw `funcref` into a `FuncRefTableId` for use in the + // GC heap. + let call_inst = builder + .ins() + .call(intern_func_ref_for_gc_heap, &[vmctx, func_ref]); + let func_ref_id = builder.func.dfg.first_result(call_inst); + + // Store the id in the field. + builder.ins().store(flags, func_ref_id, field_addr, 0); + + Ok(()) +} + fn write_field_at_addr( func_env: &mut FuncEnvironment<'_>, builder: &mut FunctionBuilder<'_>, @@ -121,9 +198,7 @@ fn write_field_at_addr( builder.ins().istore16(flags, new_val, field_addr, 0); } WasmStorageType::Val(WasmValType::Ref(r)) if r.heap_type.top() == WasmHeapTopType::Func => { - return Err(wasm_unsupported!( - "funcrefs inside the GC heap are not yet implemented" - )) + write_func_ref_at_addr(func_env, builder, r, flags, field_addr, new_val)?; } WasmStorageType::Val(WasmValType::Ref(r)) => { gc_compiler(func_env)? diff --git a/crates/cranelift/src/gc/enabled/drc.rs b/crates/cranelift/src/gc/enabled/drc.rs index 44dc4ad99e1f..a95f94348622 100644 --- a/crates/cranelift/src/gc/enabled/drc.rs +++ b/crates/cranelift/src/gc/enabled/drc.rs @@ -3,7 +3,7 @@ use super::{ emit_array_fill_impl, uextend_i32_to_pointer_type, unbarriered_load_gc_ref, - unbarriered_store_gc_ref, BoundsCheck, Offset, + unbarriered_store_gc_ref, write_func_ref_at_addr, BoundsCheck, Offset, }; use crate::gc::{gc_compiler, ArrayInit}; use crate::translate::TargetEnvironment; @@ -13,9 +13,9 @@ use cranelift_codegen::ir::{self, InstBuilder}; use cranelift_frontend::FunctionBuilder; use smallvec::SmallVec; use wasmtime_environ::{ - drc::DrcTypeLayouts, wasm_unsupported, GcArrayLayout, GcTypeLayouts, ModuleInternedTypeIndex, - PtrSize, TypeIndex, VMGcKind, WasmCompositeType, WasmHeapTopType, WasmHeapType, WasmRefType, - WasmResult, WasmStorageType, WasmValType, + drc::DrcTypeLayouts, GcArrayLayout, GcTypeLayouts, ModuleInternedTypeIndex, PtrSize, TypeIndex, + VMGcKind, WasmCompositeType, WasmHeapTopType, WasmHeapType, WasmRefType, WasmResult, + WasmStorageType, WasmValType, }; #[derive(Default)] @@ -135,9 +135,7 @@ impl DrcCompiler { WasmStorageType::Val(WasmValType::Ref(r)) if r.heap_type.top() == WasmHeapTopType::Func => { - return Err(wasm_unsupported!( - "funcrefs inside the GC heap are not yet implemented" - )); + write_func_ref_at_addr(func_env, builder, r, flags, field_addr, val)?; } WasmStorageType::Val(WasmValType::Ref(r)) => { self.translate_init_gc_reference(func_env, builder, r, field_addr, val, flags)?; diff --git a/crates/environ/src/builtin.rs b/crates/environ/src/builtin.rs index 2adb9fcf1142..e904cb8c8d2a 100644 --- a/crates/environ/src/builtin.rs +++ b/crates/environ/src/builtin.rs @@ -90,6 +90,40 @@ macro_rules! foreach_builtin_function { align: i32 ) -> reference; + // Intern a `funcref` into the GC heap, returning its + // `FuncRefTableId`. + // + // This libcall may not GC. + #[cfg(feature = "gc")] + intern_func_ref_for_gc_heap( + vmctx: vmctx, + func_ref: pointer + ) -> i32; + + // Get the raw `VMFuncRef` pointer associated with a + // `FuncRefTableId` from an earlier `intern_func_ref_for_gc_heap` + // call. + // + // This libcall may not GC. + // + // Passes in the `ModuleInternedTypeIndex` of the funcref's expected + // type, or `ModuleInternedTypeIndex::reserved_value()` if we are + // getting the function reference as an untyped `funcref` rather + // than a typed `(ref $ty)`. + // + // TODO: We will want to eventually expose the table directly to + // Wasm code, so that it doesn't need to make a libcall to go from + // id to `VMFuncRef`. That will be a little tricky: it will also + // require updating the pointer to the slab in the `VMContext` (or + // `VMRuntimeLimits` or wherever we put it) when the slab is + // resized. + #[cfg(feature = "gc")] + get_interned_func_ref( + vmctx: vmctx, + func_ref_id: i32, + module_interned_type_index: i32 + ) -> pointer; + // Builtin implementation of the `array.new_data` instruction. #[cfg(feature = "gc")] array_new_data( @@ -141,8 +175,9 @@ macro_rules! foreach_builtin_function { vmctx: vmctx, array_interned_type_index: i32, array: reference, - dst_index: i32, + dst: i32, elem_index: i32, + src: i32, len: i32 ); diff --git a/crates/wasmtime/src/runtime/vm/gc.rs b/crates/wasmtime/src/runtime/vm/gc.rs index 82601a1a130f..7500a68fc141 100644 --- a/crates/wasmtime/src/runtime/vm/gc.rs +++ b/crates/wasmtime/src/runtime/vm/gc.rs @@ -8,11 +8,13 @@ mod disabled; #[cfg(not(feature = "gc"))] pub use disabled::*; +mod func_ref; mod gc_ref; mod gc_runtime; mod host_data; mod i31; +pub use func_ref::*; pub use gc_ref::*; pub use gc_runtime::*; pub use host_data::*; @@ -42,16 +44,21 @@ pub struct GcStore { /// The `externref` host data table for this GC heap. pub host_data_table: ExternRefHostDataTable, + + /// The function-references table for this GC heap. + pub func_ref_table: FuncRefTable, } impl GcStore { /// Create a new `GcStore`. pub fn new(allocation_index: GcHeapAllocationIndex, gc_heap: Box) -> Self { let host_data_table = ExternRefHostDataTable::default(); + let func_ref_table = FuncRefTable::default(); Self { allocation_index, gc_heap, host_data_table, + func_ref_table, } } diff --git a/crates/wasmtime/src/runtime/vm/gc/enabled/arrayref.rs b/crates/wasmtime/src/runtime/vm/gc/enabled/arrayref.rs index 41c9da773b94..cf911547d39b 100644 --- a/crates/wasmtime/src/runtime/vm/gc/enabled/arrayref.rs +++ b/crates/wasmtime/src/runtime/vm/gc/enabled/arrayref.rs @@ -3,7 +3,8 @@ use crate::{ prelude::*, runtime::vm::{GcHeap, GcStore, VMGcRef}, store::{AutoAssertNoGc, StoreOpaque}, - AnyRef, ExternRef, HeapType, RootedGcRefImpl, StorageType, Val, ValType, + vm::{FuncRefTableId, SendSyncPtr}, + AnyRef, ExternRef, Func, HeapType, RootedGcRefImpl, StorageType, Val, ValType, }; use core::fmt; use wasmtime_environ::{GcArrayLayout, VMGcKind}; @@ -163,7 +164,20 @@ impl VMArrayRef { let raw = data.read_u32(offset); Val::AnyRef(AnyRef::_from_raw(store, raw)) } - HeapType::Func => todo!("funcrefs inside gc objects not yet implemented"), + HeapType::Func => { + let func_ref_id = data.read_u32(offset); + let func_ref_id = FuncRefTableId::from_raw(func_ref_id); + let func_ref = store + .unwrap_gc_store() + .func_ref_table + .get_untyped(func_ref_id); + Val::FuncRef(unsafe { + Func::from_vm_func_ref( + store, + func_ref.map_or(core::ptr::null_mut(), |f| f.as_ptr()), + ) + }) + } otherwise => unreachable!("not a top type: {otherwise:?}"), }, } @@ -236,7 +250,17 @@ impl VMArrayRef { data.write_u32(offset, gc_ref.map_or(0, |r| r.as_raw_u32())); } - Val::FuncRef(_) => todo!("funcrefs inside gc objects not yet implemented"), + Val::FuncRef(f) => { + let func_ref = match f { + Some(f) => Some(SendSyncPtr::new(f.vm_func_ref(store))), + None => None, + }; + let id = unsafe { store.gc_store_mut()?.func_ref_table.intern(func_ref) }; + store + .gc_store_mut()? + .gc_object_data(self.as_gc_ref()) + .write_u32(offset, id.into_raw()); + } } Ok(()) } @@ -329,12 +353,16 @@ impl VMArrayRef { .write_u32(offset, x); } - Val::FuncRef(_) => { - // TODO: we can't trust the GC heap, which means we can't read - // native VMFuncRef pointers out of it and trust them. That - // means we need to do the same side table kind of thing we do - // with `externref` host data here. This isn't implemented yet. - bail!("funcrefs in GC objects are not yet implemented") + Val::FuncRef(f) => { + let func_ref = match f { + Some(f) => Some(SendSyncPtr::new(f.vm_func_ref(store))), + None => None, + }; + let id = unsafe { store.gc_store_mut()?.func_ref_table.intern(func_ref) }; + store + .gc_store_mut()? + .gc_object_data(self.as_gc_ref()) + .write_u32(offset, id.into_raw()); } } Ok(()) diff --git a/crates/wasmtime/src/runtime/vm/gc/func_ref.rs b/crates/wasmtime/src/runtime/vm/gc/func_ref.rs new file mode 100644 index 000000000000..5146dfdfaf89 --- /dev/null +++ b/crates/wasmtime/src/runtime/vm/gc/func_ref.rs @@ -0,0 +1,96 @@ +//! Implementation of the side table for `funcref`s in the GC heap. +//! +//! The actual `VMFuncRef`s are kept in a side table, rather than inside the GC +//! heap, for the same reasons that an `externref`'s host data is kept in a side +//! table. We cannot trust any data coming from the GC heap, but `VMFuncRef`s +//! contain raw pointers, so if we stored `VMFuncRef`s inside the GC heap, we +//! wouldn't be able to use the raw pointers from any `VMFuncRef` we got out of +//! the heap. And that means we wouldn't be able to, for example, call a +//! `funcref` we got from inside the GC heap. + +use crate::{ + hash_map::HashMap, + type_registry::TypeRegistry, + vm::{SendSyncPtr, VMFuncRef}, +}; +use wasmtime_environ::VMSharedTypeIndex; +use wasmtime_slab::{Id, Slab}; + +/// An identifier into the `FuncRefTable`. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct FuncRefTableId(Id); + +impl FuncRefTableId { + /// Convert this `FuncRefTableId` into its raw `u32` ID. + pub fn into_raw(self) -> u32 { + self.0.into_raw() + } + + /// Create a `FuncRefTableId` from a raw `u32` ID. + pub fn from_raw(raw: u32) -> Self { + Self(Id::from_raw(raw)) + } +} + +/// Side table mapping `FuncRefTableId`s that can be stored in the GC heap to +/// raw `VMFuncRef`s. +#[derive(Default)] +pub struct FuncRefTable { + interned: HashMap>, FuncRefTableId>, + slab: Slab>>, +} + +impl FuncRefTable { + /// Intern a `VMFuncRef` in the side table, returning an ID that can be + /// stored in the GC heap. + /// + /// # Safety + /// + /// The given `func_ref` must point to a valid `VMFuncRef` and must remain + /// valid for the duration of this table's lifetime. + pub unsafe fn intern(&mut self, func_ref: Option>) -> FuncRefTableId { + *self + .interned + .entry(func_ref) + .or_insert_with(|| FuncRefTableId(self.slab.alloc(func_ref))) + } + + /// Get the `VMFuncRef` associated with the given ID. + /// + /// Checks that the `VMFuncRef` is a subtype of the expected type. + pub fn get_typed( + &self, + types: &TypeRegistry, + id: FuncRefTableId, + expected_ty: VMSharedTypeIndex, + ) -> Option> { + let f = self.slab.get(id.0).copied().expect("bad FuncRefTableId"); + + if let Some(f) = f { + // The safety contract for `intern` ensures that deref'ing `f` is safe. + let actual_ty = unsafe { f.as_ref().type_index }; + + // Ensure that the funcref actually is a subtype of the expected + // type. This protects against GC heap corruption being leveraged in + // attacks: if the attacker has a write gadget inside the GC heap, they + // can overwrite a funcref ID to point to a different funcref, but this + // assertion ensures that any calls to that wrong funcref at least + // remain well-typed, which reduces the attack surface and maintains + // memory safety. + assert!(types.is_subtype(actual_ty, expected_ty)); + } + + f + } + + /// Get the `VMFuncRef` associated with the given ID, without checking the + /// type. + /// + /// Prefer `get_typed`. This method is only suitable for getting a + /// `VMFuncRef` as an untyped `funcref` function reference, and never as a + /// typed `(ref $some_func_type)` function reference. + pub fn get_untyped(&self, id: FuncRefTableId) -> Option> { + self.slab.get(id.0).copied().expect("bad FuncRefTableId") + } +} diff --git a/crates/wasmtime/src/runtime/vm/libcalls.rs b/crates/wasmtime/src/runtime/vm/libcalls.rs index b5f1c6d62a84..83c4ec1fe717 100644 --- a/crates/wasmtime/src/runtime/vm/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/libcalls.rs @@ -501,6 +501,59 @@ unsafe fn gc_alloc_raw( Ok(gc_ref.as_raw_u32()) } +// Intern a `funcref` into the GC heap, returning its `FuncRefTableId`. +// +// This libcall may not GC. +#[cfg(feature = "gc")] +unsafe fn intern_func_ref_for_gc_heap(instance: &mut Instance, func_ref: *mut u8) -> Result { + use crate::{store::AutoAssertNoGc, vm::SendSyncPtr}; + use core::ptr::NonNull; + + let mut store = AutoAssertNoGc::new((*instance.store()).store_opaque_mut()); + + let func_ref = func_ref.cast::(); + let func_ref = NonNull::new(func_ref).map(SendSyncPtr::new); + + let func_ref_id = store.unwrap_gc_store_mut().func_ref_table.intern(func_ref); + Ok(func_ref_id.into_raw()) +} + +// Get the raw `VMFuncRef` pointer associated with a `FuncRefTableId` from an +// earlier `intern_func_ref_for_gc_heap` call. +// +// This libcall may not GC. +#[cfg(feature = "gc")] +unsafe fn get_interned_func_ref( + instance: &mut Instance, + func_ref_id: u32, + module_interned_type_index: u32, +) -> *mut u8 { + use super::FuncRefTableId; + use crate::store::AutoAssertNoGc; + use wasmtime_environ::{packed_option::ReservedValue, ModuleInternedTypeIndex}; + + let store = AutoAssertNoGc::new((*instance.store()).store_opaque_mut()); + + let func_ref_id = FuncRefTableId::from_raw(func_ref_id); + let module_interned_type_index = ModuleInternedTypeIndex::from_bits(module_interned_type_index); + + let func_ref = if module_interned_type_index.is_reserved_value() { + store + .unwrap_gc_store() + .func_ref_table + .get_untyped(func_ref_id) + } else { + let types = store.engine().signatures(); + let engine_ty = instance.engine_type_index(module_interned_type_index); + store + .unwrap_gc_store() + .func_ref_table + .get_typed(types, func_ref_id, engine_ty) + }; + + func_ref.map_or(core::ptr::null_mut(), |f| f.as_ptr().cast()) +} + /// Implementation of the `array.new_data` instruction. #[cfg(feature = "gc")] unsafe fn array_new_data( @@ -761,6 +814,7 @@ unsafe fn array_init_elem( _array: u32, _dst_index: u32, _elem_index: u32, + _src: u32, _len: u32, ) -> Result<()> { bail!("the `array.init_elem` instruction is not yet implemented") diff --git a/tests/disas/gc/funcref-in-gc-heap-get.wat b/tests/disas/gc/funcref-in-gc-heap-get.wat new file mode 100644 index 000000000000..90424482b02a --- /dev/null +++ b/tests/disas/gc/funcref-in-gc-heap-get.wat @@ -0,0 +1,40 @@ +;;! target = "x86_64" +;;! flags = "-W function-references,gc" +;;! test = "optimize" + +(module + (type $ty (struct (field (mut funcref)))) + + (func (param (ref $ty)) (result funcref) + (struct.get $ty 0 (local.get 0)) + ) +) +;; function u0:0(i64 vmctx, i64, i32) -> i64 tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1 +;; gv3 = vmctx +;; sig0 = (i64 vmctx, i32 uext, i32 uext) -> i64 system_v +;; fn0 = colocated u1:29 sig0 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64, v2: i32): +;; @0020 trapz v2, null_reference +;; @0020 v8 = uextend.i64 v2 +;; @0020 v9 = iconst.i64 16 +;; @0020 v10 = uadd_overflow_trap v8, v9, user65535 ; v9 = 16 +;; v19 = iconst.i64 24 +;; @0020 v12 = uadd_overflow_trap v8, v19, user65535 ; v19 = 24 +;; @0020 v7 = load.i64 notrap aligned readonly v0+48 +;; @0020 v13 = icmp ule v12, v7 +;; @0020 trapz v13, user65535 +;; @0020 v6 = load.i64 notrap aligned readonly v0+40 +;; @0020 v14 = iadd v6, v10 +;; @0020 v17 = load.i32 notrap aligned little v14 +;; @0020 v15 = iconst.i32 -1 +;; @0020 v18 = call fn0(v0, v17, v15) ; v15 = -1 +;; @0024 jump block1 +;; +;; block1: +;; @0024 return v18 +;; } diff --git a/tests/disas/gc/funcref-in-gc-heap-new.wat b/tests/disas/gc/funcref-in-gc-heap-new.wat new file mode 100644 index 000000000000..d1678444c20f --- /dev/null +++ b/tests/disas/gc/funcref-in-gc-heap-new.wat @@ -0,0 +1,49 @@ +;;! target = "x86_64" +;;! flags = "-W function-references,gc" +;;! test = "optimize" + +(module + (type $ty (struct (field (mut funcref)))) + + (func (param funcref) (result (ref $ty)) + (struct.new $ty (local.get 0)) + ) +) +;; function u0:0(i64 vmctx, i64, i64) -> i32 tail { +;; ss0 = explicit_slot 4, align = 4 +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1 +;; gv3 = vmctx +;; sig0 = (i64 vmctx, i32 uext, i32 uext, i32 uext, i32 uext) -> i32 system_v +;; sig1 = (i64 vmctx, i64) -> i32 uext system_v +;; fn0 = colocated u1:27 sig0 +;; fn1 = colocated u1:28 sig1 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64, v2: i64): +;; @0020 v6 = iconst.i32 -1476395008 +;; @0020 v7 = iconst.i32 0 +;; @0020 v4 = iconst.i32 24 +;; @0020 v8 = iconst.i32 8 +;; @0020 v9 = call fn0(v0, v6, v7, v4, v8) ; v6 = -1476395008, v7 = 0, v4 = 24, v8 = 8 +;; v24 = stack_addr.i64 ss0 +;; store notrap v9, v24 +;; @0020 v13 = uextend.i64 v9 +;; @0020 v14 = iconst.i64 16 +;; @0020 v15 = uadd_overflow_trap v13, v14, user65535 ; v14 = 16 +;; v27 = iconst.i64 24 +;; @0020 v17 = uadd_overflow_trap v13, v27, user65535 ; v27 = 24 +;; @0020 v12 = load.i64 notrap aligned readonly v0+48 +;; @0020 v18 = icmp ule v17, v12 +;; @0020 trapz v18, user65535 +;; @0020 v21 = call fn1(v0, v2), stack_map=[i32 @ ss0+0] +;; @0020 v11 = load.i64 notrap aligned readonly v0+40 +;; @0020 v19 = iadd v11, v15 +;; @0020 store notrap aligned little v21, v19 +;; v22 = load.i32 notrap v24 +;; @0023 jump block1 +;; +;; block1: +;; @0023 return v22 +;; } diff --git a/tests/disas/gc/funcref-in-gc-heap-set.wat b/tests/disas/gc/funcref-in-gc-heap-set.wat new file mode 100644 index 000000000000..ed13d0d21e8a --- /dev/null +++ b/tests/disas/gc/funcref-in-gc-heap-set.wat @@ -0,0 +1,39 @@ +;;! target = "x86_64" +;;! flags = "-W function-references,gc" +;;! test = "optimize" + +(module + (type $ty (struct (field (mut funcref)))) + + (func (param (ref $ty) funcref) + (struct.set $ty 0 (local.get 0) (local.get 1)) + ) +) +;; function u0:0(i64 vmctx, i64, i32, i64) tail { +;; gv0 = vmctx +;; gv1 = load.i64 notrap aligned readonly gv0+8 +;; gv2 = load.i64 notrap aligned gv1 +;; gv3 = vmctx +;; sig0 = (i64 vmctx, i64) -> i32 uext system_v +;; fn0 = colocated u1:28 sig0 +;; stack_limit = gv2 +;; +;; block0(v0: i64, v1: i64, v2: i32, v3: i64): +;; @0022 trapz v2, null_reference +;; @0022 v8 = uextend.i64 v2 +;; @0022 v9 = iconst.i64 16 +;; @0022 v10 = uadd_overflow_trap v8, v9, user65535 ; v9 = 16 +;; v17 = iconst.i64 24 +;; @0022 v12 = uadd_overflow_trap v8, v17, user65535 ; v17 = 24 +;; @0022 v7 = load.i64 notrap aligned readonly v0+48 +;; @0022 v13 = icmp ule v12, v7 +;; @0022 trapz v13, user65535 +;; @0022 v16 = call fn0(v0, v3) +;; @0022 v6 = load.i64 notrap aligned readonly v0+40 +;; @0022 v14 = iadd v6, v10 +;; @0022 store notrap aligned little v16, v14 +;; @0026 jump block1 +;; +;; block1: +;; @0026 return +;; } diff --git a/tests/misc_testsuite/gc/func-refs-in-gc-heap.wast b/tests/misc_testsuite/gc/func-refs-in-gc-heap.wast new file mode 100644 index 000000000000..43e448d38181 --- /dev/null +++ b/tests/misc_testsuite/gc/func-refs-in-gc-heap.wast @@ -0,0 +1,79 @@ +(module + (type $f0 (func (result i32))) + + ;; Test both typed and untyped function references, as well as `nofunc` + ;; references. + (type $s0 (struct (field (mut funcref)))) + (type $s1 (struct (field (mut (ref $f0))))) + (type $s2 (struct (field (mut (ref null nofunc))))) + + (table 1 1 funcref) + + (func $f (result i32) (i32.const 0x11111111)) + (func $g (result i32) (i32.const 0x22222222)) + + (elem declare func $f $g) + + (func $alloc-s0 (export "alloc-s0") (result (ref $s0)) + (struct.new $s0 (ref.func $f)) + ) + + (func $alloc-s1 (export "alloc-s1") (result (ref $s1)) + (struct.new $s1 (ref.func $f)) + ) + + (func $alloc-s2 (export "alloc-s2") (result (ref $s2)) + (struct.new $s2 (ref.null nofunc)) + ) + + (func (export "get-s0") (result i32) + (table.set (i32.const 0) (struct.get $s0 0 (call $alloc-s0))) + (call_indirect (type $f0) (i32.const 0)) + ) + + (func (export "get-s1") (result i32) + (table.set (i32.const 0) (struct.get $s1 0 (call $alloc-s1))) + (call_indirect (type $f0) (i32.const 0)) + ) + + (func (export "get-s2") (result i32) + (table.set (i32.const 0) (struct.get $s2 0 (call $alloc-s2))) + (call_indirect (type $f0) (i32.const 0)) + ) + + (func (export "set-s0") (result i32) + (local $s (ref $s0)) + (local.set $s (call $alloc-s0)) + (struct.set $s0 0 (local.get $s) (ref.func $g)) + (table.set (i32.const 0) (struct.get $s0 0 (local.get $s))) + (call_indirect (type $f0) (i32.const 0)) + ) + + (func (export "set-s1") (result i32) + (local $s (ref $s1)) + (local.set $s (call $alloc-s1)) + (struct.set $s1 0 (local.get $s) (ref.func $g)) + (table.set (i32.const 0) (struct.get $s1 0 (local.get $s))) + (call_indirect (type $f0) (i32.const 0)) + ) + + (func (export "set-s2") (result i32) + (local $s (ref $s2)) + (local.set $s (call $alloc-s2)) + (struct.set $s2 0 (local.get $s) (ref.null nofunc)) + (table.set (i32.const 0) (struct.get $s2 0 (local.get $s))) + (call_indirect (type $f0) (i32.const 0)) + ) +) + +(assert_return (invoke "alloc-s0") (ref.struct)) +(assert_return (invoke "alloc-s1") (ref.struct)) +(assert_return (invoke "alloc-s2") (ref.struct)) + +(assert_return (invoke "get-s0") (i32.const 0x11111111)) +(assert_return (invoke "get-s1") (i32.const 0x11111111)) +(assert_trap (invoke "get-s2") "uninitialized element") + +(assert_return (invoke "set-s0") (i32.const 0x22222222)) +(assert_return (invoke "set-s1") (i32.const 0x22222222)) +(assert_trap (invoke "set-s2") "uninitialized element")