Low firefox Logic Error 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactlow
DescriptionInvalid pointer in the JavaScript: GC component
ComponentSpiderMonkey
Bug ClassLogic Error
Tracker2077075
Fix commitac08cf2dd003 (firefox) +17/-16
CISA KEVNot listed
CreditedMozilla
Disclosed2026-10-27

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.

Key insight
The code asserted an invariant (no gray→nursery edges) that proxy transplant legitimately violates; the fix drops the bad assertion rather than changing marking behavior.

Attack Path

  1. ENTRY From JS, create a transplantable object and a gray GC root referencing it (transplantableObject(), grayRoot().push(...)).
  2. TRIGGER Start an incremental GC and transplant(destination) so a nursery pointer is written into a gray proxy during marking.
  3. MISSING CHECK ShouldMarkCrossCompartment treats the resulting gray→nursery edge as impossible and MOZ_CRASHes (debug builds only).
  4. IMPACT Spurious abort in debug builds; no release-build memory-safety primitive is demonstrated. (Low severity per the advisory.)

Impact Assessment

The faulty assertion is compiled only under 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.cpp
  • js/src/jit-test/tests/gc/bug-2077075.js

Audit Directions

  • Over-strict GC invariants
    Audit MOZ_CRASH/assertions that assume gray things are never touched; cross-check against transplant/wrapper-remapping paths and AutoTouchingGrayThings.
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();
Loading diff…

Regression Test / PoC

shipped with the fix
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();
Loading diff…