High firefox Logic Error 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionUse-after-free in the DOM: Bindings (WebIDL) component
ComponentDOM
Bug ClassLogic Error
Tracker2082000
Fix commit725cb0d43718 (firefox) +53/-5
CISA KEVNot listed
CreditedOpenAI Codex Security (kylebot)
Disclosed2026-10-27

Changed Functions

FunctionChangeNotes
if
dom/bindings/BindingUtils.h
modified

Files Changed

  • dom/bindings/BindingUtils.h
  • dom/bindings/Codegen.py
  • dom/bindings/DOMJSProxyHandler.cpp
  • dom/bindings/DOMJSProxyHandler.h
diff --git a/dom/bindings/BindingUtils.h b/dom/bindings/BindingUtils.h
index 19bf4535deb..9ade09b0bc6 100644
--- a/dom/bindings/BindingUtils.h
+++ b/dom/bindings/BindingUtils.h
@@ -2843,6 +2843,9 @@ class MOZ_STACK_CLASS BindingJSObjectCreator {
 
   ~BindingJSObjectCreator() {
     if (mReflector) {
+      if (IsDOMProxy(mReflector)) {
+        js::SetProxyPrivate(mReflector, JS::UndefinedValue());
+      }
       JS::SetReservedSlot(mReflector, DOM_OBJECT_SLOT, JS::UndefinedValue());
     }
   }
diff --git a/dom/bindings/Codegen.py b/dom/bindings/Codegen.py
index e5208d330be..4c22bdccd64 100644
--- a/dom/bindings/Codegen.py
+++ b/dom/bindings/Codegen.py
@@ -4708,6 +4708,14 @@ class CGWrapWithCacheMethod(CGAbstractMethod):
         )
 
         if self.descriptor.proxy:
+            if self.descriptor.hasLegacyUnforgeableMembers:
+                failureCode = fill(
+                    """
+                    DOMProxyHandler::ClearExpandoOnWrapFailure(aReflector);
+                    $*{failureCode}
+                    """,
+                    failureCode=failureCode,
+                )
             finalize = "DOMProxyHandler::getInstance()->finalize"
         else:
             finalize = FINALIZE_HOOK_NAME
@@ -4848,8 +4856,6 @@ class CGWrapNonWrapperCacheMethod(CGAbstractMethod):
         )
 
     def definition_body(self):
-        failureCode = "return false;\n"
-
         declareProto = DeclareProto(self.descriptor, noGivenProto=self.noGivenProto)
         if self.noGivenProto:
             assertGivenProto = ""
@@ -4859,6 +4865,15 @@ class CGWrapNonWrapperCacheMethod(CGAbstractMethod):
                 MOZ_ASSERT_IF(aGivenProto, js::IsObjectInContextCompartment(aGivenProto, aCx));
                 """
             )
+        failureCode = "return false;\n"
+        if self.descriptor.proxy and self.descriptor.hasLegacyUnforgeableMembers:
+            failureCode = fill(
+                """
+                DOMProxyHandler::ClearExpandoOnWrapFailure(aReflector);
+                $*{failureCode}
+                """,
+                failureCode=failureCode,
+            )
         return fill(
             """
             $*{assertions}
diff --git a/dom/bindings/DOMJSProxyHandler.cpp b/dom/bindings/DOMJSProxyHandler.cpp
index d9ba5df6bf8..d2fdfd6bfbd 100644
--- a/dom/bindings/DOMJSProxyHandler.cpp
+++ b/dom/bindings/DOMJSProxyHandler.cpp
@@ -154,6 +154,27 @@ JSObject* DOMProxyHandler::EnsureExpandoObject(JSContext* cx,
   return expando;
 }
 
+// static
+void DOMProxyHandler::ClearExpandoOnWrapFailure(JSObject* obj) {
+  MOZ_ASSERT(IsDOMProxy(obj), "expected a DOM proxy object");
+
+  JS::Value v = js::GetProxyPrivate(obj);
+  if (v.isObject() || v.isUndefined()) {
+    return;
+  }
+
+  MOZ_ASSERT(!js::GetProxyReservedSlot(obj, DOM_OBJECT_SLOT).isUndefined(),
+             "Must not clear the native slot before stopping tracing the "
+             "expando owned by native object");
+
+  auto* expandoAndGeneration =
+      static_cast<JS::ExpandoAndGeneration*>(v.toPrivate());
+  // If wrapper initialization fails, the proxy no longer owns the native
+  // object, so it should not trace the expando owned by the native object.
+  // Reset the expando as well to avoid leaving an untraced reference to it.
+  expandoAndGeneration->expando = JS::UndefinedValue();
+}
+
 bool DOMProxyHandler::preventExtensions(JSContext* cx,
                                         JS::Handle<JSObject*> proxy,
                                         JS::ObjectOpResult& result) const {
@@ -290,9 +311,12 @@ void ShadowingDOMProxyHandler::trace(JSTracer* trc, JSObject* proxy) const {
   JS::Value v = js::GetProxyPrivate(proxy);
   MOZ_ASSERT(!v.isObject(), "Should not have expando object directly!");
 
-  // The proxy's private slot is set when we allocate the proxy,
-  // so it cannot be |undefined|.
-  MOZ_ASSERT(!v.isUndefined());
+  if (v.isUndefined()) {
+    MOZ_ASSERT(js::GetProxyReservedSlot(proxy, DOM_OBJECT_SLOT).isUndefined(),
+               "Only a proxy whose initialization failed should have an "
+               "undefined private slot");
+    return;
+  }
 
   auto* expandoAndGeneration =
       static_cast<JS::ExpandoAndGeneration*>(v.toPrivate());
diff --git a/dom/bindings/DOMJSProxyHandler.h b/dom/bindings/DOMJSProxyHandler.h
index 0e4d3e1c616..b325450566c 100644
--- a/dom/bindings/DOMJSProxyHandler.h
+++ b/dom/bindings/DOMJSProxyHandler.h
@@ -125,6 +125,12 @@ class DOMProxyHandler : public BaseDOMProxyHandler {
   static JSObject* EnsureExpandoObject(JSContext* cx,
                                        JS::Handle<JSObject*> obj);
 
+  /*
+   * Clear the ExpandoAndGeneration from a proxy whose initialization failed.
+   * This must be called before releasing the native object.
+   */
+  static void ClearExpandoOnWrapFailure(JSObject* obj);
+
   static const char family;
 };
 
Loading diff…