CVE-2026-16412
Overview
Background
- Realm
- A SpiderMonkey security/GC compartment scope that owns a set of JS objects and defines which global they belong to.
- `AutoRealmUnchecked`
- An RAII helper that temporarily sets
cx->realm()to a target realm without the normal security-membrane checks. - WebAssembly table
- A wasm growable array of references (here
funcrefelements) that can hold exported functions callable across module and realm boundaries. - `Instance::getExportedFunction`
- The wasm
Instancemethod that lazily creates or returns theJSFunctionwrapper for an exported wasm function.
Root Cause Analysis
The vulnerable path is Table::getFuncRef in js/src/wasm/WasmTable.cpp, which reads a funcref element out of a wasm table and calls instance.getExportedFunction to materialize the corresponding JSFunction. Because a table element can reference an Instance living in a different Realm than the currently active cx->realm(), this call could run while the context was entered in the wrong realm. Instance::getExportedFunction allocates and caches the exported-function object and touches per-instance data, all of which implicitly assume the context realm matches the instance’s own realm; that invariant (realm() == cx->realm()) was silently violated.
The fix wraps the call in AutoRealmUnchecked ar(cx, instance.realm()) so the context is entered into the instance’s realm before the function object is created, and hardens Instance::getExportedFunction with a MOZ_RELEASE_ASSERT(realm() == cx->realm()) so any future caller that breaks the invariant crashes safely instead of corrupting memory. This ensures objects are allocated and cached in the correct realm, keeping GC and object-provenance bookkeeping consistent.
cx->realm(). The fix enters the instance’s realm via AutoRealmUnchecked at the call site and enforces the realm-match invariant with a release assertion inside getExportedFunction.Attack Path
- Cross-realm table setup
Attacker script obtains a wasm
Tablewhosefuncrefelements point at exported functions belonging to anInstancein a differentRealmthan the caller’s. - Trigger the read
The attacker reads that element (for example via
Table.prototype.getor a wasmtable.getpath) soTable::getFuncRefruns whilecx->realm()is the wrong realm. - Wrong-realm materialization
Instance::getExportedFunctionlazily creates and caches theJSFunctionwhile the context is entered in the mismatched realm, associating the object with the wrong compartment. - Realm/GC inconsistency The mis-attributed object breaks realm and garbage-collection provenance assumptions, producing exploitable memory corruption.
Impact Assessment
Table holding a funcref to an export from an instance in a different realm, then read that element. The corruption occurs in the SpiderMonkey engine within the affected content process context.Files Changed
js/src/wasm/WasmInstance.cppjs/src/wasm/WasmTable.cpp
Audit Directions
- Cross-realm lazy allocationAudit every path that reads
funcref/anyrefvalues or wasm exports and lazily creates JS objects, confirming anAutoRealm/AutoRealmUncheckedguard enters the owning instance’s realm first. - Realm invariant assertionsLook for engine methods that assume
realm() == cx->realm()but lack aMOZ_RELEASE_ASSERT, and add or verify such guards on object-creating entry points. - Table and instance boundariesReview
WasmTable.cppandWasmInstance.cppcall sites that passcxacrossInstanceboundaries, ensuring the context realm is aligned with the targetInstancebefore allocation or caching.
Patch
diff --git a/js/src/wasm/WasmInstance.cpp b/js/src/wasm/WasmInstance.cpp
index 8493d4d994e..a12cf979ea4 100644
--- a/js/src/wasm/WasmInstance.cpp
+++ b/js/src/wasm/WasmInstance.cpp
@@ -3895,6 +3895,8 @@ static bool WasmCall(JSContext* cx, unsigned argc, Value* vp) {
bool Instance::getExportedFunction(JSContext* cx, uint32_t funcIndex,
MutableHandleFunction result) {
+ MOZ_RELEASE_ASSERT(realm() == cx->realm());
+
uint32_t funcExportIndex = codeMeta().findFuncExportIndex(funcIndex);
FuncExportInstanceData& instanceData =
funcExportInstanceData(funcExportIndex);
diff --git a/js/src/wasm/WasmTable.cpp b/js/src/wasm/WasmTable.cpp
index 2bcdcf43f23..daf45189206 100644
--- a/js/src/wasm/WasmTable.cpp
+++ b/js/src/wasm/WasmTable.cpp
@@ -19,12 +19,12 @@
#include "mozilla/CheckedInt.h"
#include "vm/JSContext.h"
-#include "vm/Realm.h"
#include "wasm/WasmInstance.h"
#include "wasm/WasmJS.h"
#include "wasm/WasmValue.h"
#include "gc/StableCellHasher-inl.h"
+#include "vm/Realm-inl.h"
#include "wasm/WasmInstance-inl.h"
using namespace js;
@@ -171,6 +171,7 @@ bool Table::getFuncRef(JSContext* cx, uint32_t address,
Instance& instance = *elem.instance;
const CodeRange& codeRange = *instance.code().lookupFuncRange(elem.code);
+ AutoRealmUnchecked ar(cx, instance.realm());
return instance.getExportedFunction(cx, codeRange.funcIndex(), fun);
}