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
- Build wasmtime with GC enabled:
cargo build --release -p wasmtime-cli --features gc.
- 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
Summary
Under
-Wgc, wasmtime spuriously traps withindirect call type mismatchon a validcall_indirect. The cause is that for imported functions, wasmtime stores the declared import type (a supertype) intoVMFuncRef.type_indexinstead of the function's actual type (the subtype). At acall_indirectsite annotated with the actual subtype, the JIT then checkssupertype <: subtypeand 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
.wastPoC is attached astest_type_index_bug.wast: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 afuncreftable, and calls it indirectly under both annotations.Steps to Reproduce
cargo build --release -p wasmtime-cli --features gc../target/release/wasmtime wast -Wgc test_type_index_bug.wast.Expected Results
Both
assert_returndirectives pass and return99. This matches:99.specrepo, 3.0.0): the script exits 0 with bothassert_returndirectives passing.Actual Results
The first assertion passes; the second traps:
Versions and Environment
main(v45.0.0); root-cause analysis below is against currentmain.Extra Info
Instance::get_func_ref(crates/wasmtime/src/runtime/vm/instance.rs:884-933) populates aVMFuncRefwith the type stored inModule::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 intoVMFuncRef.type_indexand used by the JIT-emittedcall_indirect/call_refsubtype check. The check becomesdeclared_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_indirectannotation(type $super)$sub <: $super= yes$super <: $super= yes(type $sub)$sub <: $sub= yes$super <: $sub= noV8 and the reference interpreter accept the program:
test_supertest_submain(v45.0)The reference interpreter output, for the record:
The spec says
call_indirectchecks 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.typefield, set when the function is allocated from its defining module.Applied to the PoC: A's allocation stamps the function instance's type as
$sub. B's$superimport does not change it.call_indirect (type $sub)must therefore check$sub <: $suband 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].signatureis the importing module's view of functioni. For imports, that view only records the declaration type, not the type resolved at instantiation.get_func_reftrusts this view unconditionally.Possible fixes:
VMFunctionImport.vmctxback to the exporter'sVMFuncRefinget_func_ref. No struct-layout changes, but fragile under re-export chains and adds work to everyget_func_refcall.type_index: VMSharedTypeIndexfield toVMFunctionImport, populated at instantiation time.construct_func_refthen reads the correct type directly; re-export chains are handled by construction. Costs 4 bytes inVMFunctionImportand a VMContext offset recalculation.get_func_refon defined-vs-imported and retrieve the type