Firefox · Layout
CVE-2026-117882
Logic Error in Layout
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Files Changed
layout/base/ContainStyleScopeManager.cpplayout/base/nsCounterManager.cpplayout/base/nsCounterManager.htesting/web-platform/tests/css/css-contain/crashtests/contain-style-first-letter-colgroup.htmltesting/web-platform/tests/css/css-contain/crashtests/contain-style-first-letter.html
Patch
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…
References
On This Page