bytecodealliance / bytecodealliance/wasmtime
Stack-switching continuation payload GC references miss stack maps
- Dominant language
- Rust
- Stars
- 18.6k
- Forks
- 1.8k
- Avg merge
- 1d 18h
- Merged PRs (30d)
- 126
Description
### Test Case
Inline Wasm (save as `resume_gc_result_stack.wat`). This exercises a continuation that returns a GC struct reference; the result is kept on the operand stack (not in a GC-typed local) across a GC safepoint.
```wat
(module
(type $S (struct (field $x i32)))
(type $A (array i64))
(type $ft (func (result (ref $S))))
(type $ct (cont $ft))
(func $target (result (ref $S))
(struct.new $S (i32.const 0x12345678)))
(elem declare func $target)
(func (export "run") (result i32)
(local $k (ref null $ct))
(local $i i32)
(local.set $k (cont.new $ct (ref.func $target)))
;; Continuation result is a GC reference, kept as an operand-stack SSA
;; value rather than stored in a GC-typed local.
(resume $ct (local.get $k))
;; Force frequent collections while the only live copy of the struct ref
;; is the unmarked SSA value returned from the continuation payload buffer.
(local.set $i (i32.const 0))
(loop $again
(drop (array.new_default $A (i32.const 256)))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br_if $again (i32.lt_u (local.get $i) (i32.const 2000))))
;; Expected 0x12345678. Vulnerable builds read 256 here: the length field
;; of one of the arrays allocated during GC pressure.
(struct.get $S $x))
)
```
### Steps to Reproduce
1. Build `wasmtime` from `main` (tested on a binary built from commit `897aa00de`; affected code is present at `main` HEAD `08c456e887`).
2. Run the module above with stack switching and GC enabled, forcing a collection on every allocation:
```
wasmtime run \
-W stack-switching=y \
-W exceptions=y \
-W function-references=y \
-W gc=y \
-C collector=copying \
-O gc-zeal-alloc-counter=1 \
--invoke run \
resume_gc_result_stack.wat
```
### Expected Results
`305419896` (`0x12345678`) — the value of the freshly created struct's `$x` field. The continuation result is still live on the operand stack, so its struct must not be collected.
### Actual Results
Incorrect result: the program prints `256`, which is the length of one of the `array.new_default $A` allocations made during the GC pressure loop — i.e. a stale read of a reused object layout. The live GC object was missed by root discovery, reclaimed, and its memory reused while the guest still held a reference (use-after-free-like behavior).
```
warning: using `--invoke` with a function that returns values is experimental and may break in the future
256
```
As a control, storing the same continuation result into a GC-typed local before the allocation loop (`(local.set $s (resume $ct (local.get $k)))` then `(struct.get $S $x (local.get $s))`) returns `305419896`, confirming the failure is specific to the reference living as an unmarked operand-stack SSA value across the safepoint.
### Versions and Environment
Wasmtime version or commit: Wasmtime 47.0.0 (binary built from commit `897aa00de`, 2026-06-15); affected code present at `main` HEAD `08c456e8870b0b85566f9e9808b7a2f044eaa265`.
Operating system: Linux 5.15.0-139-generic (Ubuntu 20.04-based)
Architecture: x86_64
Rust: rustc 1.96.0 (ac68faa20 2026-05-25)
### Extra Info
- Root cause: `VMHostArrayRef::load_data_entries` at `crates/cranelift/src/func_environ/stack_switching/instructions.rs:370` loads continuation/tag return values from a payload buffer using only CLIF `ir::Type` and pushes them as ordinary SSA values without calling `builder.declare_value_needs_stack_map`. It is reached from `translate_resume` (`:1596`), `translate_suspend` (`:1671`), and `translate_switch` (`:1911`). `declare_value_needs_stack_map` is not called anywhere under `crates/cranelift/src/func_environ/stack_switching/`.
- Ordinary GC-producing ops do declare stack-map needs — e.g. `crates/cranelift/src/translate/code_translator.rs:239` calls `declare_value_needs_stack_map` for results whose Wasm type needs it (via `FuncEnvironment::val_ty_needs_stack_map` / `heap_ty_needs_stack_map`), so such references are surfaced to the collector at safepoints. The payload-load path loses the Wasm-level reference metadata and skips this.
- This extends a known, maintainer-documented limitation: `crates/wasmtime/src/runtime/store/gc.rs` (`trace_wasm_continuation_roots`, around line 721) has a FIXME noting that continuation payload buffers are not generally traced and that, as a workaround, `cont.bind`-ing GC values is currently disallowed. That workaround covers `cont.bind` argument buffers but not GC values loaded into live SSA values by `resume`/`suspend`/`switch`, which is the path exercised here.
- Stack switching is experimental and opt-in (`-W stack-switching=y`); default configurations are unaffected. The issue requires stack switching and Wasm GC enabled together with a precise collector such as `copying` or `drc`.
- Candidate fixes: (1) preserve Wasm value-type metadata through `load_data_entries` (or pass a parallel `needs_stack_map` bitmap) and call `builder.declare_value_needs_stack_map` for every loaded non-`i31` VM GC reference; or (2) as a stopgap matching the existing `cont.bind` workaround, reject GC payload/result types for `resume`/`suspend`/`switch` until those references are either traced directly from payload buffers or precisely represented in stack maps.
Contributor guide
Research direction
Reproduce the failure with resume_gc_result_stack.wat and the provided wasmtime command, then read load_data_entries in crates/cranelift/src/func_environ/stack_switching/instructions.rs and its callers translate_resume, translate_suspend, and translate_switch. Compare this path with stack-map handling in crates/cranelift/src/translate/code_translator.rs and the continuation-root FIXME in crates/wasmtime/src/runtime/store/gc.rs. Done means the live continuation result survives collection and the test returns 305419896.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust, wasm
- Domain
- backend, compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 48/100