High firefox UAF 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionUse-after-free in the JavaScript: GC component
ComponentSpiderMonkey
Bug ClassUAF
Tracker2053337
Fix commit014ec97fe248 (firefox) +8/-2
CISA KEVNot listed
CreditedAmy Burnett of OpenAI
Disclosed2026-08-18

Background

Ephemeron edge
A GC bookkeeping link recording that a WeakMap key keeps a value alive only while the key is itself reachable.
`EphemeronEdgeVector`
The per-cell list of ephemeron edges that the marking tracer walks to propagate mark color from a source to its dependent targets.
Atoms zone
The special GC Zone holding interned strings and symbols (atoms) that are shared across all other zones.
Mark color
The GC tri-color state (MarkColor::Gray vs MarkColor::Black) that the tracer pushes across ephemeron edges to keep dependent cells alive.

Root Cause Analysis

When marking weak-map relationships, MarkingTracerT<opts>::markEphemeronEdges iterated every edge in an EphemeronEdgeVector and unconditionally dereferenced edge.target(), assuming each recorded target still pointed at a live GC cell. The invariant that every stored ephemeron edge has a non-null, still-valid target was violated because WeakMapBase::addEphemeronEdgesForEntry did not create an edge for entries whose value was null, while atoms-zone keys still needed an edge to be tracked, leaving the edge set inconsistent with what the tracer later expected to walk. This mismatch could leave the tracer processing an edge whose target had already been swept, producing a use-after-free during GC marking.

The fix makes markEphemeronEdges skip any edge with a null edge.target(), and makes addEphemeronEdgesForEntry also add an edge when the key lives in the atoms zone even if value is null, so the recorded edge set and the tracer’s assumptions stay in agreement. Together these two changes ensure no edge is ever dereferenced with a stale or absent target.

Key insight
The single mistake was that the ephemeron-edge bookkeeping and the marking walk disagreed about which entries produce edges, so the tracer could dereference a target that was never guaranteed valid; the fix defends both sides by null-checking edge.target() before use and by consistently recording edges for atoms-zone keys.

Attack Path

  1. Establish weak-map state Attacker script creates WeakMap entries whose keys are atoms or whose values are null so the edge bookkeeping diverges from the tracer’s expectations.
  2. Provoke inconsistent edges The pre-fix addEphemeronEdgesForEntry skips recording an edge that the tracer nonetheless later expects, leaving a dangling or unexpected target.
  3. Trigger garbage collection Attacker forces GC (allocation pressure or explicit triggers) so markEphemeronEdges walks the EphemeronEdgeVector.
  4. Dereference stale target The tracer reads edge.target() on an edge whose cell was already freed, causing a use-after-free.
  5. Leverage the UAF Corrupted GC state during marking is steered toward attacker-controlled memory reuse for further exploitation.

Impact Assessment

An attacker running untrusted JavaScript gains a use-after-free inside the SpiderMonkey GC marking phase, executing in the content process. Preconditions are only that the attacker can run script and shape weak-map/atom entries that force GC marking; no elevated privileges are required, and the corrupted marking state is a strong primitive toward memory disclosure or code execution.

Changed Functions

FunctionChangeNotes
for
js/src/gc/Marking.cpp
modified

Files Changed

  • js/src/gc/Marking.cpp
  • js/src/gc/WeakMap.cpp

Audit Directions

  • Edge-set vs. walker invariants
    Whenever one function records GC bookkeeping (edges, weak refs) and another walks it, verify both agree on exactly which entries are recorded and dereferenced.
  • Null-target dereference
    Audit all iterations over EphemeronEdgeVector and similar cell lists for unchecked target()/pointer dereferences after a sweep.
  • Atoms-zone special cases
    Review weak-map and marking code for places where atoms-zone keys or shared cells need distinct handling from ordinary zones.
diff --git a/js/src/gc/Marking.cpp b/js/src/gc/Marking.cpp
index 6093d714b15..80c7c18c209 100644
--- a/js/src/gc/Marking.cpp
+++ b/js/src/gc/Marking.cpp
@@ -774,6 +774,10 @@ void MarkingTracerT<opts>::markEphemeronEdges(EphemeronEdgeVector& edges,
   DebugOnly<size_t> initialLength = edges.length();
 
   for (auto& edge : edges) {
+    if (!edge.target()) {
+      continue;
+    }
+
     MarkColor targetColor = std::min(srcColor, MarkColor(edge.color()));
     MOZ_ASSERT(markColor() >= targetColor);
     if (targetColor == markColor()) {
diff --git a/js/src/gc/WeakMap.cpp b/js/src/gc/WeakMap.cpp
index 2b5aca79835..892a44faeaa 100644
--- a/js/src/gc/WeakMap.cpp
+++ b/js/src/gc/WeakMap.cpp
@@ -117,8 +117,10 @@ bool WeakMapBase::addEphemeronEdgesForEntry(MarkColor mapColor,
     }
   }
 
-  if (value && !addEphemeronEdge(mapColor, key, value)) {
-    return false;
+  if (value || key->zone()->isAtomsZone()) {
+    if (!addEphemeronEdge(mapColor, key, value)) {
+      return false;
+    }
   }
 
   return true;
Loading diff…