Stream: git-wasmtime

Topic: wasmtime / issue #14451 Winch typed select can omit a liv...


view this post on Zulip Wasmtime GitHub notifications bot (Sep 30 2026 at 02:21):

smaeljaish771 opened issue #14451:

Summary

When Winch compiles a typed select whose result is a GC reference, the result's shadow type can incorrectly come from the second operand rather than the declared result type.

If the first operand is a non-null externref, the second operand is ref.null extern, and the condition selects the first operand, the runtime value is a real GC reference but its shadow type becomes I32.

At a later call site this causes the reference to be omitted from the stack map. With the copying collector, the referenced object can then be reclaimed or relocated without the stack copy being updated.

This was reproduced on the latest commit.

Technical Details

The missing check is the shadow-type filter used to decide whether a value needs a stack map. needs_stack_map (winch/codegen/src/stack.rs:315-327) is unconditionally false for I32, so a live GC reference whose shadow type is I32 neither increments gc_ref_count nor satisfies the collection condition in CodeGenContext::calculate_stack_map_offsets (winch/codegen/src/codegen/context.rs:663-695).

Because that function then returns an empty table, FnCall::emit (winch/codegen/src/codegen/call.rs:108-113) skips emitting any stack map.

The relevant data flow is:

Reproduction

Tested at commit:

ff7896b6d97a424aed430a48a186773fd163667f

The reproducer uses the public embedding API with Winch and compares a normal reference path with the typed-select path.

Guest input:

(module
  (import "" "make" (func $make (result externref)))
  (import "" "gc"   (func $gc))

  (func (export "control") (result externref)
    call $make
    call $gc)

  (func (export "bug") (result externref)
    call $make
    ref.null extern
    i32.const 1
    select (result externref)
    call $gc)
)

With the null collector, both cases survive.

With the copying collector, the control case survives while the typed-select case loses the referenced object:

collector=null
  [control] data=Ok(0xdecaf)  SURVIVED
  [bug] data=Ok(0xdecaf)  SURVIVED

collector=copying
  [control] data=Ok(0xdecaf)  SURVIVED
  [bug] data=Err(BUG: invalid `ExternRefHostDataId`)  HOST-DATA-SWEPT

SUMMARY null_bug=Survived copying_control=Survived copying_bug=Swept
PROBE_RESULT: reproduced

The two cases differ only in whether the reference passes through the typed select, which appears to isolate the problem to the shadow type used for the select result.

Suggested Fix

visit_typed_select (winch/codegen/src/visitor.rs:2228-2230) should honor the instruction's declared result type instead of discarding _ty.

visit_select (winch/codegen/src/visitor.rs:2208-2226) should not derive the pushed shadow type from the second operand when the declared result is a reference type. The pushed value needs to retain a reference shadow type so that needs_stack_map returns true and the live reference is included in the call-site stack map.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 30 2026 at 02:21):

smaeljaish771 added the bug label to Issue #14451.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 30 2026 at 14:37):

alexcrichton commented on issue #14451:

cc @saulecabrera @macovedj

view this post on Zulip Wasmtime GitHub notifications bot (Sep 30 2026 at 14:37):

alexcrichton added the wasm-proposal:gc label to Issue #14451.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 30 2026 at 14:37):

alexcrichton added the winch label to Issue #14451.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 01 2026 at 17:59):

saulecabrera closed issue #14451:

Summary

When Winch compiles a typed select whose result is a GC reference, the result's shadow type can incorrectly come from the second operand rather than the declared result type.

If the first operand is a non-null externref, the second operand is ref.null extern, and the condition selects the first operand, the runtime value is a real GC reference but its shadow type becomes I32.

At a later call site this causes the reference to be omitted from the stack map. With the copying collector, the referenced object can then be reclaimed or relocated without the stack copy being updated.

This was reproduced on the latest commit.

Technical Details

The missing check is the shadow-type filter used to decide whether a value needs a stack map. needs_stack_map (winch/codegen/src/stack.rs:315-327) is unconditionally false for I32, so a live GC reference whose shadow type is I32 neither increments gc_ref_count nor satisfies the collection condition in CodeGenContext::calculate_stack_map_offsets (winch/codegen/src/codegen/context.rs:663-695).

Because that function then returns an empty table, FnCall::emit (winch/codegen/src/codegen/call.rs:108-113) skips emitting any stack map.

The relevant data flow is:

Reproduction

Tested at commit:

ff7896b6d97a424aed430a48a186773fd163667f

The reproducer uses the public embedding API with Winch and compares a normal reference path with the typed-select path.

Guest input:

(module
  (import "" "make" (func $make (result externref)))
  (import "" "gc"   (func $gc))

  (func (export "control") (result externref)
    call $make
    call $gc)

  (func (export "bug") (result externref)
    call $make
    ref.null extern
    i32.const 1
    select (result externref)
    call $gc)
)

With the null collector, both cases survive.

With the copying collector, the control case survives while the typed-select case loses the referenced object:

collector=null
  [control] data=Ok(0xdecaf)  SURVIVED
  [bug] data=Ok(0xdecaf)  SURVIVED

collector=copying
  [control] data=Ok(0xdecaf)  SURVIVED
  [bug] data=Err(BUG: invalid `ExternRefHostDataId`)  HOST-DATA-SWEPT

SUMMARY null_bug=Survived copying_control=Survived copying_bug=Swept
PROBE_RESULT: reproduced

The two cases differ only in whether the reference passes through the typed select, which appears to isolate the problem to the shadow type used for the select result.

Suggested Fix

visit_typed_select (winch/codegen/src/visitor.rs:2228-2230) should honor the instruction's declared result type instead of discarding _ty.

visit_select (winch/codegen/src/visitor.rs:2208-2226) should not derive the pushed shadow type from the second operand when the declared result is a reference type. The pushed value needs to retain a reference shadow type so that needs_stack_map returns true and the live reference is included in the call-site stack map.


Last updated: Oct 11 2026 at 04:10 UTC