khagankhan opened issue #14463:
Summary
When a host function that Wasm called uses
Func::callto call a second host function, and that second function collects, the Wasm frames beneath them are scanned twice. Under the copying collector, an object held by a Wasm local is then moved twice: the local ends up pointing at a different copy than every other reference to the same object, soref.eqbetween them returns 0.If the first host function collects directly, without the nested call, the result is correct.
Test case
use wasmtime::*; fn main() -> Result<()> { let mut config = Config::new(); config.wasm_gc(true); config.wasm_function_references(true); config.collector(Collector::Copying); let engine = Engine::new(&config)?; let module = Module::new( &engine, r#" (module (type $s (struct (field i32))) (type $f (func)) (table (export "t") 1 funcref) (global $g (mut (ref null $s)) (ref.null $s)) (func (export "run") (result i32) (local $x (ref null $s)) (local.set $x (struct.new $s (i32.const 7))) (global.set $g (local.get $x)) (call_indirect (type $f) (i32.const 0)) (ref.eq (local.get $x) (global.get $g)))) "#, )?; let mut store = Store::new(&engine, ()); let inner = Func::wrap(&mut store, |mut caller: Caller<'_, ()>| caller.gc(None)); let outer = Func::wrap(&mut store, move |mut caller: Caller<'_, ()>| { inner.call(&mut caller, &[], &mut []) }); let instance = Instance::new(&mut store, &module, &[])?; let table = instance.get_table(&mut store, "t").unwrap(); table.set(&mut store, 0, Ref::Func(Some(outer)))?; let run = instance.get_typed_func::<(), i32>(&mut store, "run")?; println!("local ref.eq global = {}", run.call(&mut store, ())?); Ok(()) }Steps to reproduce
Build the program above against Wasmtime with default features and run it:
cargo run cargo run --releaseExpected results
Both build should print like DRC does:
local ref.eq global = 1Actual results
Debug build:
thread 'main' panicked at crates/wasmtime/src/runtime/vm/gc/enabled/copying.rs:1043:13: assertion failed: self.heap.is_in_idle_space(old_index)Release build:
local ref.eq global = 0
khagankhan added the bug label to Issue #14463.
khagankhan added the fuzz-bug label to Issue #14463.
alexcrichton added the wasm-proposal:gc label to Issue #14463.
fitzgen assigned fitzgen to issue #14463.
fitzgen closed issue #14463:
Summary
When a host function that Wasm called uses
Func::callto call a second host function, and that second function collects, the Wasm frames beneath them are scanned twice. Under the copying collector, an object held by a Wasm local is then moved twice: the local ends up pointing at a different copy than every other reference to the same object, soref.eqbetween them returns 0.If the first host function collects directly, without the nested call, the result is correct.
Test case
use wasmtime::*; fn main() -> Result<()> { let mut config = Config::new(); config.wasm_gc(true); config.wasm_function_references(true); config.collector(Collector::Copying); let engine = Engine::new(&config)?; let module = Module::new( &engine, r#" (module (type $s (struct (field i32))) (type $f (func)) (table (export "t") 1 funcref) (global $g (mut (ref null $s)) (ref.null $s)) (func (export "run") (result i32) (local $x (ref null $s)) (local.set $x (struct.new $s (i32.const 7))) (global.set $g (local.get $x)) (call_indirect (type $f) (i32.const 0)) (ref.eq (local.get $x) (global.get $g)))) "#, )?; let mut store = Store::new(&engine, ()); let inner = Func::wrap(&mut store, |mut caller: Caller<'_, ()>| caller.gc(None)); let outer = Func::wrap(&mut store, move |mut caller: Caller<'_, ()>| { inner.call(&mut caller, &[], &mut []) }); let instance = Instance::new(&mut store, &module, &[])?; let table = instance.get_table(&mut store, "t").unwrap(); table.set(&mut store, 0, Ref::Func(Some(outer)))?; let run = instance.get_typed_func::<(), i32>(&mut store, "run")?; println!("local ref.eq global = {}", run.call(&mut store, ())?); Ok(()) }Steps to reproduce
Build the program above against Wasmtime with default features and run it:
cargo run cargo run --releaseExpected results
Both build should print like DRC does:
local ref.eq global = 1Actual results
Debug build:
thread 'main' panicked at crates/wasmtime/src/runtime/vm/gc/enabled/copying.rs:1043:13: assertion failed: self.heap.is_in_idle_space(old_index)Release build:
local ref.eq global = 0
Last updated: Oct 11 2026 at 04:10 UTC