CVE-2026-118026
Overview
Background
- Gray marking / cross-compartment edges
- incremental-GC bookkeeping; the engine generally assumes it never operates on gray (not-yet-marked) things.
- Proxy transplant
- remapping an object’s wrappers across zones; it can legitimately write a nursery pointer into a gray proxy.
- `AutoTouchingGrayThings`
- marker for the sanctioned cases where gray things are touched.
Root Cause Analysis
ShouldMarkCrossCompartment contained a DEBUG-only block that MOZ_CRASHed if it ever saw a gray source object with a non-tenured (nursery) destination, on the assumption that such an edge is impossible. That assumption is wrong: proxy transplant remaps wrappers in all zones and can write a nursery pointer into a gray proxy, so the edge can legitimately occur and the debug assertion fired spuriously.
The fix removes the MOZ_CRASH block and documents that gray-to-nursery edges are expected here; the added bug-2077075.js test reproduces the transplant sequence. This is a correctness/invariant error in debug tooling rather than a release memory-safety primitive, consistent with the advisory’s low severity; the advisory’s “invalid pointer” phrasing overstates what is a debug-only assertion.
Attack Path
- ENTRY
From JS, create a transplantable object and a gray GC root referencing it (
transplantableObject(),grayRoot().push(...)). - TRIGGER
Start an incremental GC and
transplant(destination)so a nursery pointer is written into a gray proxy during marking. - MISSING CHECK
ShouldMarkCrossCompartmenttreats the resulting gray→nursery edge as impossible andMOZ_CRASHes (debug builds only). - IMPACT Spurious abort in debug builds; no release-build memory-safety primitive is demonstrated. (Low severity per the advisory.)
Impact Assessment
DEBUG, so release Firefox is not crashed by it; impact is limited to incorrect GC bookkeeping assumptions, matching the advisory’s low rating. No attacker memory-safety primitive is shown by the patch.Files Changed
js/src/gc/Marking.cppjs/src/jit-test/tests/gc/bug-2077075.js
Audit Directions
- Over-strict GC invariantsAudit
MOZ_CRASH/assertions that assume gray things are never touched; cross-check against transplant/wrapper-remapping paths andAutoTouchingGrayThings.
Patch
diff --git a/js/src/gc/Marking.cpp b/js/src/gc/Marking.cpp
index 5f74e5ff931..6e3d2f9d106 100644
--- a/js/src/gc/Marking.cpp
+++ b/js/src/gc/Marking.cpp
@@ -242,22 +242,11 @@ template void CheckTracedThing<wasm::AnyRef>(JSTracer*, const wasm::AnyRef&);
static inline bool ShouldMarkCrossCompartment(GCMarker* marker, JSObject* src,
Cell* dstCell, const char* name) {
-#ifdef DEBUG
- if (src->isMarkedGray() && !dstCell->isTenured()) {
- // Bug 1743098: This shouldn't be possible but it does seem to happen. Log
- // some useful information in debug builds.
- SEprinter printer;
- printer.printf(
- "ShouldMarkCrossCompartment: cross compartment edge '%s' from gray "
- "object to nursery thing\n",
- name);
- printer.put("src: ");
- src->dump(printer);
- printer.put("dst: ");
- dstCell->dump(printer);
- MOZ_CRASH("Found cross compartment edge from gray object to nursery thing");
- }
-#endif
+ // Note that we can observe gray to nursery edges here. This does not normally
+ // happen because of the invariant that the JS engine does not operate on gray
+ // GC things. However there are cases where we ignore this: for example when a
+ // proxy is transplanted wrappers in all zones are remapped and this may write
+ // a nursery pointer into a gray proxy. See uses of AutoTouchingGrayThings.
CellColor targetColor = AsCellColor(marker->markColor());
CellColor currentColor = dstCell->color();
diff --git a/js/src/jit-test/tests/gc/bug-2077075.js b/js/src/jit-test/tests/gc/bug-2077075.js
new file mode 100644
index 00000000000..b0bbc993fb9
--- /dev/null
+++ b/js/src/jit-test/tests/gc/bug-2077075.js
@@ -0,0 +1,12 @@
+gczeal(0);
+const {object, transplant} = transplantableObject();
+const holder = newGlobal({newCompartment: true});
+const destination = newGlobal({newCompartment: true});
+holder.target = object;
+holder.eval('grayRoot().push(target); target = undefined;');
+gc();
+schedulezone(destination);
+startgc(1);
+while (gcstate() === 'Prepare' || gcstate() === 'MarkRoots') gcslice(1);
+transplant(destination);
+finishgc();
Regression Test / PoC
diff --git a/js/src/jit-test/tests/gc/bug-2077075.js b/js/src/jit-test/tests/gc/bug-2077075.js
new file mode 100644
index 00000000000..b0bbc993fb9
--- /dev/null
+++ b/js/src/jit-test/tests/gc/bug-2077075.js
@@ -0,0 +1,12 @@
+gczeal(0);
+const {object, transplant} = transplantableObject();
+const holder = newGlobal({newCompartment: true});
+const destination = newGlobal({newCompartment: true});
+holder.target = object;
+holder.eval('grayRoot().push(target); target = undefined;');
+gc();
+schedulezone(destination);
+startgc(1);
+while (gcstate() === 'Prepare' || gcstate() === 'MarkRoots') gcslice(1);
+transplant(destination);
+finishgc();