High firefox Logic Error 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionUse-after-free in the SVG component
ComponentDOM
Bug ClassLogic Error
Tracker2082112
Fix commit0a4a573472d1 (firefox) +59/-0
CISA KEVNot listed
CreditedOpenAI Codex Security (kylebot)
Disclosed2026-10-27

Files Changed

  • dom/svg/SVGSwitchElement.cpp
  • testing/web-platform/tests/svg/struct/scripted/switch_reentrant_removal.html
diff --git a/dom/svg/SVGSwitchElement.cpp b/dom/svg/SVGSwitchElement.cpp
index 1fe6c032ea2..d273e3c16d2 100644
--- a/dom/svg/SVGSwitchElement.cpp
+++ b/dom/svg/SVGSwitchElement.cpp
@@ -4,6 +4,7 @@
 
 #include "mozilla/dom/SVGSwitchElement.h"
 
+#include "mozAutoDocUpdate.h"
 #include "mozilla/SVGUtils.h"
 #include "mozilla/dom/SVGSwitchElementBinding.h"
 #include "nsLayoutUtils.h"
@@ -69,6 +70,7 @@ NS_IMPL_ELEMENT_CLONE_WITH_INIT(SVGSwitchElement)
 void SVGSwitchElement::InsertChildBefore(
     nsIContent* aKid, nsIContent* aBeforeThis, bool aNotify, ErrorResult& aRv,
     nsINode* aOldParent, MutationEffectOnScript aMutationEffectOnScript) {
+  mozAutoDocUpdate updateBatch(GetComposedDoc(), aNotify);
   SVGSwitchElementBase::InsertChildBefore(aKid, aBeforeThis, aNotify, aRv,
                                           aOldParent, aMutationEffectOnScript);
   if (aRv.Failed()) {
@@ -81,6 +83,7 @@ void SVGSwitchElement::InsertChildBefore(
 void SVGSwitchElement::RemoveChildNode(
     nsIContent* aKid, bool aNotify, const BatchRemovalState* aState,
     nsINode* aNewParent, MutationEffectOnScript aMutationEffectOnScript) {
+  mozAutoDocUpdate updateBatch(GetComposedDoc(), aNotify);
   SVGSwitchElementBase::RemoveChildNode(aKid, aNotify, aState, aNewParent,
                                         aMutationEffectOnScript);
   MaybeInvalidate();
diff --git a/testing/web-platform/tests/svg/struct/scripted/switch_reentrant_removal.html b/testing/web-platform/tests/svg/struct/scripted/switch_reentrant_removal.html
new file mode 100644
index 00000000000..338c30977c5
--- /dev/null
+++ b/testing/web-platform/tests/svg/struct/scripted/switch_reentrant_removal.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<title>SVG switch active child during reentrant removal</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<svg width="300" height="300">
+  <switch id="switch">
+    <foreignObject id="child" width="100" height="100">
+      <iframe id="trigger" srcdoc="<!doctype html><body>unload"></iframe>
+    </foreignObject>
+    <rect width="50" height="50"/>
+  </switch>
+</svg>
+<iframe id="destination" srcdoc="<!doctype html>
+  <svg width='300' height='300'>
+    <g id='target' transform='translate(1 1)'></g>
+  </svg>"></iframe>
+
+<script>
+async_test(t => {
+  window.addEventListener("load", () => {
+    const sw = document.getElementById("switch");
+    const child = document.getElementById("child");
+    const trigger = document.getElementById("trigger");
+    const destination = document.getElementById("destination").contentDocument;
+    const target = destination.getElementById("target");
+
+    assert_equals(sw.getBBox().width, 100, "The foreignObject is initially active");
+
+    let didUnload = false;
+    trigger.contentWindow.addEventListener("unload", () => {
+      didUnload = true;
+      assert_equals(child.parentNode, null, "The active child has been removed");
+      trigger.remove();
+      target.appendChild(child);
+      assert_equals(child.getBBox().width, 100, "The adopted child has a frame");
+
+      // Invalidate the old switch so its bounds have to be recomputed while
+      // removeChild is still on the stack.
+      sw.setAttribute("transform", "translate(2 2)");
+      assert_equals(sw.getBBox().width, 50, "Only the remaining child contributes bounds");
+      child.remove();
+    }, { once: true });
+
+    sw.removeChild(child);
+    assert_true(didUnload, "The unload handler ran during removeChild");
+    assert_equals(sw.getBBox().width, 50, "The remaining child is still active");
+    t.done();
+ }, { once: true });
+}, "SVG switch active child during reentrant removal");
+</script>
+
+</script>
+</html>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/testing/web-platform/tests/svg/struct/scripted/switch_reentrant_removal.html b/testing/web-platform/tests/svg/struct/scripted/switch_reentrant_removal.html
new file mode 100644
index 00000000000..338c30977c5
--- /dev/null
+++ b/testing/web-platform/tests/svg/struct/scripted/switch_reentrant_removal.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<title>SVG switch active child during reentrant removal</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<svg width="300" height="300">
+  <switch id="switch">
+    <foreignObject id="child" width="100" height="100">
+      <iframe id="trigger" srcdoc="<!doctype html><body>unload"></iframe>
+    </foreignObject>
+    <rect width="50" height="50"/>
+  </switch>
+</svg>
+<iframe id="destination" srcdoc="<!doctype html>
+  <svg width='300' height='300'>
+    <g id='target' transform='translate(1 1)'></g>
+  </svg>"></iframe>
+
+<script>
+async_test(t => {
+  window.addEventListener("load", () => {
+    const sw = document.getElementById("switch");
+    const child = document.getElementById("child");
+    const trigger = document.getElementById("trigger");
+    const destination = document.getElementById("destination").contentDocument;
+    const target = destination.getElementById("target");
+
+    assert_equals(sw.getBBox().width, 100, "The foreignObject is initially active");
+
+    let didUnload = false;
+    trigger.contentWindow.addEventListener("unload", () => {
+      didUnload = true;
+      assert_equals(child.parentNode, null, "The active child has been removed");
+      trigger.remove();
+      target.appendChild(child);
+      assert_equals(child.getBBox().width, 100, "The adopted child has a frame");
+
+      // Invalidate the old switch so its bounds have to be recomputed while
+      // removeChild is still on the stack.
+      sw.setAttribute("transform", "translate(2 2)");
+      assert_equals(sw.getBBox().width, 50, "Only the remaining child contributes bounds");
+      child.remove();
+    }, { once: true });
+
+    sw.removeChild(child);
+    assert_true(didUnload, "The unload handler ran during removeChild");
+    assert_equals(sw.getBBox().width, 50, "The remaining child is still active");
+    t.done();
+ }, { once: true });
+}, "SVG switch active child during reentrant removal");
+</script>
+
+</script>
+</html>
Loading diff…