CVE-2026-74937
Overview
Background
- Ephemeron edge
- A GC bookkeeping link recording that a
WeakMapkey keeps avaluealive 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
Zoneholding interned strings and symbols (atoms) that are shared across all other zones. - Mark color
- The GC tri-color state (
MarkColor::GrayvsMarkColor::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.
edge.target() before use and by consistently recording edges for atoms-zone keys.Attack Path
- Establish weak-map state
Attacker script creates
WeakMapentries whose keys are atoms or whose values are null so the edge bookkeeping diverges from the tracer’s expectations. - Provoke inconsistent edges
The pre-fix
addEphemeronEdgesForEntryskips recording an edge that the tracer nonetheless later expects, leaving a dangling or unexpected target. - Trigger garbage collection
Attacker forces GC (allocation pressure or explicit triggers) so
markEphemeronEdgeswalks theEphemeronEdgeVector. - Dereference stale target
The tracer reads
edge.target()on an edge whose cell was already freed, causing a use-after-free. - Leverage the UAF Corrupted GC state during marking is steered toward attacker-controlled memory reuse for further exploitation.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
forjs/src/gc/Marking.cpp |
modified |
Files Changed
js/src/gc/Marking.cppjs/src/gc/WeakMap.cpp
Audit Directions
- Edge-set vs. walker invariantsWhenever 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 dereferenceAudit all iterations over
EphemeronEdgeVectorand similar cell lists for uncheckedtarget()/pointer dereferences after a sweep. - Atoms-zone special casesReview weak-map and marking code for places where atoms-zone keys or shared cells need distinct handling from ordinary zones.
Patch
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;