Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Blink
DescriptionUse after free in Blink
ComponentBlink
Bug ClassUAF
Tracker487117772
Fix commit45c5a70d984d (chromium/src) +30/-30
CISA KEVNot listed
CreditedSyn4pse
Disclosed2026-03-18

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/css/cssom/css_unparsed_value.cc
modified

Files Changed

  • third_party/blink/renderer/core/css/cssom/css_unparsed_value.cc
  • third_party/blink/renderer/core/css/cssom/css_unparsed_value.h
From 45c5a70d984d630370e9ee15265f88381251a55a Mon Sep 17 00:00:00 2001
From: Anders Hartvoll Ruud <[email protected]>
Date: Wed, 25 Feb 2026 03:24:19 -0800
Subject: [PATCH] Describe a vector of segments as "segments", not "tokens"

The specification uses the term "tokens" to refer to a sequence
of V8CSSUnparsedSegment objects, and CSSUnparsedValue has adopted
this terminology. While it is usually a good idea for Blink
to mirror the language used in specifications, "tokens" is very
confusing here, since it always means CSSParserTokens in every other
place in the style code.

Bug: 487117772
Change-Id: I2dc132c4e618e398e1f8bdabc03a8d2ab6c118e7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7606599
Commit-Queue: Anders Hartvoll Ruud <[email protected]>
Reviewed-by: Steinar H Gunderson <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1590040}
---

diff --git a/third_party/blink/renderer/core/css/cssom/css_unparsed_value.cc b/third_party/blink/renderer/core/css/cssom/css_unparsed_value.cc
index 486e9d1..567d4fa 100644
--- a/third_party/blink/renderer/core/css/cssom/css_unparsed_value.cc
+++ b/third_party/blink/renderer/core/css/cssom/css_unparsed_value.cc
@@ -28,12 +28,12 @@
 
 V8CSSUnparsedSegment* VariableReferenceValue(
     const StringView& variable_name,
-    const HeapVector<Member<V8CSSUnparsedSegment>>& tokens) {
+    const HeapVector<Member<V8CSSUnparsedSegment>>& segments) {
   CSSUnparsedValue* unparsed_value;
-  if (tokens.size() == 0) {
+  if (segments.size() == 0) {
     unparsed_value = nullptr;
   } else {
-    unparsed_value = CSSUnparsedValue::Create(tokens);
+    unparsed_value = CSSUnparsedValue::Create(segments);
   }
 
   CSSStyleVariableReferenceValue* variable_reference =
@@ -50,13 +50,13 @@
 HeapVector<Member<V8CSSUnparsedSegment>> ParserTokenStreamToTokens(
     CSSParserTokenStream& stream) {
   int nesting_level = 0;
-  HeapVector<Member<V8CSSUnparsedSegment>> tokens;
+  HeapVector<Member<V8CSSUnparsedSegment>> segments;
   StringBuilder builder;
   while (stream.Peek().GetType() != kEOFToken) {
     if (stream.Peek().FunctionId() == CSSValueID::kVar ||
         stream.Peek().FunctionId() == CSSValueID::kEnv) {
       if (!builder.empty()) {
-        tokens.push_back(MakeGarbageCollected<V8CSSUnparsedSegment>(
+        segments.push_back(MakeGarbageCollected<V8CSSUnparsedSegment>(
             builder.ReleaseString()));
       }
 
@@ -71,7 +71,7 @@
       if (!ref) {
         break;
       }
-      tokens.push_back(ref);
+      segments.push_back(ref);
     } else {
       if (stream.Peek().GetBlockType() == CSSParserToken::kBlockStart) {
         ++nesting_level;
@@ -86,10 +86,10 @@
     }
   }
   if (!builder.empty()) {
-    tokens.push_back(
+    segments.push_back(
         MakeGarbageCollected<V8CSSUnparsedSegment>(builder.ReleaseString()));
   }
-  return tokens;
+  return segments;
 }
 
 }  // namespace
@@ -109,8 +109,8 @@
 V8CSSUnparsedSegment* CSSUnparsedValue::AnonymousIndexedGetter(
     uint32_t index,
     ExceptionState& exception_state) const {
-  if (index < tokens_.size()) {
-    return tokens_[index].Get();
+  if (index < segments_.size()) {
+    return segments_[index].Get();
   }
   return nullptr;
 }
@@ -119,20 +119,20 @@
     uint32_t index,
     V8CSSUnparsedSegment* segment,
     ExceptionState& exception_state) {
-  if (index < tokens_.size()) {
-    tokens_[index] = segment;
+  if (index < segments_.size()) {
+    segments_[index] = segment;
     return IndexedPropertySetterResult::kIntercepted;
   }
 
-  if (index == tokens_.size()) {
-    tokens_.push_back(segment);
+  if (index == segments_.size()) {
+    segments_.push_back(segment);
     return IndexedPropertySetterResult::kIntercepted;
   }
 
   exception_state.ThrowRangeError(
       ExceptionMessages::IndexOutsideRange<unsigned>(
-          "index", index, 0, ExceptionMessages::kInclusiveBound, tokens_.size(),
-          ExceptionMessages::kInclusiveBound));
+          "index", index, 0, ExceptionMessages::kInclusiveBound,
+          segments_.size(), ExceptionMessages::kInclusiveBound));
   return IndexedPropertySetterResult::kIntercepted;
 }
 
@@ -195,14 +195,14 @@
     return false;  // Cycle.
   }
   values_on_stack.insert(this);
-  for (unsigned i = 0; i < tokens_.size(); i++) {
+  for (unsigned i = 0; i < segments_.size(); i++) {
     if (i) {
       builder.Append("/**/");
     }
-    switch (tokens_[i]->GetContentType()) {
+    switch (segments_[i]->GetContentType()) {
       case V8CSSUnparsedSegment::ContentType::kCSSVariableReferenceValue: {
         const auto* reference_value =
-            tokens_[i]->GetAsCSSVariableReferenceValue();
+            segments_[i]->GetAsCSSVariableReferenceValue();
         builder.Append("var(");
         builder.Append(reference_value->variable());
         if (reference_value->fallback()) {
@@ -216,7 +216,7 @@
         break;
       }
       case V8CSSUnparsedSegment::ContentType::kString:
-        builder.Append(tokens_[i]->GetAsString());
+        builder.Append(segments_[i]->GetAsString());
         break;
     }
   }
diff --git a/third_party/blink/renderer/core/css/cssom/css_unparsed_value.h b/third_party/blink/renderer/core/css/cssom/css_unparsed_value.h
index c9dab7a..5d1961b1 100644
--- a/third_party/blink/renderer/core/css/cssom/css_unparsed_value.h
+++ b/third_party/blink/renderer/core/css/cssom/css_unparsed_value.h
@@ -26,8 +26,8 @@
 
  public:
   static CSSUnparsedValue* Create(
-      const HeapVector<Member<V8CSSUnparsedSegment>>& tokens) {
-    return MakeGarbageCollected<CSSUnparsedValue>(tokens);
+      const HeapVector<Member<V8CSSUnparsedSegment>>& segments) {
+    return MakeGarbageCollected<CSSUnparsedValue>(segments);
   }
 
   // Blink-internal constructor
@@ -37,14 +37,14 @@
   static CSSUnparsedValue* FromCSSValue(const CSSUnparsedDeclarationValue&);
   static CSSUnparsedValue* FromCSSVariableData(const CSSVariableData&);
   static CSSUnparsedValue* FromString(const String& string) {
-    HeapVector<Member<V8CSSUnparsedSegment>> tokens;
-    tokens.push_back(MakeGarbageCollected<V8CSSUnparsedSegment>(string));
-    return Create(tokens);
+    HeapVector<Member<V8CSSUnparsedSegment>> segments;
+    segments.push_back(MakeGarbageCollected<V8CSSUnparsedSegment>(string));
+    return Create(segments);
   }
 
   explicit CSSUnparsedValue(
-      const HeapVector<Member<V8CSSUnparsedSegment>>& tokens)
-      : tokens_(tokens) {}
+      const HeapVector<Member<V8CSSUnparsedSegment>>& segments)
+      : segments_(segments) {}
   CSSUnparsedValue(const CSSUnparsedValue&) = delete;
   CSSUnparsedValue& operator=(const CSSUnparsedValue&) = delete;
 
@@ -60,10 +60,10 @@
       V8CSSUnparsedSegment* segment,
       ExceptionState& exception_state);
 
-  wtf_size_t length() const { return tokens_.size(); }
+  wtf_size_t length() const { return segments_.size(); }
 
   void Trace(Visitor* visitor) const override {
-    visitor->Trace(tokens_);
+    visitor->Trace(segments_);
     CSSStyleValue::Trace(visitor);
   }
 
@@ -81,7 +81,7 @@
       StringBuilder&,
       HeapHashSet<Member<const CSSUnparsedValue>>& values_on_stack) const;
 
-  HeapVector<Member<V8CSSUnparsedSegment>> tokens_;
+  HeapVector<Member<V8CSSUnparsedSegment>> segments_;
 
   FRIEND_TEST_ALL_PREFIXES(CSSUnparsedDeclarationValueTest, MixedList);
 };
Loading diff…

Original Bug Report

reported by [email protected]

UAF in blink::PendingInvalidations

Summary

PendingInvalidations::ScheduleInvalidationSetsForNode() takes a NodeInvalidationSets& reference to a value stored inside PendingInvalidations::pending_invalidation_map_ (a HeapHashMap) and then calls PossiblyScheduleNthPseudoInvalidations(node) while that reference is still live. PossiblyScheduleNthPseudoInvalidations() can synchronously re-enter invalidation scheduling on the parent (via StyleEngine::ScheduleNthPseudoInvalidations(parent)), which can insert into the same HeapHashMap and trigger Rehash(). That rehash frees/poisons the old hash table backing, invalidating the outer reference. Therefore, when the outer frame resumes, it access the stale NodeInvalidationSets and crash with UAF.

Details

PendingInvalidations::ScheduleInvalidationSetsForNode keeps a reference into pending_invalidation_map_ while calling out to a helper that can re-enter scheduling:

NodeInvalidationSets& pending_invalidations = EnsurePendingInvalidations(node);
for (auto& invalidation_set : invalidation_lists.siblings) {
  if (pending_invalidations.Siblings().Contains(invalidation_set)) {
    continue;
  }
  if (invalidation_set->InvalidatesNth()) {
    PossiblyScheduleNthPseudoInvalidations(node);
  }
  pending_invalidations.Siblings().push_back(invalidation_set);
}

The callout is synchronous. In PossiblyScheduleNthPseudoInvalidations, when the parent has positional-rule flags set, it immediately schedules nth invalidations on the parent:

if ((parent->ChildrenAffectedByForwardPositionalRules() && node.nextSibling()) ||
    (parent->ChildrenAffectedByBackwardPositionalRules() &&
     node.previousSibling())) {
  node.GetDocument().GetStyleEngine().ScheduleNthPseudoInvalidations(*parent);
}

StyleEngine::ScheduleNthPseudoInvalidations() then re-enters the same PendingInvalidations instance (source):

pending_invalidations_.ScheduleInvalidationSetsForNode(invalidation_lists,
                                                       nth_parent);

The re-entrant call can insert into the same HeapHashMap via PendingInvalidations::EnsurePendingInvalidations:

PendingInvalidationMap::AddResult add_result =
    pending_invalidation_map_.insert(&node, NodeInvalidationSets());
return add_result.stored_value->value;

When the insertion crosses the hash table’s expansion threshold and MustRehashInPlace() is true, HashTable::Expand can do a same-size rehash:

// ...
else if (MustRehashInPlace()) {
  new_size = table_size_;
} else {
  new_size = table_size_ * 2;
}
return Rehash(new_size, entry);

When returned to the outer ScheduleInvalidationSetsForNode() frame, it continues operating on the stale pending_invalidations reference. The next Vector access through that stale/invalid value (Vector::size() in wtf/vector.h:1348) and triggers the UAF.

Bisection

This issue is introduced by the commit c98887131608ef87756a77a3493aac8b6b8ec02e, hence it affects all stable versions.

Reproduction

Download the chrome from https://storage.googleapis.com/chromium-browser-asan/linux-release/asan-linux-release-1588577.zip

Run the following commandline:

./chrome --no-sandbox poc.html

You would get the ASAN crash with UAP in asan.txt.

Suggested Fix

Avoid holding references/pointers into pending_invalidation_map_ across any call that can re-enter PendingInvalidations and mutate/rehash the same map.

View on issue tracker