Stream: git-wasmtime

Topic: wasmtime / issue #13095 Spurious "indirect call type mism...


view this post on Zulip Wasmtime GitHub notifications bot (Apr 14 2026 at 17:41):

shumbo opened issue #13095:

Summary

Under -Wgc, wasmtime spuriously traps with indirect call type mismatch on a valid call_indirect. The cause is that for imported functions, wasmtime stores the declared import type (a supertype) into VMFuncRef.type_index instead of the function's actual type (the subtype). At a call_indirect site annotated with the actual subtype, the JIT then checks supertype <: subtype and traps.

The bug requires the GC proposal (it only manifests when function types have non-trivial subtyping) and is strictly fail-closed — no invalid call can be accepted — so it's a spec-conformance issue, not a security vulnerability. V8 and the Wasm reference interpreter both accept the program.

Test Case

A minimal .wast PoC is attached as test_type_index_bug.wast:

(module $A
  (type $super (sub (func (param i32) (result i32))))
  (type $sub (sub $super (func (param i32) (result i32))))
  (func (export "f") (type $sub) (param i32) (result i32) (local.get 0))
)
(register "A" $A)

(module $B
  (type $super (sub (func (param i32) (result i32))))
  (type $sub (sub $super (func (param i32) (result i32))))

  ;; Valid covariant import: $sub <: $super
  (import "A" "f" (func $f (type $super)))

  (table 10 funcref)
  (elem declare func $f)

  (func (export "test_super") (result i32)
    (table.set (i32.const 0) (ref.func $f))
    (i32.const 99) (i32.const 0)
    (call_indirect (type $super)))

  (func (export "test_sub") (result i32)
    (table.set (i32.const 0) (ref.func $f))
    (i32.const 99) (i32.const 0)
    (call_indirect (type $sub)))
)

(assert_return (invoke "test_super") (i32.const 99))
(assert_return (invoke "test_sub")   (i32.const 99))

The pattern: module A defines and exports a function with actual type $sub; module B imports it declared as the supertype $super (valid via function-type subtyping in the GC proposal), places it in a funcref table, and calls it indirectly under both annotations.

Steps to Reproduce

  1. Build wasmtime with GC enabled: cargo build --release -p wasmtime-cli --features gc.
  2. Run the test: ./target/release/wasmtime wast -Wgc test_type_index_bug.wast.

Expected Results

Both assert_return directives pass and return 99. This matches:

Actual Results

The first assertion passes; the second traps:

Error: failed to run script file 'test_type_index_bug.wast'

Caused by:
    0: failed directive on test_type_index_bug.wast:34
    1: error while executing at wasm backtrace:
           0:  0x75 - B!<wasm function 2>
    2: wasm trap: indirect call type mismatch

Versions and Environment

Extra Info

Instance::get_func_ref (crates/wasmtime/src/runtime/vm/instance.rs:884-933) populates a VMFuncRef with the type stored in Module::functions[index].signature. For a locally defined function this is the actual type. For an imported function it is the declared import type, which may be a proper supertype of the actual type. The wrong type is then stamped into VMFuncRef.type_index and used by the JIT-emitted call_indirect / call_ref subtype check. The check becomes declared_import_type <: call_site_annotation, which spuriously fails when the annotation is the real (sub) type.

The bug is strictly one-directional (it makes the check too strict, never too permissive), so sandbox integrity is not affected — per wasmtime's security policy this is a correctness / spec-conformance issue, not a security vulnerability.

call_indirect annotation Correct check Buggy check Result
(type $super) $sub <: $super = yes $super <: $super = yes OK (accidental)
(type $sub) $sub <: $sub = yes $super <: $sub = no wrong trap

V8 and the reference interpreter accept the program:

Runtime test_super test_sub
V8 13.6 (Node.js 24.3.0) 99 99
Wasm reference interpreter 3.0.0 99 99
Wasmtime main (v45.0) 99 traps (indirect call type mismatch)

The reference interpreter output, for the record:

$ wasm -t test_type_index_bug.wast
-- Asserting return...
-- Invoking function "test_super"...
-- Asserting return...
-- Invoking function "test_sub"...
(exit 0)

The spec says

Applied to the PoC: A's allocation stamps the function instance's type as $sub. B's $super import does not change it. call_indirect (type $sub) must therefore check $sub <: $sub and succeed. Wasmtime's trap substitutes B's declared import type for the function instance's actual stored type.

Root cause and suggested fix

Module::functions[i].signature is the importing module's view of function i. For imports, that view only records the declaration type, not the type resolved at instantiation. get_func_ref trusts this view unconditionally.

Possible fixes:

view this post on Zulip Wasmtime GitHub notifications bot (Apr 14 2026 at 17:41):

shumbo added the bug label to Issue #13095.

view this post on Zulip Wasmtime GitHub notifications bot (Apr 14 2026 at 17:41):

shumbo edited issue #13095:

Summary

Under -Wgc, wasmtime spuriously traps with indirect call type mismatch on a valid call_indirect. The cause is that for imported functions, wasmtime stores the declared import type (a supertype) into VMFuncRef.type_index instead of the function's actual type (the subtype). At a call_indirect site annotated with the actual subtype, the JIT then checks supertype <: subtype and traps.

The bug requires the GC proposal (it only manifests when function types have non-trivial subtyping) and is strictly fail-closed — no invalid call can be accepted — so it's a spec-conformance issue, not a security vulnerability. V8 and the Wasm reference interpreter both accept the program.

Test Case

A minimal .wast PoC is attached as test_type_index_bug.wast:

(module $A
  (type $super (sub (func (param i32) (result i32))))
  (type $sub (sub $super (func (param i32) (result i32))))
  (func (export "f") (type $sub) (param i32) (result i32) (local.get 0))
)
(register "A" $A)

(module $B
  (type $super (sub (func (param i32) (result i32))))
  (type $sub (sub $super (func (param i32) (result i32))))

  ;; Valid covariant import: $sub <: $super
  (import "A" "f" (func $f (type $super)))

  (table 10 funcref)
  (elem declare func $f)

  (func (export "test_super") (result i32)
    (table.set (i32.const 0) (ref.func $f))
    (i32.const 99) (i32.const 0)
    (call_indirect (type $super)))

  (func (export "test_sub") (result i32)
    (table.set (i32.const 0) (ref.func $f))
    (i32.const 99) (i32.const 0)
    (call_indirect (type $sub)))
)

(assert_return (invoke "test_super") (i32.const 99))
(assert_return (invoke "test_sub")   (i32.const 99))

The pattern: module A defines and exports a function with actual type $sub; module B imports it declared as the supertype $super (valid via function-type subtyping in the GC proposal), places it in a funcref table, and calls it indirectly under both annotations.

Steps to Reproduce

  1. Build wasmtime with GC enabled: cargo build --release -p wasmtime-cli --features gc.
  2. Run the test: ./target/release/wasmtime wast -Wgc test_type_index_bug.wast.

Expected Results

Both assert_return directives pass and return 99. This matches:

Actual Results

The first assertion passes; the second traps:

Error: failed to run script file 'test_type_index_bug.wast'

Caused by:
    0: failed directive on test_type_index_bug.wast:34
    1: error while executing at wasm backtrace:
           0:  0x75 - B!<wasm function 2>
    2: wasm trap: indirect call type mismatch

Versions and Environment

Extra Info

Instance::get_func_ref (crates/wasmtime/src/runtime/vm/instance.rs:884-933) populates a VMFuncRef with the type stored in Module::functions[index].signature. For a locally defined function this is the actual type. For an imported function it is the declared import type, which may be a proper supertype of the actual type. The wrong type is then stamped into VMFuncRef.type_index and used by the JIT-emitted call_indirect / call_ref subtype check. The check becomes declared_import_type <: call_site_annotation, which spuriously fails when the annotation is the real (sub) type.

The bug is strictly one-directional (it makes the check too strict, never too permissive), so sandbox integrity is not affected — per wasmtime's security policy this is a correctness / spec-conformance issue, not a security vulnerability.

call_indirect annotation Correct check Buggy check Result
(type $super) $sub <: $super = yes $super <: $super = yes OK (accidental)
(type $sub) $sub <: $sub = yes $super <: $sub = no wrong trap

V8 and the reference interpreter accept the program:

Runtime test_super test_sub
V8 13.6 (Node.js 24.3.0) 99 99
Wasm reference interpreter 3.0.0 99 99
Wasmtime main (v45.0) 99 traps (indirect call type mismatch)

The reference interpreter output, for the record:

$ wasm -t test_type_index_bug.wast
-- Asserting return...
-- Invoking function "test_super"...
-- Asserting return...
-- Invoking function "test_sub"...
(exit 0)

The spec says

Applied to the PoC: A's allocation stamps the function instance's type as $sub. B's $super import does not change it. call_indirect (type $sub) must therefore check $sub <: $sub and succeed. Wasmtime's trap substitutes B's declared import type for the function instance's actual stored type.

Root cause and suggested fix

Module::functions[i].signature is the importing module's view of function i. For imports, that view only records the declaration type, not the type resolved at instantiation. get_func_ref trusts this view unconditionally.

Possible fixes:

view this post on Zulip Wasmtime GitHub notifications bot (Apr 14 2026 at 18:57):

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

view this post on Zulip Wasmtime GitHub notifications bot (Apr 14 2026 at 19:12):

fitzgen assigned fitzgen to issue #13095.

view this post on Zulip Wasmtime GitHub notifications bot (Apr 21 2026 at 17:21):

fitzgen closed issue #13095:

Summary

Under -Wgc, wasmtime spuriously traps with indirect call type mismatch on a valid call_indirect. The cause is that for imported functions, wasmtime stores the declared import type (a supertype) into VMFuncRef.type_index instead of the function's actual type (the subtype). At a call_indirect site annotated with the actual subtype, the JIT then checks supertype <: subtype and traps.

The bug requires the GC proposal (it only manifests when function types have non-trivial subtyping) and is strictly fail-closed — no invalid call can be accepted — so it's a spec-conformance issue, not a security vulnerability. V8 and the Wasm reference interpreter both accept the program.

Test Case

A minimal .wast PoC is attached as test_type_index_bug.wast:

(module $A
  (type $super (sub (func (param i32) (result i32))))
  (type $sub (sub $super (func (param i32) (result i32))))
  (func (export "f") (type $sub) (param i32) (result i32) (local.get 0))
)
(register "A" $A)

(module $B
  (type $super (sub (func (param i32) (result i32))))
  (type $sub (sub $super (func (param i32) (result i32))))

  ;; Valid covariant import: $sub <: $super
  (import "A" "f" (func $f (type $super)))

  (table 10 funcref)
  (elem declare func $f)

  (func (export "test_super") (result i32)
    (table.set (i32.const 0) (ref.func $f))
    (i32.const 99) (i32.const 0)
    (call_indirect (type $super)))

  (func (export "test_sub") (result i32)
    (table.set (i32.const 0) (ref.func $f))
    (i32.const 99) (i32.const 0)
    (call_indirect (type $sub)))
)

(assert_return (invoke "test_super") (i32.const 99))
(assert_return (invoke "test_sub")   (i32.const 99))

The pattern: module A defines and exports a function with actual type $sub; module B imports it declared as the supertype $super (valid via function-type subtyping in the GC proposal), places it in a funcref table, and calls it indirectly under both annotations.

Steps to Reproduce

  1. Build wasmtime with GC enabled: cargo build --release -p wasmtime-cli --features gc.
  2. Run the test: ./target/release/wasmtime wast -Wgc test_type_index_bug.wast.

Expected Results

Both assert_return directives pass and return 99. This matches:

Actual Results

The first assertion passes; the second traps:

Error: failed to run script file 'test_type_index_bug.wast'

Caused by:
    0: failed directive on test_type_index_bug.wast:34
    1: error while executing at wasm backtrace:
           0:  0x75 - B!<wasm function 2>
    2: wasm trap: indirect call type mismatch

Versions and Environment

Extra Info

Instance::get_func_ref (crates/wasmtime/src/runtime/vm/instance.rs:884-933) populates a VMFuncRef with the type stored in Module::functions[index].signature. For a locally defined function this is the actual type. For an imported function it is the declared import type, which may be a proper supertype of the actual type. The wrong type is then stamped into VMFuncRef.type_index and used by the JIT-emitted call_indirect / call_ref subtype check. The check becomes declared_import_type <: call_site_annotation, which spuriously fails when the annotation is the real (sub) type.

The bug is strictly one-directional (it makes the check too strict, never too permissive), so sandbox integrity is not affected — per wasmtime's security policy this is a correctness / spec-conformance issue, not a security vulnerability.

call_indirect annotation Correct check Buggy check Result
(type $super) $sub <: $super = yes $super <: $super = yes OK (accidental)
(type $sub) $sub <: $sub = yes $super <: $sub = no wrong trap

V8 and the reference interpreter accept the program:

Runtime test_super test_sub
V8 13.6 (Node.js 24.3.0) 99 99
Wasm reference interpreter 3.0.0 99 99
Wasmtime main (v45.0) 99 traps (indirect call type mismatch)

The reference interpreter output, for the record:

$ wasm -t test_type_index_bug.wast
-- Asserting return...
-- Invoking function "test_super"...
-- Asserting return...
-- Invoking function "test_sub"...
(exit 0)

The spec says

Applied to the PoC: A's allocation stamps the function instance's type as $sub. B's $super import does not change it. call_indirect (type $sub) must therefore check $sub <: $sub and succeed. Wasmtime's trap substitutes B's declared import type for the function instance's actual stored type.

Root cause and suggested fix

Module::functions[i].signature is the importing module's view of function i. For imports, that view only records the declaration type, not the type resolved at instantiation. get_func_ref trusts this view unconditionally.

Possible fixes:


Last updated: May 03 2026 at 22:13 UTC