CVE-2026-4449
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/css/cssom/css_unparsed_value.cc |
modified |
Files Changed
third_party/blink/renderer/core/css/cssom/css_unparsed_value.ccthird_party/blink/renderer/core/css/cssom/css_unparsed_value.h
Patch
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); };
Original Bug Report
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.
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/css/invalidation/pending_invalidations.cc;l=21
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/css/invalidation/pending_invalidations.cc;l=213
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/css/style_engine.cc;l=1459
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/css/style_engine.cc;l=2111
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/wtf/hash_table.h;l=1619
- https://storage.googleapis.com/chromium-browser-asan/linux-release/asan-linux-release-1588577.zip