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
Tracker2010940
Fix commit2e29a3d3d930 (firefox) +25/-1
CISA KEVNot listed
Creditedx0e
Disclosed2026-02-24

Files Changed

  • js/src/builtin/FinalizationRegistryObject.cpp
  • js/src/builtin/FinalizationRegistryObject.h
  • js/src/gc/FinalizationObservers.cpp
diff --git a/js/src/builtin/FinalizationRegistryObject.cpp b/js/src/builtin/FinalizationRegistryObject.cpp
index cfe546ec5fb..480cd28284f 100644
--- a/js/src/builtin/FinalizationRegistryObject.cpp
+++ b/js/src/builtin/FinalizationRegistryObject.cpp
@@ -720,6 +720,13 @@ void FinalizationQueueObject::setHasRegistry(bool newValue) {
   setReservedSlot(HasRegistrySlot, BooleanValue(newValue));
 }
 
+void FinalizationQueueObject::clear() {
+  MOZ_ASSERT(!hasRegistry());
+  if (FinalizationRecordVector* records = recordsToBeCleanedUp()) {
+    records->clear();
+  }
+}
+
 bool FinalizationQueueObject::hasRegistry() const {
   return getReservedSlot(HasRegistrySlot).toBoolean();
 }
@@ -825,9 +832,11 @@ bool FinalizationQueueObject::cleanupQueuedRecords(
   //    b. Remove cell from finalizationRegistry.[[Cells]].
   //    c. Perform ? Call(callback, undefined, « cell.[[HeldValue]] »).
 
+  FinalizationRecordVector* records = queue->recordsToBeCleanedUp();
+  MOZ_ASSERT_IF(!queue->hasRegistry(), records->empty());
+
   RootedValue heldValue(cx);
   RootedValue rval(cx);
-  FinalizationRecordVector* records = queue->recordsToBeCleanedUp();
   while (!records->empty()) {
     FinalizationRecordObject* record = records->popCopy();
     MOZ_ASSERT(!record->isInRecordMap());
diff --git a/js/src/builtin/FinalizationRegistryObject.h b/js/src/builtin/FinalizationRegistryObject.h
index 42c2781ca57..b19d2cb04fd 100644
--- a/js/src/builtin/FinalizationRegistryObject.h
+++ b/js/src/builtin/FinalizationRegistryObject.h
@@ -233,6 +233,7 @@ class FinalizationQueueObject : public NativeObject {
   void setQueuedForCleanup(bool value);
 
   void setHasRegistry(bool newValue);
+  void clear();
 
   static FinalizationQueueObject* create(JSContext* cx,
                                          HandleObject cleanupCallback);
diff --git a/js/src/gc/FinalizationObservers.cpp b/js/src/gc/FinalizationObservers.cpp
index b67f07890a9..47112af8abd 100644
--- a/js/src/gc/FinalizationObservers.cpp
+++ b/js/src/gc/FinalizationObservers.cpp
@@ -375,6 +375,11 @@ void FinalizationObservers::traceWeakFinalizationRegistryEdges(JSTracer* trc) {
     if (result.isDead()) {
       auto* registry = result.initialTarget();
       registry->queue()->setHasRegistry(false);
+
+      // Remove any queued records. These might be dead since the registry was
+      // not marked.
+      registry->queue()->clear();
+
       e.removeFront();
     } else {
       FinalizationRegistryObject* registry = result.finalTarget();
@@ -414,8 +419,16 @@ void FinalizationObservers::traceWeakFinalizationRegistryEdges(JSTracer* trc) {
         auto* record = &iter->as<FinalizationRecordObject>();
         record->setInRecordMap(false);
         record->unlink();
+
+        // Move the record to the queue object. In theory this requires a read
+        // barrier since the record pointer is weak. However we have may have
+        // finished marking the record's zone at this point so this is not
+        // possible. Instead note that the record will be marked if the registry
+        // is alive. If not then we clear any queued records when we discover
+        // the the registry is dead above.
         FinalizationQueueObject* queue = record->queue();
         queue->queueRecordToBeCleanedUp(record);
+
         if (shouldQueueFinalizationRegistryForCleanup(queue)) {
           gc->queueFinalizationRegistryForCleanup(queue);
         }
@@ -435,6 +448,7 @@ bool FinalizationObservers::shouldQueueFinalizationRegistryForCleanup(
   //
   // In this case we defer queuing the registry and this happens when the
   // registry is swept.
+  MOZ_ASSERT(queue->hasRegistry());
   Zone* zone = queue->zone();
   return !zone->wasGCStarted() || zone->gcState() >= Zone::Sweep;
 }
Loading diff…