shumbo opened issue #13095:
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:(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 afuncreftable, 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_returndirectives pass and return99. This matches:
- V8 13.6 (Node.js 24.3.0): both calls return
99.- Wasm reference interpreter (
specrepo, 3.0.0): the script exits 0 with bothassert_returndirectives 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 mismatchVersions and Environment
- Wasmtime version:
main(v45.0.0); root-cause analysis below is against currentmain.- 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 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_indirectannotationCorrect check Buggy check Result (type $super)$sub <: $super= yes$super <: $super= yesOK (accidental) (type $sub)$sub <: $sub= yes$super <: $sub= nowrong trap V8 and the reference interpreter accept the program:
Runtime test_supertest_subV8 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_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.- A function instance has a single
typefield, 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$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:
- Pointer-chase from
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.- Add a
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.- Branch in
get_func_refon defined-vs-imported and retrieve the type via (A) or (B) for imports.
shumbo added the bug label to Issue #13095.
shumbo edited issue #13095:
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:(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 afuncreftable, 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_returndirectives pass and return99. This matches:
- V8 13.6 (Node.js 24.3.0): both calls return
99.- Wasm reference interpreter (
specrepo, 3.0.0): the script exits 0 with bothassert_returndirectives 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 mismatchVersions and Environment
- Wasmtime version:
main(v45.0.0); root-cause analysis below is against currentmain.- 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 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_indirectannotationCorrect check Buggy check Result (type $super)$sub <: $super= yes$super <: $super= yesOK (accidental) (type $sub)$sub <: $sub= yes$super <: $sub= nowrong trap V8 and the reference interpreter accept the program:
Runtime test_supertest_subV8 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_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.- A function instance has a single
typefield, 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$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:
- Pointer-chase from
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.- Add a
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.- Branch in
get_func_refon defined-vs-imported and retrieve the type
alexcrichton added the wasm-proposal:gc label to Issue #13095.
fitzgen assigned fitzgen to issue #13095.
fitzgen closed issue #13095:
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:(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 afuncreftable, 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_returndirectives pass and return99. This matches:
- V8 13.6 (Node.js 24.3.0): both calls return
99.- Wasm reference interpreter (
specrepo, 3.0.0): the script exits 0 with bothassert_returndirectives 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 mismatchVersions and Environment
- Wasmtime version:
main(v45.0.0); root-cause analysis below is against currentmain.- 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 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_indirectannotationCorrect check Buggy check Result (type $super)$sub <: $super= yes$super <: $super= yesOK (accidental) (type $sub)$sub <: $sub= yes$super <: $sub= nowrong trap V8 and the reference interpreter accept the program:
Runtime test_supertest_subV8 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_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.- A function instance has a single
typefield, 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$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:
- Pointer-chase from
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.- Add a
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.- Branch in
get_func_refon defined-vs-imported and retrieve the type
Last updated: May 03 2026 at 22:13 UTC