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 <andruud@chromium.org>
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 <andruud@chromium.org>
Reviewed-by: Steinar H Gunderson <sesse@chromium.org>
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