bytecodealliance / bytecodealliance/wasmtime

Stack-switching continuation payload GC references miss stack maps

Open
#13,750 0 comments 0 reactions 0 assignees View on GitHub
bug wasm-proposal:gc wasm-proposal:stack-switching
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.