Skip to content

Stack-switching continuation payload GC references miss stack maps #13750

Description

@sdjasj

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.

(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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugIncorrect behavior in the current implementation that needs fixingwasm-proposal:gcIssues with the implementation of the gc wasm proposalwasm-proposal:stack-switchingIssues related to WebAssembly stack switching

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions