CVE-2026-74938
Overview
Background
- `RemapRemoteWindowProxies`
- SpiderMonkey routine that transplants a set of remote (cross-process)
WindowProxyobjects onto a new target proxy in another compartment. - Dead wrapper
- a placeholder
JSObjectthat replaces a wrapper whose referent is gone, forwarding or trapping accesses that would otherwise dangle. - Finalization observer
- GC bookkeeping for
FinalizationRegistry/WeakRefentries that must be notified or updated when their observed target object moves or is replaced. - `relocateFinalizationObserverTarget`
GCRuntimemethod that repoints any finalization observer currently watching one object value at a replacement target value.
Root Cause Analysis
In js::RemapRemoteWindowProxies, each old remote proxy in otherProxies was turned into a deadWrapper and remapped onto target via js::RemapDeadWrapper, but the GC’s finalization observer tables were never told that the old object’s identity was being folded into target. This violated the invariant that every finalization observer must track the live object that supersedes its original target; observers registered against the object that becomes a dead wrapper were left pointing at a stale entry rather than at target. Because the remap is a mitigation-relevant identity change, leaving observers unrelocated is a logic error that lets FinalizationRegistry/WeakRef state diverge from the actual object graph after the transplant.
The fix obtains the GCRuntime via cx->runtime()->gc and, for each proxy, calls gc->relocateFinalizationObserverTarget(ObjectValue(*deadWrapper), ObjectValue(*target)) before RemapDeadWrapper, treating an allocation failure as fatal through oomUnsafe.crash. This restores the invariant so finalization observation follows the object to its new target instead of observing the discarded dead wrapper.
relocateFinalizationObserverTarget call in the remap loop so observer state is moved to target in lockstep with the proxy transplant.Attack Path
- Register an observer
Script places a remote
WindowProxyunder observation by aFinalizationRegistryorWeakRef. - Trigger a remap
A navigation or process/compartment change drives
js::RemapRemoteWindowProxies, converting that proxy into adeadWrapperand transplanting it onto a newtarget. - Observer left stale
Without relocation, the finalization observer keeps referencing the pre-remap identity instead of following it to
target. - Bypass the mitigation The divergence between observer state and the actual object identity defeats the GC bookkeeping the remap was meant to preserve, yielding inconsistent finalization behavior an attacker can steer.
Impact Assessment
FinalizationRegistry/WeakRef behavior. Preconditions are the ability to register a finalization observer on a remote WindowProxy and to induce a remap of that proxy. The metadata classifies this as a high-impact logic error / mitigation bypass in the JavaScript GC component.Changed Functions
| Function | Change | Notes |
|---|---|---|
forjs/src/jsapi.cpp |
modified |
Files Changed
js/src/jsapi.cpp
Audit Directions
- Identity transplantsWherever an object is replaced by a dead wrapper or remapped onto a new target, verify GC observer tables (
FinalizationRegistry,WeakRef, weak maps) are relocated in the same step. - Cross-compartment remap pathsAudit other callers of
RemapDeadWrapperand proxy transplant helpers for missingrelocateFinalizationObserverTarget-style updates. - OOM handling in GC-critical loopsConfirm relocation calls that can fail treat failure as fatal via
oomUnsafe.crashrather than silently continuing with partially migrated observer state.
Patch
diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp
index f7fd53a47e2..9daab1a54ba 100644
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -803,9 +803,16 @@ JS_PUBLIC_API void js::RemapRemoteWindowProxies(
target.set(targetCompartmentProxy);
}
+ gc::GCRuntime* gc = &cx->runtime()->gc;
RootedField<JSObject*, 2> deadWrapper(roots);
for (JSObject*& obj : otherProxies) {
deadWrapper = obj;
+
+ if (!gc->relocateFinalizationObserverTarget(ObjectValue(*deadWrapper),
+ ObjectValue(*target))) {
+ oomUnsafe.crash("js::RemapRemoteWindowProxies");
+ }
+
js::RemapDeadWrapper(cx, deadWrapper, target);
}
}