sdjasj opened issue #13750:
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
- Build
wasmtimefrommain(tested on a binary built from commit897aa00de; affected code is present atmainHEAD08c456e887).- 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.watExpected Results
305419896(0x12345678) — the value of the freshly created struct's$xfield. 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 thearray.new_default $Aallocations 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 256As 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))) returns305419896, 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 atmainHEAD08c456e8870b0b85566f9e9808b7a2f044eaa265.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_entriesatcrates/cranelift/src/func_environ/stack_switching/instructions.rs:370loads continuation/tag return values from a payload buffer using only CLIFir::Typeand pushes them as ordinary SSA values without callingbuilder.declare_value_needs_stack_map. It is reached fromtranslate_resume(:1596),translate_suspend(:1671), andtranslate_switch(:1911).declare_value_needs_stack_mapis not called anywhere undercrates/cranelift/src/func_environ/stack_switching/.- Ordinary GC-producing ops do declare stack-map needs — e.g.
crates/cranelift/src/translate/code_translator.rs:239callsdeclare_value_needs_stack_mapfor results whose Wasm type needs it (viaFuncEnvironment::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 coverscont.bindargument buffers but not GC values loaded into live SSA values byresume/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 ascopyingordrc.- Candidate fixes: (1) preserve Wasm value-type metadata through
load_data_entries(or pass a parallelneeds_stack_mapbitmap) and callbuilder.declare_value_needs_stack_mapfor every loaded non-i31VM GC reference; or (2) as a stopgap matching the existingcont.bindworkaround, reject GC payload/result types forresume/suspend/switchuntil those references are either traced directly from payload buffers or precisely represented in stack maps.
sdjasj added the bug label to Issue #13750.
alexcrichton added the wasm-proposal:stack-switching label to Issue #13750.
alexcrichton added the wasm-proposal:gc label to Issue #13750.
Last updated: Jul 29 2026 at 05:03 UTC