High firefox Logic Error 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionUse-after-free in the Layout component
ComponentLayout
Bug ClassLogic Error
Tracker2076268
Fix commitb0af5bf7cd94 (firefox) +81/-15
CISA KEVNot listed
CreditedAmy Burnett of OpenAI
Disclosed2026-10-27

Files Changed

  • layout/base/ContainStyleScopeManager.cpp
  • layout/base/nsCounterManager.cpp
  • layout/base/nsCounterManager.h
  • testing/web-platform/tests/css/css-contain/crashtests/contain-style-first-letter-colgroup.html
  • testing/web-platform/tests/css/css-contain/crashtests/contain-style-first-letter.html
diff --git a/layout/base/ContainStyleScopeManager.cpp b/layout/base/ContainStyleScopeManager.cpp
index a019235cace..832149e93e8 100644
--- a/layout/base/ContainStyleScopeManager.cpp
+++ b/layout/base/ContainStyleScopeManager.cpp
@@ -139,6 +139,10 @@ bool ContainStyleScopeManager::DestroyCounterNodesFor(nsIFrame* aFrame) {
 }
 
 bool ContainStyleScopeManager::AddCounterChanges(nsIFrame* aNewFrame) {
+  if (!nsCounterManager::HasCounterChanges(aNewFrame)) {
+    MOZ_ASSERT(!aNewFrame->HasAnyStateBits(NS_FRAME_HAS_CSS_COUNTER_STYLE));
+    return false;
+  }
   return GetOrCreateScopeForContent(
              aNewFrame->GetContent()->GetFlattenedTreeParent())
       .GetCounterManager()
diff --git a/layout/base/nsCounterManager.cpp b/layout/base/nsCounterManager.cpp
index 36a1101297c..ff378ca5c26 100644
--- a/layout/base/nsCounterManager.cpp
+++ b/layout/base/nsCounterManager.cpp
@@ -427,25 +427,29 @@ static bool HasCounters(const nsStyleContent& aStyle) {
          !aStyle.mCounterReset.IsEmpty() || !aStyle.mCounterSet.IsEmpty();
 }
 
+// For elements with 'display:list-item' we add a default
+// 'counter-increment:list-item' unless 'counter-increment' already has a
+// value for 'list-item'.
+//
+// https://drafts.csswg.org/css-lists-3/#declaring-a-list-item
+//
+// We inherit `display` for some anonymous boxes, but we don't want them to
+// increment the list-item counter.
+static bool RequiresListItemIncrement(const nsIFrame* aFrame) {
+  return aFrame->StyleDisplay()->IsListItem() && !aFrame->Style()->IsAnonBox();
+}
+
+bool nsCounterManager::HasCounterChanges(const nsIFrame* aFrame) {
+  return RequiresListItemIncrement(aFrame) ||
+         HasCounters(*aFrame->StyleContent());
+}
+
 bool nsCounterManager::AddCounterChanges(nsIFrame* aFrame) {
-  // For elements with 'display:list-item' we add a default
-  // 'counter-increment:list-item' unless 'counter-increment' already has a
-  // value for 'list-item'.
-  //
-  // https://drafts.csswg.org/css-lists-3/#declaring-a-list-item
-  //
-  // We inherit `display` for some anonymous boxes, but we don't want them to
-  // increment the list-item counter.
-  const bool requiresListItemIncrement =
-      aFrame->StyleDisplay()->IsListItem() && !aFrame->Style()->IsAnonBox();
+  MOZ_ASSERT(HasCounterChanges(aFrame));
 
+  const bool requiresListItemIncrement = RequiresListItemIncrement(aFrame);
   const nsStyleContent* styleContent = aFrame->StyleContent();
 
-  if (!requiresListItemIncrement && !HasCounters(*styleContent)) {
-    MOZ_ASSERT(!aFrame->HasAnyStateBits(NS_FRAME_HAS_CSS_COUNTER_STYLE));
-    return false;
-  }
-
   aFrame->AddStateBits(NS_FRAME_HAS_CSS_COUNTER_STYLE);
 
   bool dirty = false;
diff --git a/layout/base/nsCounterManager.h b/layout/base/nsCounterManager.h
index 0c74532126c..91fef59d53d 100644
--- a/layout/base/nsCounterManager.h
+++ b/layout/base/nsCounterManager.h
@@ -288,6 +288,9 @@ class nsCounterManager {
   explicit nsCounterManager(mozilla::ContainStyleScope* scope)
       : mScope(scope) {}
 
+  // Whether aFrame resets, increments or sets any counter.
+  static bool HasCounterChanges(const nsIFrame* aFrame);
+
   // Returns true if dirty
   bool AddCounterChanges(nsIFrame* aFrame);
 
diff --git a/testing/web-platform/tests/css/css-contain/crashtests/contain-style-first-letter-colgroup.html b/testing/web-platform/tests/css/css-contain/crashtests/contain-style-first-letter-colgroup.html
new file mode 100644
index 00000000000..0b2a2147b8a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-contain/crashtests/contain-style-first-letter-colgroup.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<meta charset="utf-8">
+<style>
+  #host::first-letter { float: left; font-size: 200% }
+  .scope { contain: style }
+  .counter { counter-increment: list-item }
+  .counter::before { content: counter(list-item) }
+  .counters::after { content: counters(list-item, '.') }
+  .active .old { display: none !important }
+</style>
+<div id="host"><span class="old">First</span><span id="wrapper" style="display:contents"><span id="a" class="scope" style="display:contents">Second</span></span></div>
+<div id="b" class="scope"><span id="before" class="counter"></span></div>
+<script>
+function flush() { document.body.offsetHeight; }
+onload = () => {
+  flush();
+  document.body.classList.add("active");
+  a.style.display = "inline";
+  wrapper.style.display = "table-column-group";
+  flush();
+  a.className = "counter counters";
+  b.append(a);
+  flush();
+  a.remove();
+  flush();
+  before.remove();
+  flush();
+};
+</script>
diff --git a/testing/web-platform/tests/css/css-contain/crashtests/contain-style-first-letter.html b/testing/web-platform/tests/css/css-contain/crashtests/contain-style-first-letter.html
new file mode 100644
index 00000000000..6e1208a654a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-contain/crashtests/contain-style-first-letter.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<meta charset="utf-8">
+<style>
+  #host::first-letter { float: left; font-size: 200% }
+  .scope { contain: style }
+  .q::before { content: open-quote }
+  .active .old, .active .a { display: none !important }
+</style>
+<div id="host"><span class="old">First</span><span id="a" class="a scope" style="display:contents">Second</span></div>
+<div id="b" class="scope"><span id="before" class="q"></span></div>
+<script>
+function flush() { document.body.offsetHeight; }
+onload = () => {
+  flush();
+  host.classList.add("active");
+  flush();
+  a.className = "q";
+  a.style.display = "inline";
+  b.append(a);
+  flush();
+  a.remove();
+  flush();
+  before.remove();
+  flush();
+};
+</script>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/testing/web-platform/tests/css/css-contain/crashtests/contain-style-first-letter-colgroup.html b/testing/web-platform/tests/css/css-contain/crashtests/contain-style-first-letter-colgroup.html
new file mode 100644
index 00000000000..0b2a2147b8a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-contain/crashtests/contain-style-first-letter-colgroup.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<meta charset="utf-8">
+<style>
+  #host::first-letter { float: left; font-size: 200% }
+  .scope { contain: style }
+  .counter { counter-increment: list-item }
+  .counter::before { content: counter(list-item) }
+  .counters::after { content: counters(list-item, '.') }
+  .active .old { display: none !important }
+</style>
+<div id="host"><span class="old">First</span><span id="wrapper" style="display:contents"><span id="a" class="scope" style="display:contents">Second</span></span></div>
+<div id="b" class="scope"><span id="before" class="counter"></span></div>
+<script>
+function flush() { document.body.offsetHeight; }
+onload = () => {
+  flush();
+  document.body.classList.add("active");
+  a.style.display = "inline";
+  wrapper.style.display = "table-column-group";
+  flush();
+  a.className = "counter counters";
+  b.append(a);
+  flush();
+  a.remove();
+  flush();
+  before.remove();
+  flush();
+};
+</script>
diff --git a/testing/web-platform/tests/css/css-contain/crashtests/contain-style-first-letter.html b/testing/web-platform/tests/css/css-contain/crashtests/contain-style-first-letter.html
new file mode 100644
index 00000000000..6e1208a654a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-contain/crashtests/contain-style-first-letter.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<meta charset="utf-8">
+<style>
+  #host::first-letter { float: left; font-size: 200% }
+  .scope { contain: style }
+  .q::before { content: open-quote }
+  .active .old, .active .a { display: none !important }
+</style>
+<div id="host"><span class="old">First</span><span id="a" class="a scope" style="display:contents">Second</span></div>
+<div id="b" class="scope"><span id="before" class="q"></span></div>
+<script>
+function flush() { document.body.offsetHeight; }
+onload = () => {
+  flush();
+  host.classList.add("active");
+  flush();
+  a.className = "q";
+  a.style.display = "inline";
+  b.append(a);
+  flush();
+  a.remove();
+  flush();
+  before.remove();
+  flush();
+};
+</script>
Loading diff…