fitzgen commented on issue #14563:
<details> <summary>Full LLM report</summary>
Guest-debug GC root tracing treats
module_start's stack words as GC roots
Date 2026-10-05 Wasmtime commit 73b04cff3317d1e308866eb24359483ac6116669(main)Host macOS 15.8.1 (Darwin 24.6.0), aarch64-apple-darwinModel Claude Opus 5.5 ( claude-opus-5-5)Features Config::guest_debug(true)/-D guest-debug=y, plus Wasm GC with thecopying(default) ordrccollectorClass Wrong GC roots: stack corruption (copying), bogus refcount traffic (DRC) Summary
When guest debugging is enabled, root tracing asks the guest-debug frame table
which locals and operand-stack slots of each Wasm frame hold GC refs
(crates/wasmtime/src/runtime/store/gc.rs:796-803->
crate::debug::gc_refs_in_frame,crates/wasmtime/src/runtime/debug.rs:838).
That lookup goes throughFrameTable::find_program_point
(crates/environ/src/frame_table.rs:178-193):let index = match self.progpoint_pcs.binary_search_by_key(&key, ...) { Ok(idx) => idx, Err(idx) if idx > 0 => idx - 1, // nearest preceding program point Err(_) => return None, };The
Err(idx) => idx - 1fallback never checks that the preceding program
point belongs to the same function aspc. The compiledmodule_start
function has no guest-debug program points of its own. It runs global
initializers, element-segment setup and the start function. So it inherits
the descriptor of whichever function precedes it in the text section.
wasmtime objdump --frame-tablesshows this.When a GC happens while
module_startis on the stack, for example when an
array.new_defaultin a global initializer exhausts the heap,
gc_refs_in_framedecodes that unrelated descriptor. It then hands the
collector FP-relative slots inmodule_start's frame that hold arbitrary
words:
Copying collector (the default): forwards those words and writes the
"new" address back into the native stack, corrupting live state of compiled
code.DRC: treats them as refs and reads or marks whatever object the garbage
index names.
VirtualFrame::decode(debug.rs:684) uses the same lookup, so the debugger
API (FrameHandlelocals) reports wrong frames and values for these frames
too.No GC-heap corruption or unsafe API is needed. A valid module, a public
Configoption, andInstance::neware enough.Reproduction
repro.wast: three globals each initialized with a 100000-byte
array.new_default, and a preceding function with 32externreflocals so
the borrowed descriptor names many slots.$ target/debug/wasmtime wast -W gc=y -C collector=copying -D guest-debug=y reports/014-guest-debug-stale-frame-roots/repro.wast 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) $ target/debug/wasmtime wast -W gc=y -C collector=drc -D guest-debug=y reports/014-guest-debug-stale-frame-roots/repro.wast thread 'main' panicked at crates/wasmtime/src/runtime/vm/gc/enabled/drc.rs:567:13: every on-stack gc ref inside a Wasm frame should have be in our over-approximated stack roots set, but 0xa8000000 is not in the set
- Both commands pass without
-D guest-debug=y.- The null collector passes with or without it, because it never collects.
- The in-tree
tests/misc_testsuite/gc/issue-13247.wastfails the same way
under-D guest-debug=ywith--wasmtime-builtins.Rust API (
crate/, binaryguest_debug_roots), which shows the release-build
behaviour:$ cd reports/014-guest-debug-stale-frame-roots/crate $ cargo run --release --bin guest_debug_roots copying collector=Copying guest_debug=false: Ok(Ok(())) collector=Copying guest_debug=true: Ok(Err(BUG: gc object and/or heap misaligned ...)) $ cargo run --release --bin guest_debug_roots drc thread 'main' panicked at crates/wasmtime/src/runtime/vm/gc/gc_runtime.rs:517:9: collector=DeferredReferenceCounting guest_debug=true: Err(Any { .. })The release results depend on which stray words happen to be on the stack.
These two were stable here. Before the collector notices anything, the
copying collector has already written to the stray stack slots it accepted
as roots.
observed.txtrecords the full matrix.Suggested fix
Make the frame-table lookup function-boundary aware:
Record each function's text range in the frame table, or check that the
matched program point lies inside the function that containspc.Return no slots for functions that have no program points, such as
module_startand trampolines.Make
VirtualFrame::decodereport zero virtual frames for such functions
instead of borrowing a neighbour's.Run the GC
misc_testsuitewithguest_debugenabled in CI to catch this
class of bug.</details>
fitzgen opened issue #14563:
With
-D guest-debug=y, GC root tracing looks up frame descriptors via
FrameTable::find_program_point. When a PC has no exact match, that lookup
falls back to the previous program point without checking whether it belongs
to the same function. Functions with no program points of their own, such as
the compiledmodule_start, inherit a neighbour's descriptor. A GC during
global initialization then hands arbitrary stack words to the collector as
roots, and the copying collector rewrites those stack slots.Test Case
(module (type $a (array (mut i8))) (global $g1 (ref $a) (array.new_default $a (i32.const 100000))) (global $g2 (ref $a) (array.new_default $a (i32.const 100000))) (global $g3 (ref $a) (array.new_default $a (i32.const 100000))) (func (local externref externref externref externref externref externref externref externref externref externref externref externref externref externref externref externref externref externref externref externref externref externref externref externref externref externref externref externref externref externref externref externref)) )Steps to Reproduce
wasmtime wast -W gc=y -C collector=copying -D guest-debug=y test.wastExpected Results
The module instantiates successfully, just as it does without
-D guest-debug=y.Actual Results
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)With
-C collector=drc:thread 'main' panicked at crates/wasmtime/src/runtime/vm/gc/enabled/drc.rs:567:13: every on-stack gc ref inside a Wasm frame should have be in our over-approximated stack roots set, but 0xa8000000 is not in the setRelease builds give
BUG: gc object and/or heap misalignedwith the copying
collector, and panic inGcHeap::index_mutwith DRC.Versions and Environment
Wasmtime version or commit:
73b04cff33Operating system: macOS 15.8.1
Architecture: aarch64
fitzgen added the wasmtime:debugging label to Issue #14563.
fitzgen added the wasm-proposal:gc label to Issue #14563.
alexcrichton added the bug label to Issue #14563.
Last updated: Oct 11 2026 at 04:10 UTC