High patch-now ✔︎ verified CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Blink
DescriptionUse after free in Blink
ComponentBlink
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — factors set 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
Reviewer notes: none material; all cited lines (cc:85-99, :219-221, h:19-20) match. Minor wording: EnsurePendingInvalidations still returns NodeInvalidationSets& (dereferenced unique_ptr), not a unique_ptr. Mapped commit 45c5a70 is an unrelated rename; b28cbf5 is the fix.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker487117772
Fix commitb28cbf5d5292 (chromium/src) +14/-7
Related commits45c5a70d984d previously mapped (not the fix) · chromium/src
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedSyn4pse
Reported2026-02-24 (per vendor advisory)
Fix landed2026-03-03 (public git)
Disclosed2026-03-18
Patch gap15 days — fix public in source before the advisory

Background

`PendingInvalidations`
Blink’s deferred style-invalidation bookkeeping that records each node’s pending invalidation sets until invalidation runs.
`PendingInvalidationMap`
a HeapHashMap keyed by ContainerNode whose value, before the fix, was a NodeInvalidationSets stored inline in the table.
`HeapHashMap`
Blink’s garbage-collected open-addressed hash map that is not node-based, so an insert can rehash and relocate its value storage.

Root Cause Analysis

Re-mapped on 2026-10-03: our pipeline had picked the earliest commit citing this bug (45c5a70d984d), which an audit found was a preparatory/helper change, not the fix. This page now points to the commit whose diff removes the bug.

In the pre-fix source, PendingInvalidations::ScheduleInvalidationSetsForNode binds a live reference NodeInvalidationSets& pending_invalidations = EnsurePendingInvalidations(node); at pending_invalidations.cc:85-86 and then iterates invalidation_lists.siblings at :87-99. Inside that loop, :94-96 calls PossiblyScheduleNthPseudoInvalidations(node) while the reference is still held, and :97 subsequently touches pending_invalidations.Siblings().push_back(...). EnsurePendingInvalidations at :219-221 inserts into pending_invalidation_map_ via pending_invalidation_map_.insert(&node, NodeInvalidationSets()) and returns add_result.stored_value->value, i.e. a reference directly into the map’s backing. Because PendingInvalidationMap was HeapHashMap<Member<ContainerNode>, NodeInvalidationSets> (pending_invalidations.h:19-20) with the value stored inline, a re-entrant insertion for the parent (via StyleEngine::ScheduleNthPseudoInvalidations, per the report) can cross the growth threshold, rehash, and move that backing — leaving the outer reference dangling and read after free.

The fix changes the value type to std::unique_ptr<NodeInvalidationSets>, so the referenced object has a stable heap address that survives a rehash; EnsurePendingInvalidations now inserts std::make_unique<NodeInvalidationSets>() and returns *add_result.stored_value->value, and the callers dereference the pointer.

The patch TIGHTENS the object’s lifetime/address stability rather than relaxing any check.

Key insight
The code held a reference into a non-node-based hash map’s inline value across a call that could re-enter and rehash the same map; storing the value behind a std::unique_ptr gives it a stable address so the reference stays valid.

Attack Path

  1. Trigger nth-affecting invalidation Use CSS positional/nth rules and script-driven DOM changes so ScheduleInvalidationSetsForNode runs on a node whose parent has positional-rule flags set.
  2. Re-enter scheduling The still-live sibling loop calls PossiblyScheduleNthPseudoInvalidations, which schedules nth invalidations on the parent and re-enters PendingInvalidations on the same map. (per reporter)
  3. Force a rehash The re-entrant EnsurePendingInvalidations insert crosses the map’s expansion threshold and rehashes, relocating the inline value backing. (inferred)
  4. Use the stale reference Control returns to the outer frame, which accesses the now-freed NodeInvalidationSets, producing the use-after-free.

Impact Assessment

An attacker-controlled page (CSS plus DOM mutation) can drive a heap use-after-free in the renderer process. The advisory rates this high severity and states it was not exploited in the wild. Escalation beyond the UAF to code execution is (inferred).

Changed Functions

FunctionChangeNotes
Document
third_party/blink/renderer/core/css/invalidation/pending_invalidations.h
modified
Element
third_party/blink/renderer/core/css/invalidation/pending_invalidations.h
modified

Files Changed

  • third_party/blink/renderer/core/css/invalidation/pending_invalidations.cc
  • third_party/blink/renderer/core/css/invalidation/pending_invalidations.h
  • third_party/blink/renderer/core/css/invalidation/style_invalidator.cc

Audit Directions

  • Reference into rehashable container
    Flag any reference or pointer into a HeapHashMap/HashMap value that is held across a call able to insert into the same map.
  • Re-entrant scheduling
    Audit invalidation and scheduling helpers that can synchronously re-enter and mutate the same collection mid-iteration.
diff --git a/third_party/blink/renderer/core/css/invalidation/pending_invalidations.cc b/third_party/blink/renderer/core/css/invalidation/pending_invalidations.cc
index 19c561c..6ea0879 100644
--- a/third_party/blink/renderer/core/css/invalidation/pending_invalidations.cc
+++ b/third_party/blink/renderer/core/css/invalidation/pending_invalidations.cc
@@ -186,11 +186,11 @@
   auto pending_invalidations_iterator =
       pending_invalidation_map_.find(&element);
   if (pending_invalidations_iterator == pending_invalidation_map_.end() ||
-      pending_invalidations_iterator->value.Siblings().empty()) {
+      pending_invalidations_iterator->value->Siblings().empty()) {
     return;
   }
   NodeInvalidationSets& pending_invalidations =
-      pending_invalidations_iterator->value;
+      *pending_invalidations_iterator->value;
 
   InvalidationLists invalidation_lists;
   for (const auto& invalidation_set : pending_invalidations.Siblings()) {
@@ -214,11 +214,12 @@
     ContainerNode& node) {
   auto it = pending_invalidation_map_.find(&node);
   if (it != pending_invalidation_map_.end()) {
-    return it->value;
+    return *it->value;
   }
   PendingInvalidationMap::AddResult add_result =
-      pending_invalidation_map_.insert(&node, NodeInvalidationSets());
-  return add_result.stored_value->value;
+      pending_invalidation_map_.insert(
+          &node, std::make_unique<NodeInvalidationSets>());
+  return *add_result.stored_value->value;
 }
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/css/invalidation/pending_invalidations.h b/third_party/blink/renderer/core/css/invalidation/pending_invalidations.h
index 5680db9c..81ce1a7b 100644
--- a/third_party/blink/renderer/core/css/invalidation/pending_invalidations.h
+++ b/third_party/blink/renderer/core/css/invalidation/pending_invalidations.h
@@ -5,6 +5,8 @@
 #ifndef THIRD_PARTY_BLINK_RENDERER_CORE_CSS_INVALIDATION_PENDING_INVALIDATIONS_H_
 #define THIRD_PARTY_BLINK_RENDERER_CORE_CSS_INVALIDATION_PENDING_INVALIDATIONS_H_
 
+#include <memory>
+
 #include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/core/css/invalidation/node_invalidation_sets.h"
 #include "third_party/blink/renderer/platform/heap/collection_support/heap_hash_map.h"
@@ -16,8 +18,12 @@
 class Document;
 class Element;
 
+// HeapHashMap is not node-based and thus does not have stable iterators,
+// but we rely on pointers to stay stable even during modification of the map.
+// Thus, use unique_ptr to make sure the NodeInvalidationSets has a stable
+// address.
 using PendingInvalidationMap =
-    HeapHashMap<Member<ContainerNode>, NodeInvalidationSets>;
+    HeapHashMap<Member<ContainerNode>, std::unique_ptr<NodeInvalidationSets>>;
 
 // Performs deferred style invalidation for DOM subtrees.
 //
diff --git a/third_party/blink/renderer/core/css/invalidation/style_invalidator.cc b/third_party/blink/renderer/core/css/invalidation/style_invalidator.cc
index 3c49524..3c11b75 100644
--- a/third_party/blink/renderer/core/css/invalidation/style_invalidator.cc
+++ b/third_party/blink/renderer/core/css/invalidation/style_invalidator.cc
@@ -185,7 +185,7 @@
     return;
   }
   NodeInvalidationSets& pending_invalidations =
-      pending_invalidations_iterator->value;
+      *pending_invalidations_iterator->value;
 
   DCHECK(pending_nth_sets_.empty());
 
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