Medium webkit UAF 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactProcessing maliciously crafted web content may lead to arbitrary code execution
DescriptionA use-after-free issue was addressed with improved memory management.
ComponentWebCore
Bug ClassUAF
Tracker322120
Fix commit1b03d891f40e (webkit) +36/-11
CISA KEVNot listed
CreditedEuntae Jang
Disclosed2026-10-26

Background

`@page` rules / `StyleRulePage`
A StyleRulePage represents a CSS @page { ... } rule. These rules live inside a stylesheet’s parsed representation and are indexed into a RuleSet.
`RuleSet::m_pageRules`
The collection on a RuleSet holding the @page rules contributed by a stylesheet; it is filled by RuleSet::addPageRule.
`PageRuleCollector`
During printing/pagination it gathers matching @page rules into matchedPageRules, sorts them by specificity (comparePageRules), and uses them to compute page style.
`beforeprint` reentrancy
beginPrinting() dispatches the beforeprint event 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-Ref ownership 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.

Key insight
Matched @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

  1. Install an `@page` rule Page (or a user style sheet) defines an @page rule so that RuleSet::addPageRule records a pointer to a StyleRulePage.
  2. Force style resolution Trigger layout/style so the resolver references the @page rule and the RuleSet is populated.
  3. Start printing Call window.print(), invoking beginPrinting(), which dispatches beforeprint before computing page size.
  4. Remove the style sheet in `beforeprint` The handler disables/removes the style sheet owning the @page rule (e.g. setUserStyleSheetEnabled(false)), destroying the RuleSet and its StyleRulePage objects.
  5. Dangling matched rules PageRuleCollector continues with raw pointers into the freed StyleRulePages (captured in matchedPageRules/m_pageRules).
  6. 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

This is a renderer-process (WebCore style/printing) use-after-free reachable from unprivileged JavaScript on a page that defines @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

FunctionChangeNotes
for
Source/WebCore/style/PageRuleCollector.cpp
modified

Files Changed

  • LayoutTests/printing/page-rule-user-style-sheet-removed-during-beforeprint-expected.txt
  • LayoutTests/printing/page-rule-user-style-sheet-removed-during-beforeprint.html
  • Source/WebCore/style/PageRuleCollector.cpp
  • Source/WebCore/style/PageRuleCollector.h
  • Source/WebCore/style/RuleSet.cpp
  • Source/WebCore/style/RuleSet.h

Audit Directions

  • Raw-pointer rule collections
    Audit other RuleSet/collector members that store StyleRule*-family raw pointers (media, container, counter-style, view-transition rules) for the same stylesheet-removal-during-event dangling scenario.
  • `beforeprint`/`afterprint` reentrancy
    Review 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 teardown
    Examine setUserStyleSheetEnabled/style-sheet removal paths for other structures that hold borrowed pointers into a RuleSet that can be destroyed synchronously.
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>
Loading diff…

Regression Test / PoC

shipped with the fix
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>
Loading diff…