CVE-2026-112651
Overview
Background
- `@page` rules / `StyleRulePage`
- A
StyleRulePagerepresents a CSS@page { ... }rule. These rules live inside a stylesheet’s parsed representation and are indexed into aRuleSet. - `RuleSet::m_pageRules`
- The collection on a
RuleSetholding the@pagerules contributed by a stylesheet; it is filled byRuleSet::addPageRule. - `PageRuleCollector`
- During printing/pagination it gathers matching
@pagerules intomatchedPageRules, sorts them by specificity (comparePageRules), and uses them to compute page style. - `beforeprint` reentrancy
beginPrinting()dispatches thebeforeprintevent before it reads page geometry, giving script a window to mutate/remove stylesheets mid-print.- WebKit lifetime model
- Lifetime is
Ref/RefPtr/WeakPtr; the fix here is precisely a raw-pointer-to-Refownership upgrade.
Root Cause Analysis
RuleSet::m_pageRules and the page-rule plumbing in PageRuleCollector stored raw StyleRulePage* pointers (Vector<StyleRulePage*>). RuleSet::addPageRule appended &rule (a bare address), and PageRuleCollector::matchPageRules/matchPageRulesForList collected those raw pointers into a Vector<StyleRulePage*> matchedPageRules. The StyleRulePage objects are owned by the stylesheet’s RuleSet, not by the collector. During printing, beginPrinting() dispatches beforeprint before computing page size; a handler can disable/remove the user (or author) style sheet that supplied the @page rule. Removing the style sheet destroys its RuleSet and the StyleRulePage objects it owned, so the raw pointers already captured in matchedPageRules (and in m_pageRules) become dangling. The collector then dereferences them — e.g. comparePageRules calling r1->selector().specificityForPage() during the sort, or later matching — reading freed memory: a use-after-free.
The patch upgrades the ownership model throughout to hold strong references: m_pageRules becomes Vector<Ref<StyleRulePage>>, pageRules() returns const Vector<Ref<StyleRulePage>>&, addPageRule appends the rule by reference (m_pageRules.append(rule) constructing a Ref), and matchedPageRules/matchPageRulesForList are retyped to Vector<Ref<StyleRulePage>>. comparePageRules is changed to take const StyleRulePage& (dereferencing a Ref rather than a raw pointer). Because matched rules are now held by Ref, each referenced StyleRulePage stays alive for as long as the collector holds it, even if the originating style sheet is removed during beforeprint. The accompanying layout test (page-rule-user-style-sheet-removed-during-beforeprint.html) reproduces exactly this: it installs an @page rule via a user style sheet and disables that sheet inside the beforeprint handler, expecting “PASS if no crash.” The advisory’s UAF/ACE label matches the diff.
@page rules were held as raw StyleRulePage* owned by the stylesheet’s RuleSet, so removing that style sheet during the beforeprint event (fired before page geometry is read) freed the rules while the PageRuleCollector still dereferenced the dangling pointers.Attack Path
- Install an `@page` rule
Page (or a user style sheet) defines an
@pagerule so thatRuleSet::addPageRulerecords a pointer to aStyleRulePage. - Force style resolution
Trigger layout/style so the resolver references the
@pagerule and theRuleSetis populated. - Start printing
Call
window.print(), invokingbeginPrinting(), which dispatchesbeforeprintbefore computing page size. - Remove the style sheet in `beforeprint`
The handler disables/removes the style sheet owning the
@pagerule (e.g.setUserStyleSheetEnabled(false)), destroying theRuleSetand itsStyleRulePageobjects. - Dangling matched rules
PageRuleCollectorcontinues with raw pointers into the freedStyleRulePages (captured inmatchedPageRules/m_pageRules). - UAF primitive
Sorting/matching dereferences the freed rules (
comparePageRules→selector().specificityForPage()), a use-after-free during printing usable toward info-leak/corruption (escalation inferred; the test asserts only the crash).
Impact Assessment
@page rules and drives printing (window.print()), removing the contributing style sheet from the beforeprint handler. Apple’s advisory states arbitrary code execution; the diff and test substantiate a genuine UAF of StyleRulePage objects, while reliable code execution beyond the crash is an escalation not proven by the diff (inferred). No elevated privilege is required beyond script execution and (for the user-style-sheet variant in the test) the printing path, which is page-triggerable.Changed Functions
| Function | Change | Notes |
|---|---|---|
forSource/WebCore/style/PageRuleCollector.cpp |
modified |
Files Changed
LayoutTests/printing/page-rule-user-style-sheet-removed-during-beforeprint-expected.txtLayoutTests/printing/page-rule-user-style-sheet-removed-during-beforeprint.htmlSource/WebCore/style/PageRuleCollector.cppSource/WebCore/style/PageRuleCollector.hSource/WebCore/style/RuleSet.cppSource/WebCore/style/RuleSet.h
Audit Directions
- Raw-pointer rule collectionsAudit other
RuleSet/collector members that storeStyleRule*-family raw pointers (media, container, counter-style, view-transition rules) for the same stylesheet-removal-during-event dangling scenario. - `beforeprint`/`afterprint` reentrancyReview all printing-path code that caches pointers across
beginPrinting()/event dispatch, since script can mutate stylesheets and the DOM in those handlers before geometry is computed. - User style sheet teardownExamine
setUserStyleSheetEnabled/style-sheet removal paths for other structures that hold borrowed pointers into aRuleSetthat can be destroyed synchronously.
Patch
diff --git a/Source/WebCore/style/PageRuleCollector.cpp b/Source/WebCore/style/PageRuleCollector.cpp
index 9a4067dd279d..8ce52e19ec08 100644
--- a/Source/WebCore/style/PageRuleCollector.cpp
+++ b/Source/WebCore/style/PageRuleCollector.cpp
@@ -38,9 +38,9 @@
namespace WebCore {
namespace Style {
-static inline bool LEGACY_NODELETE comparePageRules(const StyleRulePage* r1, const StyleRulePage* r2)
+static inline bool LEGACY_NODELETE comparePageRules(const StyleRulePage& r1, const StyleRulePage& r2)
{
- return r1->selector().specificityForPage() < r2->selector().specificityForPage();
+ return r1.selector().specificityForPage() < r2.selector().specificityForPage();
}
bool PageRuleCollector::isLeftPage(int pageIndex) const
@@ -79,7 +79,7 @@ void PageRuleCollector::matchPageRules(RuleSet* rules, bool isLeftPage, bool isF
if (!rules)
return;
- Vector<StyleRulePage*> matchedPageRules;
+ Vector<Ref<StyleRulePage>> matchedPageRules;
matchPageRulesForList(matchedPageRules, rules->pageRules(), isLeftPage, isFirstPage, pageName);
if (matchedPageRules.isEmpty())
return;
@@ -111,11 +111,9 @@ static bool LEGACY_NODELETE checkPageSelectorComponents(const CSSSelector& selec
return true;
}
-void PageRuleCollector::matchPageRulesForList(Vector<StyleRulePage*>& matchedRules, const Vector<StyleRulePage*>& rules, bool isLeftPage, bool isFirstPage, const WTF::String& pageName)
+void PageRuleCollector::matchPageRulesForList(Vector<Ref<StyleRulePage>>& matchedRules, const Vector<Ref<StyleRulePage>>& rules, bool isLeftPage, bool isFirstPage, const WTF::String& pageName)
{
- for (unsigned i = 0; i < rules.size(); ++i) {
- RefPtr rule = rules[i];
-
+ for (auto& rule : rules) {
if (!checkPageSelectorComponents(rule->selector(), isLeftPage, isFirstPage, pageName))
continue;
diff --git a/Source/WebCore/style/PageRuleCollector.h b/Source/WebCore/style/PageRuleCollector.h
index e089000fc69e..94a0071de74d 100644
--- a/Source/WebCore/style/PageRuleCollector.h
+++ b/Source/WebCore/style/PageRuleCollector.h
@@ -49,7 +49,7 @@ private:
WTF::String LEGACY_NODELETE pageName(int pageIndex) const;
void matchPageRules(RuleSet* rules, bool isLeftPage, bool isFirstPage, const WTF::String& pageName);
- void matchPageRulesForList(Vector<StyleRulePage*>& matchedRules, const Vector<StyleRulePage*>& rules, bool isLeftPage, bool isFirstPage, const WTF::String& pageName);
+ void matchPageRulesForList(Vector<Ref<StyleRulePage>>& matchedRules, const Vector<Ref<StyleRulePage>>& rules, bool isLeftPage, bool isFirstPage, const WTF::String& pageName);
ScopeRuleSets& m_ruleSets;
WritingMode m_rootWritingMode;
diff --git a/Source/WebCore/style/RuleSet.cpp b/Source/WebCore/style/RuleSet.cpp
index a8857a3d7fe3..97d5e264f926 100644
--- a/Source/WebCore/style/RuleSet.cpp
+++ b/Source/WebCore/style/RuleSet.cpp
@@ -533,7 +533,7 @@ void RuleSet::addRuleToBucket(RuleData& ruleData)
void RuleSet::addPageRule(StyleRulePage& rule)
{
- m_pageRules.append(&rule);
+ m_pageRules.append(rule);
}
void RuleSet::setViewTransitionRule(StyleRuleViewTransition& rule)
diff --git a/Source/WebCore/style/RuleSet.h b/Source/WebCore/style/RuleSet.h
index 0d957926bf3e..b5ec1abd9ce1 100644
--- a/Source/WebCore/style/RuleSet.h
+++ b/Source/WebCore/style/RuleSet.h
@@ -132,7 +132,7 @@ public:
PseudoElementBoxGeneration universalHTMLPseudoElementBoxGeneration() const { return m_universalHTMLPseudoElementBoxGeneration; }
PseudoElementBoxGeneration universalPseudoElementBoxGeneration() const { return m_universalPseudoElementBoxGeneration; }
- const Vector<StyleRulePage*>& pageRules() const LIFETIME_BOUND { return m_pageRules; }
+ const Vector<Ref<StyleRulePage>>& pageRules() const LIFETIME_BOUND { return m_pageRules; }
unsigned ruleCount() const { return m_ruleCount; }
@@ -250,7 +250,7 @@ private:
EnumSet<PseudoElementType> m_universalPseudoElementTypes;
PseudoElementBoxGeneration m_universalHTMLPseudoElementBoxGeneration { PseudoElementBoxGeneration::NotForBeforeOrAfter };
PseudoElementBoxGeneration m_universalPseudoElementBoxGeneration { PseudoElementBoxGeneration::NotForBeforeOrAfter };
- Vector<StyleRulePage*> m_pageRules;
+ Vector<Ref<StyleRulePage>> m_pageRules;
RefPtr<StyleRuleViewTransition> m_viewTransitionRule;
RuleFeatureSet m_features;
Vector<DynamicMediaQueryRules> m_dynamicMediaQueryRules;
diff --git a/LayoutTests/printing/page-rule-user-style-sheet-removed-during-beforeprint-expected.txt b/LayoutTests/printing/page-rule-user-style-sheet-removed-during-beforeprint-expected.txt
new file mode 100644
index 000000000000..ddad5eaf7f1e
--- /dev/null
+++ b/LayoutTests/printing/page-rule-user-style-sheet-removed-during-beforeprint-expected.txt
@@ -0,0 +1 @@
+PASS if no crash
diff --git a/LayoutTests/printing/page-rule-user-style-sheet-removed-during-beforeprint.html b/LayoutTests/printing/page-rule-user-style-sheet-removed-during-beforeprint.html
new file mode 100644
index 000000000000..5b315cc37007
--- /dev/null
+++ b/LayoutTests/printing/page-rule-user-style-sheet-removed-during-beforeprint.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script>
+if (window.testRunner)
+ testRunner.dumpAsText();
+
+// @page rule via a data: URL so the user style sheet applies synchronously.
+testRunner.setUserStyleSheetLocation("data:text/css;charset=utf-8;base64," + btoa("@page { margin: 7px; }"));
+testRunner.setUserStyleSheetEnabled(true);
+
+// Force style resolution so the resolver references the @page rule.
+document.body.offsetTop;
+
+// beginPrinting() dispatches "beforeprint" before it computes the page size, so
+// disabling the user style sheet here drops the @page rule mid-print.
+window.addEventListener("beforeprint", () => {
+ testRunner.setUserStyleSheetEnabled(false);
+});
+
+window.print();
+
+document.body.textContent = "PASS if no crash";
+</script>
+</body>
+</html>
Regression Test / PoC
diff --git a/LayoutTests/printing/page-rule-user-style-sheet-removed-during-beforeprint-expected.txt b/LayoutTests/printing/page-rule-user-style-sheet-removed-during-beforeprint-expected.txt
new file mode 100644
index 000000000000..ddad5eaf7f1e
--- /dev/null
+++ b/LayoutTests/printing/page-rule-user-style-sheet-removed-during-beforeprint-expected.txt
@@ -0,0 +1 @@
+PASS if no crash
diff --git a/LayoutTests/printing/page-rule-user-style-sheet-removed-during-beforeprint.html b/LayoutTests/printing/page-rule-user-style-sheet-removed-during-beforeprint.html
new file mode 100644
index 000000000000..5b315cc37007
--- /dev/null
+++ b/LayoutTests/printing/page-rule-user-style-sheet-removed-during-beforeprint.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script>
+if (window.testRunner)
+ testRunner.dumpAsText();
+
+// @page rule via a data: URL so the user style sheet applies synchronously.
+testRunner.setUserStyleSheetLocation("data:text/css;charset=utf-8;base64," + btoa("@page { margin: 7px; }"));
+testRunner.setUserStyleSheetEnabled(true);
+
+// Force style resolution so the resolver references the @page rule.
+document.body.offsetTop;
+
+// beginPrinting() dispatches "beforeprint" before it computes the page size, so
+// disabling the user style sheet here drops the @page rule mid-print.
+window.addEventListener("beforeprint", () => {
+ testRunner.setUserStyleSheetEnabled(false);
+});
+
+window.print();
+
+document.body.textContent = "PASS if no crash";
+</script>
+</body>
+</html>