How to Fix: [gc] Panic: every on-stack gc_ref inside a Wasm frame should have an entry in the VMGcRefActivationsTable

6 min read

A GC activation-table panic inside a Wasm frame usually means the runtime has discovered a managed reference on the stack that was never registered in the VMGcRefActivationsTable. In practice, that points to a mismatch between reference-producing Wasm execution and the stack map / activation tracking the garbage collector relies on to find live references safely.

Symptoms and Reproduction

The failure appears as a panic similar to:

every on-stack gc_ref inside a Wasm frame should have an entry in the VMGcRefActivationsTable

This is a strong invariant check inside Wasmtime’s GC integration. When enabled proposals such as Wasm GC are active, Wasmtime must keep precise bookkeeping for every live GC reference. If a test case like the attached reproduction triggers this panic with a command such as:

./target/debug/wasmtime -W=all-proposals ...

then the engine is accepting a program path where a gc_ref survives on the machine stack, but the runtime’s activation metadata does not reflect it.

Understanding the Root Cause

At a low level, Wasmtime’s garbage collector does not scan arbitrary native stack memory conservatively. It depends on precise rooting: every live Wasm-managed reference must be discoverable through metadata such as stack maps, frame layouts, and the VMGcRefActivationsTable.

This panic happens when those mechanisms diverge. Common technical causes include:

  • A JIT-generated Wasm frame contains a live gc_ref in a spill slot or local stack slot, but no activation-table entry was created for it.
  • A runtime transition, such as a host call, trap path, or safepoint, occurs while a GC reference is temporarily materialized on stack without proper registration.
  • An optimization or lowering pass moves a GC reference into an on-stack location, but the GC liveness metadata is not updated to match.
  • An experimental proposal combination enabled by -W=all-proposals exposes an incomplete implementation path for Wasm GC features.

In short: the panic is not the bug itself. The panic is the runtime catching a violation of its core memory-safety rule: every live managed reference in a Wasm frame must be visible to the collector.

That is why this issue often appears with proposal-heavy test runs. Enabling all proposals can activate code paths where reference types, GC objects, exception-like unwinding paths, or stack spills interact in ways that normal MVP-style Wasm programs do not.

Step-by-Step Solution

The fix strategy is to identify where a live gc_ref reaches the stack without corresponding activation tracking, then ensure the compiler or runtime records it correctly.

1. Reproduce with a debug build

Build Wasmtime with debug assertions and run the reproducer in isolation.

cargo build -p wasmtime-cli
RUST_BACKTRACE=1 ./target/debug/wasmtime -W=all-proposals path/to/reproducer.wasm

If the issue comes from a bundled archive, unpack it first and identify the exact .wasm or command line used by the test case.

2. Capture the failing frame and backtrace

The goal is to determine which Wasm function and runtime path triggered the invariant.

RUST_BACKTRACE=full ./target/debug/wasmtime -W=all-proposals path/to/reproducer.wasm

Focus on frames mentioning components such as:

  • gc
  • activations table
  • stack map
  • cranelift
  • wasm gc/ref lowering

3. Narrow the feature interaction

Because -W=all-proposals enables many experimental behaviors at once, isolate the smallest feature set that still reproduces the crash. For example, test with and without GC-related proposals if your local CLI supports separate flags.

./target/debug/wasmtime path/to/reproducer.wasm
./target/debug/wasmtime -W=gc path/to/reproducer.wasm
./target/debug/wasmtime -W=all-proposals path/to/reproducer.wasm

If the panic only occurs when GC proposals are enabled, that strongly confirms the issue is in reference liveness tracking rather than generic execution.

4. Inspect whether a gc_ref is spilled without registration

The most likely code defect is one of these:

  • A Wasm local holding a GC reference is spilled to stack.
  • A temporary value survives across a call, trap boundary, or safepoint.
  • The code generator emits a stack slot but does not add it to the GC roots / activation metadata.

When inspecting the relevant compiler or runtime area, look for transitions involving:

  • Reference-typed locals
  • Call lowering
  • Stack map generation
  • Safepoint insertion
  • Entry/exit of Wasm frames

The fix is usually not to suppress the panic. The correct fix is to make the collector aware of the reference.

5. Patch the runtime or compiler metadata path

The exact patch depends on where the mismatch occurs, but the correction generally falls into one of these categories:

  • Ensure every stack-resident gc_ref contributes an entry to the VMGcRefActivationsTable.
  • Ensure the generated stack map marks the slot as live at every safepoint where the collector may run.
  • Avoid leaving gc_ref values in unmanaged temporary stack locations during runtime transitions.
  • Update spill/reload logic so reference-typed values are handled differently from plain integers or raw pointers.

A conceptual before/after looks like this:

// Incorrect behavior conceptually:
// 1. Produce a gc_ref value in Wasm code
// 2. Spill it to a native stack slot
// 3. Enter a point where GC may inspect the frame
// 4. No activation-table entry exists for that slot => panic
// Correct behavior conceptually:
// 1. Produce a gc_ref value in Wasm code
// 2. Spill it only into a tracked stack/root location
// 3. Record that location in activation/stack-map metadata
// 4. GC scans it safely

6. Add a regression test from the reproducer

Once fixed, convert the provided test case into an automated regression test so future compiler or GC changes do not reintroduce the bug.

# Pseudocode workflow
# - add the wasm reproducer to the appropriate tests directory
# - assert successful execution or expected trap
# - ensure it no longer panics under debug assertions

Regression coverage is especially important for proposal-gated execution paths, where subtle liveness bugs can return after unrelated refactors.

7. Use a temporary workaround if you only need to unblock execution

If you are a user rather than a Wasmtime contributor, the immediate workaround is to avoid the triggering proposal set until the engine-side fix lands.

# Prefer the minimal required feature set instead of all proposals
./target/debug/wasmtime path/to/reproducer.wasm

or disable the specific experimental feature combination that introduces GC-managed references onto problematic code paths.

This does not solve the underlying engine bug, but it can restore short-term stability.

Common Edge Cases

  • Host function boundaries: a Wasm gc_ref may remain live across a host call, and if lowering does not root it correctly before the transition, the collector can detect an inconsistent frame.
  • Trap and unwind paths: exceptional control flow may bypass the normal bookkeeping path that records live references.
  • Optimized spills: register allocation can create stack slots only under specific pressure patterns, making the bug appear nondeterministic across builds.
  • Feature-flag interactions: the issue may only reproduce when GC is combined with other proposals enabled by all-proposals.
  • Debug vs release differences: debug builds often expose invariant checks earlier, while release builds may hide the panic but still contain incorrect rooting behavior.
  • Incorrect assumptions about raw pointers: a gc_ref is not interchangeable with a normal native pointer; it must participate in the runtime’s managed-reference tracking rules.

FAQ

1. Is this a bug in my Wasm module or in Wasmtime?

Most likely this specific panic indicates a Wasmtime engine bug, not an application-level logic error. The runtime is asserting that its own GC bookkeeping invariant was violated.

2. Why does it happen only with -W=all-proposals?

Because that flag enables experimental and less frequently exercised execution paths, especially around Wasm GC and advanced reference handling. Those paths are more likely to expose missing stack-root metadata.

3. Can I safely ignore the panic in release builds?

No. Even if the assertion is absent or less visible, the underlying problem is still unsafe for a precise collector. The right fix is to ensure every live on-stack gc_ref is recorded in the VMGcRefActivationsTable or equivalent stack metadata.

The practical resolution is straightforward: find the point where a managed reference becomes stack-resident, make that location visible to the GC, and lock the fix down with a regression test. For this issue, that is the difference between a collector-safe Wasm frame and a panic-triggering one.

Leave a Reply

Your email address will not be published. Required fields are marked *