Found by: Claude (Anthropic) during automated Firefox source code review Affects: Firefox < 148, Thunderbird < 148 Severity: Critical (RCE from web content) Write-up: https://red.anthropic.com/2026/exploit/
MaybeOptimizeFunctionCallBind() in WasmInstance.cpp unwraps
Function.prototype.call.bind(wasmExport) without verifying that the inner
function's Wasm type signature matches the import's declared type. This allows
type confusion between Wasm function signatures at instantiation time.
When Module A declares an import with type (externref) -> (i64) and receives
a call.bind-wrapped export from Module B with type (externref) -> (externref),
the JIT strips the wrapper and links Module B's function body directly. Module A
then interprets the externref return value as i64, leaking the raw object pointer.
| File | Description |
|---|---|
trigger.html |
Basic type confusion trigger (Stage 0 only) |
exploit-chain.html |
Full exploitation chain (Stages 0-6) |
setup-firefox.sh |
Download Firefox 147 on Linux |
setup-firefox.ps1 |
Download Firefox 147 on Windows |
# Linux
chmod +x setup-firefox.sh
./setup-firefox.sh
# Run the trigger
./firefox-147.0/run-exploit.sh trigger.html
# Run the full chain
./firefox-147.0/run-exploit.sh exploit-chain.html# Windows
powershell -ExecutionPolicy Bypass -File setup-firefox.ps1
# Run
.\firefox-147.0\run-exploit.bat exploit-chain.htmlOr serve over HTTP:
python3 -m http.server 8080
# Open http://localhost:8080/exploit-chain.html in Firefox 147| Stage | Primitive | Technique |
|---|---|---|
| 0 | Type confusion trigger | call.bind bypass - f(1337) returns 1337 instead of 0 |
| 1 | addrof |
Module B: (externref)->externref, Module A declares (externref)->i64. Externref return reinterpreted as i64 leaks raw heap pointer |
| 2 | fakeobj |
Reverse: Module B: (i64)->i64, Module A declares (i64)->externref. i64 return reinterpreted as externref forges object reference |
| 3 | Arbitrary read | Forge a fake Float64Array with controlled dataPointer, read through it |
| 4 | Arbitrary write | Same forged TypedArray technique, write through fake[0] = value |
| 5 | Fake ArrayBuffer | Forge ArrayBuffer with controlled backing store for byte-granular R/W |
| 6 | Code execution | Leak Wasm JIT code address, overwrite entry point or write shellcode to RWX region |
In js/src/wasm/WasmInstance.cpp, the fix adds a type signature check:
// Before (vulnerable):
if (IsCallBind(importFun)) {
unwrapped = GetCallBindTarget(importFun);
// Used unwrapped directly without type check
}
// After (patched, Firefox 148):
if (IsCallBind(importFun)) {
unwrapped = GetCallBindTarget(importFun);
if (!CheckWasmSignatureMatch(unwrapped, expectedType)) {
// Reject - type mismatch
return false;
}
}Firefox 147 (vulnerable):
- Stage 0:
result = 1337(call.bind wrapper stripped) - Stage 1:
addrofreturns valid BigInt heap addresses - Stage 2:
fakeobj(addrof(obj)) === objround-trip works - Stages 3-6: Arbitrary R/W and code execution
Firefox 148+ (patched):
- Stage 0:
result = 0(call.bind semantics preserved) or type trap - Stages 1-6: Type confusion does not trigger, primitives non-functional
- The exploit requires Wasm GC support (struct types, externref) which is
available in Firefox 147. The
call.bindtrigger itself works on any Firefox < 148 with basic Wasm support. - SpiderMonkey object layout offsets (Shape*, data pointer, etc.) are build-specific. The exploit-chain.html includes common offsets for release builds; debug builds may differ.
- On aarch64, pointer tagging may affect address interpretation. The addrof/fakeobj primitives handle both x86-64 and aarch64 pointer formats.