Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in CSS
DescriptionUse after free in CSS
ComponentCSS
Bug ClassUAF
Tracker493952652
Fix commitd8b01057f740 (chromium/src) +32/-3
CISA KEVNot listed
CreditedAnonymous
Disclosed2026-03-31

Files Changed

  • third_party/blink/renderer/core/css/element_rule_collector.cc
  • third_party/blink/web_tests/external/wpt/css/css-values/crashtests/chrome-bug-493952652.html
From d8b01057f740d3bb0ec880b34372da63147c2521 Mon Sep 17 00:00:00 2001
From: Steinar H. Gunderson <sesse@chromium.org>
Date: Fri, 20 Mar 2026 07:22:02 -0700
Subject: [PATCH] Fix another use-after-free with lazy style attributes.

This is a similar problem as regular attribute checks, just for
the special case of input type="" (which is a similar but separate
path).

Style perftest and Speedometer3 are neutral.

Fixed: 493952652
Change-Id: I264503545c345325e6d21afa0726f524bb9394b8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7686835
Reviewed-by: Anders Hartvoll Ruud <andruud@chromium.org>
Commit-Queue: Steinar H Gunderson <sesse@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1602570}
---

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 901a258..64198a33 100644
--- a/third_party/blink/renderer/core/css/element_rule_collector.cc
+++ b/third_party/blink/renderer/core/css/element_rule_collector.cc
@@ -957,11 +957,14 @@
     if (const AtomicString& input_type =
             element.getAttribute(html_names::kTypeAttr);
         !input_type.IsNull()) {
+      // Do not use input_type in the loop; the reference
+      // may be dangling if CollectMatchingRulesForList()
+      // adds lazy attributes.
+      AtomicString input_type_lower = input_type.ToAsciiLower();
       for (const auto bundle : match_request.RuleSetsWithInputRules()) {
         if (CollectMatchingRulesForList<stop_at_first_match>(
-                bundle.rule_set->InputRules(input_type.ToAsciiLower()),
-                match_request, bundle.rule_set, bundle.style_sheet_index,
-                checker, context) &&
+                bundle.rule_set->InputRules(input_type_lower), match_request,
+                bundle.rule_set, bundle.style_sheet_index, checker, context) &&
             stop_at_first_match) {
           return true;
         }
diff --git a/third_party/blink/web_tests/external/wpt/css/css-values/crashtests/chrome-bug-493952652.html b/third_party/blink/web_tests/external/wpt/css/css-values/crashtests/chrome-bug-493952652.html
new file mode 100644
index 0000000..a9c96ec5
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-values/crashtests/chrome-bug-493952652.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<head>
+  <link rel="help" href="https://crbug.com/493952652">
+  <style>
+    /*
+       Evaluation of [style] will add a new attribute to the element,
+       as lazy style gets synchronized. However, the bucketing will be
+       on input type, so we do not synchronize the attributes _before_
+       iteration.
+     */
+    input[style][type="text"] {}
+  </style>
+  <style>
+    /*
+       For the second stylesheet, when checking the type="" value string,
+       we must not use a reference into the old attributes.
+     */
+    input[style][type="text"] {}
+  </style>
+</head>
+<body>
+  <input id="target" type="text"></input>
+  <script>
+    document.getElementById('target').style.color = 'red';
+  </script>
+</body>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/css/css-values/crashtests/chrome-bug-493952652.html b/third_party/blink/web_tests/external/wpt/css/css-values/crashtests/chrome-bug-493952652.html
new file mode 100644
index 0000000..a9c96ec5
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-values/crashtests/chrome-bug-493952652.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<head>
+  <link rel="help" href="https://crbug.com/493952652">
+  <style>
+    /*
+       Evaluation of [style] will add a new attribute to the element,
+       as lazy style gets synchronized. However, the bucketing will be
+       on input type, so we do not synchronize the attributes _before_
+       iteration.
+     */
+    input[style][type="text"] {}
+  </style>
+  <style>
+    /*
+       For the second stylesheet, when checking the type="" value string,
+       we must not use a reference into the old attributes.
+     */
+    input[style][type="text"] {}
+  </style>
+</head>
+<body>
+  <input id="target" type="text"></input>
+  <script>
+    document.getElementById('target').style.color = 'red';
+  </script>
+</body>
Loading diff…

Original Bug Report

reported by nn...@gmail.com

heap-use-after-free READ in ElementRuleCollector InputRules path, page load, MiraclePtr unprotected

Steps to reproduce the problem

  1. Save the attached poc.html to disk
  2. Open poc.html in Chrome (no flags required, works with file:// protocol)
  3. Renderer crashes on page load ? no user interaction required

Tested on:

  • Chrome 146.0.7680.x stable (Linux x64) ? ASAN build crashes
  • Chrome 147.0.7722.0 ? ASAN build crashes
  • Current main/ToT vulnerable code unchanged

The PoC requires two <style> elements with input[style][type=“text”] selectors
(creating two RuleSet bundles) and JavaScript that sets an inline style then
forces style resolution via getComputedStyle().

Problem Description

heap-use-after-free READ of size 8 in blink::AtomicString::LowerASCII(), called
from ElementRuleCollector::CollectMatchingRulesInternal() on the InputRules path.

Root cause: element_rule_collector.cc (~line 952) takes a const AtomicString&
reference into Vector<Attribute> heap storage. During CSS rule matching, the
[style] selector triggers SynchronizeAttributeHinted(kStyleAttr), which appends
the lazy style attribute via AppendAttributeInternal(). This grows the vector,
calling ReallocateBuffer() which frees the old buffer. The reference is now
dangling. The next bundle iteration calls input_type.LowerASCII() ? reading
StringImpl* from offset 24 of the freed 32-byte region.

MiraclePtr Status: NOT PROTECTED. No raw_ptr<T> wraps this allocation.

The freed buffer is in PartitionAlloc’s 32-byte bucket. The dangling read
dereferences a StringImpl* at offset 24. If the slot is reclaimed via heap
spray, the attacker controls what LowerASCII() reads, influencing which CSS
InputRules are matched. Additionally, LowerASCII(AtomicString source) receives
source by value ? the copy constructor calls AddRef() on the attacker-controlled
StringImpl*, producing a 4-byte write (refcount increment) at an
attacker-influenced address.

Fix: change const AtomicString& to const AtomicString (value copy) at line ~952.

Additional Comments

Attachments:

  • poc.html: minimized 10-line PoC, crashes on page load, no flags
  • asan_trace.txt: full symbolized ASAN trace from Chrome 146 stable
  • controlled_uaf_demo.cc: standalone C++ demonstrating controlled read/write
  • README.md: full root cause analysis with memory layout and exploitability

Summary

heap-use-after-free READ in ElementRuleCollector InputRules path, page load, MiraclePtr unprotected

Custom Questions

Type of crash:

Renderer (tab) crash

Crash state:

ERROR: AddressSanitizer: heap-use-after-free on address 0x7b795a082658
READ of size 8 at 0x7b795a082658 thread T0 (chrome)

Crash stack:
#0 blink::AtomicString::LowerASCII() const
#1 blink::ElementRuleCollector::CollectMatchingRulesInternal<false>()
#2 blink::ScopedStyleResolver::CollectMatchingElementScopeRules()
#3 blink::StyleResolver::MatchAuthorRules()
#4 blink::StyleResolver::MatchAllRules()
#5 blink::StyleResolver::ApplyBaseStyleNoCache()
#6 blink::StyleResolver::ResolveStyle()
#7 blink::Element::OriginalStyleForLayoutObject()

Freed by:
#0 free
#1 blink::Vector<blink::Attribute, 4u>::ReallocateBuffer()
#2 blink::Vector<blink::Attribute, 4u>::ExpandCapacity()
#3 blink::Vector<blink::Attribute, 4u>::AppendSlowCaseblink::Attribute()
#4 blink::MutableAttributeCollection::Append()
#5 blink::Element::AppendAttributeInternal()
#6 blink::Element::SetSynchronizedLazyAttribute()
#7 blink::Element::SynchronizeStyleAttributeInternal() const #8 blink::Element::SynchronizeAttributeHinted() const
#9 blink::ElementRuleCollector::CollectMatchingRulesForListInternal<false, false>()

0x7b795a082658 is located 24 bytes inside of 32-byte region [0x7b795a082640,0x7b795a082660)

MiraclePtr Status: NOT PROTECTED
No raw_ptr<T> access to this region was detected prior to this crash.
This crash is still exploitable with MiraclePtr.

Full symbolized ASAN trace attached as asan_trace.txt.

Reporter credit:

Anonymous

Additional Data

Category: Security
Chrome Channel: Not sure
Regression: N/A \

View on issue tracker