Skip to content

Spurious "indirect call type mismatch" trap when calling an imported function at its actual subtype #13095

@shumbo

Description

@shumbo

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:

  • V8 13.6 (Node.js 24.3.0): both calls return 99.
  • Wasm reference interpreter (spec repo, 3.0.0): the script exits 0 with both assert_return directives passing.

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

  • Wasmtime version: main (v45.0.0); root-cause analysis below is against current main.
  • Operating system: Linux (reproduced on arch linux, kernel 6.10.8).
  • Architecture: aarch64 (also reproduces on x86_64 by code inspection — the affected code is architecture-independent pure-Rust runtime code).

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

  • call_indirect checks the call-site annotation against the type of the function as recorded in the store, looked up via the table entry's function address — not against anything in the caller module.
  • A function instance has a single type field, set when the function is allocated from its defining module.
  • Imports reuse the exporter's function address; extern-type matching uses subtyping only for the compatibility check at instantiation time and never rewraps the stored type.

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:

  • Pointer-chase from VMFunctionImport.vmctx back to the exporter's VMFuncRef in get_func_ref. No struct-layout changes, but fragile under re-export chains and adds work to every get_func_ref call.
  • Add a type_index: VMSharedTypeIndex field to VMFunctionImport, populated at instantiation time. construct_func_ref then reads the correct type directly; re-export chains are handled by construction. Costs 4 bytes in VMFunctionImport and a VMContext offset recalculation.
  • Branch in get_func_ref on defined-vs-imported and retrieve the type

Metadata

Metadata

Assignees

Labels

bugIncorrect behavior in the current implementation that needs fixingwasm-proposal:gcIssues with the implementation of the gc wasm proposal

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions