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
Tracker2081977
Fix commita6e4e54eb517 (firefox) +102/-21
CISA KEVNot listed
CreditedOpenAI Codex Security (kylebot)
Disclosed2026-10-27

Changed Functions

FunctionChangeNotes
if
layout/generic/nsIFrame.h
modified
if
layout/style/RestyleManager.cpp
modified

Files Changed

  • layout/base/nsCSSFrameConstructor.cpp
  • layout/generic/nsIFrame.cpp
  • layout/generic/nsIFrame.h
  • layout/style/RestyleManager.cpp
  • testing/web-platform/tests/css/printing/fixed-container-query-removal-crash-print.html
  • testing/web-platform/tests/css/printing/table-row-group-container-query-crash-print.html
diff --git a/layout/base/nsCSSFrameConstructor.cpp b/layout/base/nsCSSFrameConstructor.cpp
index 34c2fb567f9..0502afa802b 100644
--- a/layout/base/nsCSSFrameConstructor.cpp
+++ b/layout/base/nsCSSFrameConstructor.cpp
@@ -7620,6 +7620,18 @@ static nsIFrame* FindPreviousNonWhitespaceSibling(nsIFrame* aFrame) {
   return f;
 }
 
+// Returns whether aFrame is inside a table that is fragmented across pages.
+// Such tables can't deal with removals (see nsTableCellMap::Synchronize and
+// CreateContinuingTableFrame).
+static bool IsInFragmentedTable(nsIFrame* aFrame) {
+  for (nsIFrame* f = aFrame->GetParent(); f; f = f->GetParent()) {
+    if (f->IsTableFrame() && (f->GetPrevInFlow() || f->GetNextInFlow())) {
+      return true;
+    }
+  }
+  return false;
+}
+
 static nsIFrame* CheckRubyContainers(nsIFrame* aFrame, nsIFrame* aParent) {
   auto* ancestor = aParent;
   auto* ancestorChild = aFrame;
@@ -7710,6 +7722,19 @@ bool nsCSSFrameConstructor::MaybeRecreateContainerForFrameRemoval(
     }
   }
 
+  if (aFrame->PresContext()->IsPaginated() &&
+      aFrame->GetContent() != mDocument->GetRootElement() &&
+      (IsInFragmentedTable(inFlowFrame) ||
+       aFrame->IsInReplicatedFixedPosTree())) {
+    // Fragmented tables and replicated fixed-pos frames on subsequent pages
+    // can't deal with removals. Rebuild the whole document instead, which also
+    // makes sure that the new frames get reflowed, since paginated documents
+    // don't do incremental reflow (see nsPageSequenceFrame::Reflow).
+    TRACE("Paginated fragmented content");
+    RecreateFramesForContent(mDocument->GetRootElement(), InsertionKind::Async);
+    return true;
+  }
+
   if (IsFramePartOfIBSplit(aFrame)) {
     // The removal functions can't handle removal of an {ib} split directly; we
     // need to rebuild the containing block.
diff --git a/layout/generic/nsIFrame.cpp b/layout/generic/nsIFrame.cpp
index e4621044be1..b480d5e4a05 100644
--- a/layout/generic/nsIFrame.cpp
+++ b/layout/generic/nsIFrame.cpp
@@ -9501,6 +9501,23 @@ void nsIFrame::DumpFrameTreeLimitedInCSSPixels() const {
 
 #endif
 
+// FIXME(emilio, bug 1633685): We should ideally figure out how to properly
+// restyle replicated fixed pos frames... We seem to assume everywhere that they
+// can't get restyled at the moment...
+bool nsIFrame::IsInReplicatedFixedPosTree() const {
+  if (!PresContext()->IsPaginated()) {
+    return false;
+  }
+  for (const nsIFrame* f = this; f; f = f->GetParent()) {
+    if (f->StyleDisplay()->mPosition == StylePositionProperty::Fixed &&
+        nsLayoutUtils::IsReallyFixedPos(f)) {
+      const nsIFrame* pageContent = f->GetParent();
+      return pageContent->GetPrevInFlow() || pageContent->GetNextInFlow();
+    }
+  }
+  return false;
+}
+
 bool nsIFrame::IsVisibleForPainting() const {
   return StyleVisibility()->IsVisible();
 }
diff --git a/layout/generic/nsIFrame.h b/layout/generic/nsIFrame.h
index 371f684248b..0a6f9994651 100644
--- a/layout/generic/nsIFrame.h
+++ b/layout/generic/nsIFrame.h
@@ -2687,6 +2687,13 @@ class nsIFrame : public nsQueryFrame {
    */
   bool IsPrimaryFrame() const { return mIsPrimaryFrame; }
 
+  /**
+   * Return true if this frame is in a fixed-pos subtree that is replicated
+   * across pages (see nsCSSFrameConstructor::ReplicateFixedFrames), either as
+   * the original or as one of the copies.
+   */
+  bool IsInReplicatedFixedPosTree() const;
+
   void SetIsPrimaryFrame(bool aIsPrimary) {
     if (mIsPrimaryFrame == aIsPrimary) {
       return;
diff --git a/layout/style/RestyleManager.cpp b/layout/style/RestyleManager.cpp
index 6b4dfd5e37e..f6ce68c9ad3 100644
--- a/layout/style/RestyleManager.cpp
+++ b/layout/style/RestyleManager.cpp
@@ -2275,25 +2275,6 @@ static const nsIFrame* ExpectedOwnerForChild(const nsIFrame* aFrame) {
   return parent;
 }
 
-// FIXME(emilio, bug 1633685): We should ideally figure out how to properly
-// restyle replicated fixed pos frames... We seem to assume everywhere that they
-// can't get restyled at the moment...
-static bool IsInReplicatedFixedPosTree(const nsIFrame* aFrame) {
-  if (!aFrame->PresContext()->IsPaginated()) {
-    return false;
-  }
-
-  for (; aFrame; aFrame = aFrame->GetParent()) {
-    if (aFrame->StyleDisplay()->mPosition == StylePositionProperty::Fixed &&
-        !aFrame->FirstContinuation()->IsPrimaryFrame() &&
-        nsLayoutUtils::IsReallyFixedPos(aFrame)) {
-      return true;
-    }
-  }
-
-  return true;
-}
-
 void ServoRestyleState::AssertOwner(const ServoRestyleState& aParent) const {
   MOZ_ASSERT(mOwner);
   MOZ_ASSERT(CanUseHandledHintsFromAncestors(mOwner));
@@ -2303,7 +2284,7 @@ void ServoRestyleState::AssertOwner(const ServoRestyleState& aParent) const {
   // chains of ServoRestyleStates in some cases where it's just not worth it.
   if (aParent.mOwner) {
     const nsIFrame* owner = ExpectedOwnerForChild(mOwner);
-    if (owner != aParent.mOwner && !IsInReplicatedFixedPosTree(mOwner)) {
+    if (owner != aParent.mOwner && !mOwner->IsInReplicatedFixedPosTree()) {
       MOZ_ASSERT(IsAnonBox(owner),
                  "Should only have expected owner weirdness when anon boxes "
                  "are involved");
@@ -2327,7 +2308,7 @@ nsChangeHint ServoRestyleState::ChangesHandledFor(
   }
 
   MOZ_ASSERT(mOwner == ExpectedOwnerForChild(aFrame) ||
-                 IsInReplicatedFixedPosTree(aFrame),
+                 aFrame->IsInReplicatedFixedPosTree(),
              "Missed some frame in the hierarchy?");
   return mChangesHandled;
 }
diff --git a/testing/web-platform/tests/css/printing/fixed-container-query-removal-crash-print.html b/testing/web-platform/tests/css/printing/fixed-container-query-removal-crash-print.html
new file mode 100644
index 00000000000..0a1ffe434a1
--- /dev/null
+++ b/testing/web-platform/tests/css/printing/fixed-container-query-removal-crash-print.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>Hiding content in a replicated fixed-position element with a container query</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=2081977">
+<!-- Really a crashtest but since we can't really have print crashtests, we assert that we print something -->
+<link rel="mismatch" href="/css/reference/blank.html">
+<style>
+@page { size: 360px 420px; margin: 15px; }
+body { font-size: 12px; }
+#fixed { position: fixed; top: 0; right: 0; width: 200px; container-type: inline-size; }
+#target::before { content: "before"; }
+@container (min-width: 100px) {
+  #target { display: none; }
+}
+.spacer { height: 2000px; }
+</style>
+<div id="fixed"><div id="target">fixed</div></div>
+<div class="spacer">hello</div>
diff --git a/testing/web-platform/tests/css/printing/table-row-group-container-query-crash-print.html b/testing/web-platform/tests/css/printing/table-row-group-container-query-crash-print.html
new file mode 100644
index 00000000000..cc0c36e1e89
--- /dev/null
+++ b/testing/web-platform/tests/css/printing/table-row-group-container-query-crash-print.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<meta charset="utf-8">
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=2081977">
+<!-- Really a crashtest but since we can't really have print crashtests, we assert that we print something -->
+<link rel="mismatch" href="/css/reference/blank.html">
+<style>
+.c { container-type: inline-size; }
+table { border-collapse: collapse; width: 100%; }
+td { border: 1px solid black; height: 100px; }
+@container (min-width: 1px) {
+  thead, tfoot { display: none; }
+}
+</style>
+<div class="c">
+<table>
+<thead><tr><td>h</td></tr></thead>
+<tbody>
+<tr><td>a</td></tr>
+<tr><td>a</td></tr>
+<tr><td>a</td></tr>
+<tr><td>a</td></tr>
+<tr><td>a</td></tr>
+<tr><td>a</td></tr>
+<tr><td>a</td></tr>
+<tr><td>a</td></tr>
+</tbody>
+<tfoot><tr><td>f</td></tr></tfoot>
+</table>
+</div>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/testing/web-platform/tests/css/printing/fixed-container-query-removal-crash-print.html b/testing/web-platform/tests/css/printing/fixed-container-query-removal-crash-print.html
new file mode 100644
index 00000000000..0a1ffe434a1
--- /dev/null
+++ b/testing/web-platform/tests/css/printing/fixed-container-query-removal-crash-print.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>Hiding content in a replicated fixed-position element with a container query</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=2081977">
+<!-- Really a crashtest but since we can't really have print crashtests, we assert that we print something -->
+<link rel="mismatch" href="/css/reference/blank.html">
+<style>
+@page { size: 360px 420px; margin: 15px; }
+body { font-size: 12px; }
+#fixed { position: fixed; top: 0; right: 0; width: 200px; container-type: inline-size; }
+#target::before { content: "before"; }
+@container (min-width: 100px) {
+  #target { display: none; }
+}
+.spacer { height: 2000px; }
+</style>
+<div id="fixed"><div id="target">fixed</div></div>
+<div class="spacer">hello</div>
diff --git a/testing/web-platform/tests/css/printing/table-row-group-container-query-crash-print.html b/testing/web-platform/tests/css/printing/table-row-group-container-query-crash-print.html
new file mode 100644
index 00000000000..cc0c36e1e89
--- /dev/null
+++ b/testing/web-platform/tests/css/printing/table-row-group-container-query-crash-print.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<meta charset="utf-8">
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=2081977">
+<!-- Really a crashtest but since we can't really have print crashtests, we assert that we print something -->
+<link rel="mismatch" href="/css/reference/blank.html">
+<style>
+.c { container-type: inline-size; }
+table { border-collapse: collapse; width: 100%; }
+td { border: 1px solid black; height: 100px; }
+@container (min-width: 1px) {
+  thead, tfoot { display: none; }
+}
+</style>
+<div class="c">
+<table>
+<thead><tr><td>h</td></tr></thead>
+<tbody>
+<tr><td>a</td></tr>
+<tr><td>a</td></tr>
+<tr><td>a</td></tr>
+<tr><td>a</td></tr>
+<tr><td>a</td></tr>
+<tr><td>a</td></tr>
+<tr><td>a</td></tr>
+<tr><td>a</td></tr>
+</tbody>
+<tfoot><tr><td>f</td></tr></tfoot>
+</table>
+</div>
Loading diff…