How to Fix: [GC] id from different slab
Fixing Wasmtime GC Error: id from different slab
This crash points to a GC object identity mismatch: a runtime value is being looked up in the wrong internal slab allocator, so Wasmtime finds an ID that belongs to a different allocation domain than the one currently being accessed. In practice, that usually means a bug in how GC-managed references, stores, heaps, or component/runtime boundaries are being tracked during execution.
Symptoms and Reproduction
The issue appears when running the provided test case with a Wasmtime binary built with GC proposal support enabled. The failure message indicates that an internal ID was resolved from the wrong slab, which is a classic sign that one of these invariants was broken:
- A GC reference escaped the Store it belongs to.
- A runtime object was reused after a store/context transition.
- An index or handle created for one heap/slab was later interpreted against another.
- A bug in Wasmtime’s internal GC bookkeeping allowed a cross-slab lookup.
Typical reproduction flow:
./target/debug/wasmtime -W=all-proposals ...
If you are validating the attached reproducer from the issue, use that exact artifact and run it against a debug build first. Debug assertions are much more useful for narrowing down where the mismatch originates.
Understanding the Root Cause
Wasmtime uses internal allocation tables, commonly referred to as slabs, to track runtime entities efficiently. Each slab stores objects of a particular category or ownership context, and object IDs are only meaningful inside the slab that created them.
The error id from different slab means the runtime received an ID whose numeric value may be valid, but whose provenance is not. In other words, the system is not just checking “does this number exist?” but also “does this handle belong to this exact storage domain?” That check failed.
Technically, this happens for one of two reasons:
- User-level misuse of runtime ownership: for example, passing or caching GC-backed references across incompatible stores, instances, or execution contexts.
- Engine/runtime bug: internal translation or execution logic creates a handle in one slab, then later resolves it through another slab due to a state desynchronization.
Because the issue is triggered under the GC proposal, the most likely area is the lifecycle of externref/anyref/struct/array-like GC values and their mapping to a particular store-local heap. GC values are usually store-affine, meaning they cannot be safely reused outside the store that created them. If a call path violates that rule, Wasmtime can detect the inconsistency and trap or panic with this exact kind of message.
Another important detail: debug builds often panic with a strong internal assertion, while release builds may show less obvious behavior. So although the reproducer surfaces as a slab mismatch, the actual bug may have occurred several frames earlier when a handle was first captured, cloned, returned, or translated incorrectly.
Step-by-Step Solution
The right fix depends on whether you are a Wasmtime user embedding the runtime or a Wasmtime contributor debugging the engine itself. Use the following workflow to isolate and fix the problem.
1. Reproduce on a debug build
Build Wasmtime with debug assertions enabled and rerun the reproducer.
cargo build -p wasmtime-cli
RUST_BACKTRACE=1 ./target/debug/wasmtime -W=all-proposals path/to/reproducer.wasm
If the test case is bundled in an archive, unpack it and run the exact command sequence included in the issue.
2. Confirm whether the bug is store-affinity related
Audit your embedding code for any value that originates from one Store and is later used with another. This includes:
- GC references
- ExternRef or host references
- Compiled values cached together with runtime state
- Instance-specific handles reused after teardown
Problematic pattern:
let mut store_a = Store::new(&engine, state_a);
let mut store_b = Store::new(&engine, state_b);
let value_from_a = make_gc_value(&mut store_a);
use_value(&mut store_b, value_from_a);
Correct pattern:
let mut store = Store::new(&engine, state);
let value = make_gc_value(&mut store);
use_value(&mut store, value);
If a value must cross a boundary, do not move the raw runtime reference. Instead, serialize logical data and recreate a fresh value in the destination store.
3. Eliminate stale handles
If you cache IDs, indexes, or references in your host layer, verify that they are not being reused after:
- Store reset or drop
- Instance re-instantiation
- GC heap mutation that invalidates prior assumptions
- Component model boundary crossings
Safer approach:
struct SafeHandleCache {
// cache logical keys, not raw runtime GC handles
}
fn rebuild_value_for_store(store: &mut Store<MyState>, key: LogicalKey) {
// recreate the runtime value for this specific store
}
The key idea is simple: cache data, not store-bound runtime identities.
4. If you are debugging Wasmtime internals, instrument slab creation and lookup
Add temporary logging around the code paths that allocate and resolve the relevant GC object IDs. You want to record:
- Slab type or slab instance identity
- Allocated object ID
- Store/heap identifier
- Call site that created the object
- Call site that later resolved it
Example diagnostic pattern:
eprintln!(
"alloc gc object: slab={:?} store={:?} id={:?}",
slab_id,
store_id,
obj_id
);
eprintln!(
"lookup gc object: slab={:?} store={:?} id={:?}",
slab_id,
store_id,
obj_id
);
If the allocation and lookup paths report different slab identities for the same object ID, you have found the corruption boundary.
5. Verify proposal feature alignment
The command line uses -W=all-proposals, so make sure the binary, parser, validator, and runtime all agree on enabled experimental features. Mismatched feature plumbing can produce invalid assumptions during translation or execution.
cargo clean
cargo build -p wasmtime-cli
RUST_BACKTRACE=1 ./target/debug/wasmtime -W=all-proposals path/to/reproducer.wasm
If you maintain a local branch, rebase onto the latest mainline and retest. GC-related fixes often land quickly, and reproductions involving proposal features may already be resolved upstream.
6. Minimize the reproducer
If the issue still reproduces, reduce the test case until only the smallest failing module remains. Focus on instructions and constructs involving:
- struct.new, array.new, and GC heap allocation
- ref.cast, ref.test, and typed references
- Cross-function returns of GC values
- Imports/exports carrying reference-typed values
A minimized test makes it much easier to determine whether the problem is in translation, runtime storage, or host integration.
7. Apply the practical fix
For most embedders, the fix is one of these:
- Keep all GC values inside the store that created them.
- Do not persist raw runtime handles outside their valid lifetime.
- Reconstruct values in the destination context instead of reusing internal references.
- Upgrade to a Wasmtime revision that includes the relevant GC bug fix.
For Wasmtime contributors, the fix usually means enforcing or repairing one of these invariants:
- Object IDs must be tagged and resolved only against the correct slab.
- Store-local heaps must never leak references into another store’s lookup path.
- Translation/runtime layers must preserve the exact ownership context of every GC allocation.
Common Edge Cases
- Multiple stores sharing one engine: this is valid, but runtime values are still usually store-local. Sharing an engine does not mean sharing GC handles.
- Host callbacks: a callback may accidentally capture a GC reference from one invocation and reuse it in another store or instance.
- Async execution: suspended tasks can outlive the context in which a reference was created, leading to stale handle reuse.
- Component model boundaries: lowering/lifting reference-like values incorrectly can surface as a slab mismatch later.
- Version skew: a reproducer built against one revision may trigger assertions only on another due to internal representation changes.
- Release-only assumptions: if debug assertions disappear in release mode, the bug may still exist but show up as a trap, wrong cast result, or memory corruption symptom elsewhere.
FAQ
Is this definitely a Wasmtime bug?
No. If your host application moves store-bound GC references across stores or keeps stale runtime handles, you can trigger this kind of failure yourself. But if the attached reproducer is a standalone wasm program crashing the CLI without embedding misuse, that strongly suggests an internal Wasmtime bug.
Why does this happen mainly with GC proposal features?
Because GC-managed values have stricter ownership and lifetime rules than plain integers or linear-memory offsets. Their identities are tracked through runtime-managed heaps and slabs, so ownership mistakes are detected more explicitly.
What should I include when reporting or triaging this issue?
Include the exact Wasmtime commit or release, the full command line, whether it reproduces in debug and release, a backtrace, and ideally a minimized wasm file. If possible, link the test artifact with a short reproduction script rather than pasting long command lines.
The core takeaway is that id from different slab is not a random panic: it is a strong signal that a GC object handle crossed the wrong ownership boundary. Fix the handle lifetime or store affinity, or patch the internal lookup path if the engine itself is violating that invariant.