Stream: git-wasmtime

Topic: wasmtime / issue #14845 Panic in GC with guest-debug enabled


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

alexcrichton opened issue #14845:

This input:

(module
  (type $s (struct (field i32)))
  (func (export "main") (param $n i32) (result anyref)
    (loop $l
      (drop (struct.new $s (i32.const 0x30)))
      (br_if $l (local.tee $n (i32.sub (local.get $n) (i32.const 1)))))
    (ref.null any)))

fails with:

$ cargo run run -D guest-debug --invoke main ../wasmtime-vuln-triage/bug-in-triage/0163-gc-guest-debug-cold-libcall-wrong-stack-shape/foo.wat 10000
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.10s
     Running `target/debug/wasmtime run -D guest-debug --invoke main ../wasmtime-vuln-triage/bug-in-triage/0163-gc-guest-debug-cold-libcall-wrong-stack-shape/foo.wat 10000`
warning: using `--invoke` with a function that takes arguments is experimental and may break in the future

thread 'main' (408509) panicked at crates/wasmtime/src/runtime/vm/gc/enabled/copying.rs:1044:13:
assertion failed: self.heap.is_in_idle_space(old_index)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

An LLM summary, if helpful, is:

<details>

With Config::guest_debug(true) and the copying collector, a GC that
happens inside a libcall (for example the gc_alloc_raw slow path of
struct.new) traces the frame's debug state slot using the stack shape of
some earlier program point. That can make the collector treat a live
non-reference operand, such as an i32, as a GC root.

Reproduction

gc-libcall.wat:

(module
  (type $s (struct (field i32)))
  (func (export "main") (param $n i32) (result anyref)
    (loop $l
      (drop (struct.new $s (i32.const 0x30)))
      (br_if $l (local.tee $n (i32.sub (local.get $n) (i32.const 1)))))
    (ref.null any)))

With a debug build of the CLI at bytecodealliance/wasmtime@68b32edee9 (GC,
function references and the copying collector are all on by default):

$ wasmtime run -D guest-debug --invoke main gc-libcall.wat 10000
thread 'main' panicked at crates/wasmtime/src/runtime/vm/gc/enabled/copying.rs:1044:13:
assertion failed: self.heap.is_in_idle_space(old_index)

$ wasmtime run --invoke main gc-libcall.wat 10000
<null anyref>

The argument must be large enough for an allocation to trigger a GC: here
2045 iterations still pass and 2046 panic.

The value that ends up treated as a root is the guest's i32 operand, so
other constants fail differently. With 0x100, the 49.0.3 release CLI
aborts with a panic at crates/environ/src/gc.rs:523:32. A release build of
main showed no visible effect for 0x30, 0x100 or 0x7fffffff. But it
still reads a guest-chosen value as a GC reference, so wrong results are
possible.

Cause

The normal stack maps aren't affected; this is only about the debug state
slot. Any libcall that can GC has the same problem, not just gc_alloc_raw:
for example other allocation paths, explicit collections, table or array
growth, and epoch or fuel yields that run host code.

Possible fix

One option is to tag every call that doesn't already have tags with the
tags of the breakpoint of the operator it was emitted for. The state slot's
operand stack is only flushed between operators (update_state_slot_stack,
func_environ.rs:1358, called from after_translate_operator). So for
the whole of an operator's translation, the slot has the shape recorded at
that operator's breakpoint. For example:

// In `before_translate_operator`, after setting the breakpoint's tags:
self.breakpoints.insert(builder.srcloc(), inst);

// At the end of the function (e.g. from `finish_debug_metadata`), for each
// `call`, `call_indirect`, `try_call` or `try_call_indirect` without tags:
if let Some(bp) = self.breakpoints.get(&func.srcloc(inst)) {
    func.debug_tags.clone_tags(*bp, inst);
}

With that, the reproducer passes. tests/all and tests/disas also pass;
tests/disas/debug-exceptions.wat only gains debug frame state lines on
libcalls.

Making find_program_point(.., Post) require an exact match, so the
frame contributes no debug roots, isn't enough with a moving collector. The
state slot's references wouldn't be updated, and the debugger would later
read stale values.

Open question: calls emitted before the first operator (the function-entry
stack-limit check and fuel/epoch setup) have no breakpoint to copy tags
from, and would stay untagged. If one of them can GC while the state slot
holds reference-typed parameters, it needs its own program point. I haven't
checked this.

</details>

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

alexcrichton added the wasmtime:debugging label to Issue #14845.

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

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

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

alexcrichton commented on issue #14845:

cc @cfallin


Last updated: Oct 31 2026 at 23:03 UTC