Stream: git-wasmtime

Topic: wasmtime / issue #14463 gc_ops fuzzbug: Copying collector...


view this post on Zulip Wasmtime GitHub notifications bot (Oct 01 2026 at 08:10):

khagankhan opened issue #14463:

Summary

When a host function that Wasm called uses Func::call to 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, so ref.eq between 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 --release

Expected results

Both build should print like DRC does:

local ref.eq global = 1

Actual 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

view this post on Zulip Wasmtime GitHub notifications bot (Oct 01 2026 at 08:10):

khagankhan added the bug label to Issue #14463.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 01 2026 at 08:10):

khagankhan added the fuzz-bug label to Issue #14463.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 01 2026 at 19:25):

alexcrichton added the wasm-proposal:gc label to Issue #14463.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 05 2026 at 17:14):

fitzgen assigned fitzgen to issue #14463.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 05 2026 at 22:47):

fitzgen closed issue #14463:

Summary

When a host function that Wasm called uses Func::call to 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, so ref.eq between 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 --release

Expected results

Both build should print like DRC does:

local ref.eq global = 1

Actual 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