High watch ✔︎ verified CVSS 6.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
6.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in CSS
DescriptionUse after free in CSS
ComponentCSS
Bug ClassUAF
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar — revised by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed (tier revised from patch-now)
Reviewer notes: Mechanism confirmed, but diff shows a read-only UAF (ToAsciiLower on a dangling reference); claim's 'corruption' primitive overstated, no write/corruption demonstrated. Advisory CVSS 6.3.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker493952652
Fix commitd8b01057f740 (chromium/src) +32/-3
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:L
CISA KEVNot listed
CreditedAnonymous
Reported2026-03-18 (per vendor advisory)
Fix landed2026-03-20 (public git)
Disclosed2026-03-31
Patch gap11 days — fix public in source before the advisory

Background

Lazy style attributes
Blink defers materializing an element’s inline style attribute into its real attribute storage until something reads it, synchronizing (and thus mutating) the attribute collection on demand.
`AtomicString`
an interned, reference-counted string; a const AtomicString& returned by getAttribute points into the element’s live attribute storage and is only valid while that storage is unchanged.
`InputRules` bucketing
RuleSet indexes selectors that match <input> elements by the element’s type value, so rule matching looks up the lowercased type string to find candidate rules.
`CollectMatchingRulesForList`
the per-rule-set matcher that evaluates candidate selectors and, as a side effect, can read [style] and trigger lazy-attribute synchronization.

Root Cause Analysis

The vulnerable code in element_rule_collector.cc bound input_type as a const AtomicString& reference into the element’s attribute storage via element.getAttribute(html_names::kTypeAttr), then entered a loop over match_request.RuleSetsWithInputRules(), calling input_type.ToAsciiLower() on each iteration. Inside that loop CollectMatchingRulesForList<stop_at_first_match>() can evaluate a [style] selector, which synchronizes the element’s lazy style attribute and appends a new attribute — reallocating or mutating the attribute storage and leaving the previously obtained input_type reference dangling. On a subsequent iteration, dereferencing that dangling reference to call ToAsciiLower() is a use-after-free read.

The fix snapshots the value once, before the loop, into an independent local AtomicString input_type_lower = input_type.ToAsciiLower(); and passes input_type_lower into InputRules(), so no reference into the attribute storage survives across the side-effecting calls. The invariant restored is that a value read from attribute storage must be copied out before any operation that may mutate that storage.

Key insight
A reference into mutable attribute storage was held live across a call that can synchronize lazy attributes and reallocate that storage; copying the lowercased type string into a stable local before the loop removes the dangling-reference window.

Attack Path

  1. Craft the markup The page contains an <input type="text"> plus two stylesheets whose selectors (e.g. input[style][type="text"]) bucket on input type and also reference [style], so matching will touch the lazy style attribute.
  2. Force lazy style Script sets target.style.color = 'red', establishing inline style that has not yet been synchronized into the element’s real attribute storage.
  3. Begin rule collection Style resolution reads input_type as a reference into attribute storage, then iterates the input-rule rule sets.
  4. Trigger synchronization While matching the first rule set, CollectMatchingRulesForList() evaluates [style], synchronizing the lazy attribute and reallocating the attribute storage, which invalidates the still-held input_type reference.
  5. Use-after-free read A later loop iteration calls input_type.ToAsciiLower() on the freed/moved storage, reading dangling memory; further consequences beyond this read are not shown by the diff (inferred).

Impact Assessment

An attacker gains an out-of-bounds/use-after-free read in the renderer process reachable from ordinary web content in a stock build — no special flag, extension, or policy is required, only an <input> element, two matching stylesheets, and a script touch of style. The vendor advisory rates this high. Any escalation from the dangling read to information disclosure or memory corruption is not demonstrated by the patch and is (inferred).

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

Audit Directions

  • References across side-effecting calls
    Flag any const AtomicString& or raw reference/pointer obtained from getAttribute/attribute storage that is dereferenced after a call that can synchronize lazy attributes or mutate the attribute collection.
  • Loops over rule sets
    Review other CollectMatchingRulesFor* call sites that read an element value once and reuse it per iteration; snapshot such values before iterating.
  • Lazy synchronization triggers
    Audit code paths where selector matching evaluates [style] or other presentation attributes, since these can mutate node state mid-traversal and invalidate earlier reads.
From d8b01057f740d3bb0ec880b34372da63147c2521 Mon Sep 17 00:00:00 2001
From: Steinar H. Gunderson <[email protected]>
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 <[email protected]>
Commit-Queue: Steinar H Gunderson <[email protected]>
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 [email protected]

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