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 HTML
DescriptionUse after free in HTML
ComponentHTML
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld 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
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker540100588
Fix commit4de277c7202e (chromium/src) +2/-1
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
CreditedGoogle
Reported2026-07-28 (per vendor advisory)
Fix landed2026-08-03 (public git)
Disclosed2026-08-11
Patch gap8 days — fix public in source before the advisory

Background

Slots and assigned nodes
In Shadow DOM, an HTMLSlotElement holds the light-DOM nodes assigned to it. slot.AssignedNodes() returns a reference to the slot’s internal assigned_nodes_ vector.
Flattened assigned nodes
CollectFlattenedAssignedNodes builds the flat-tree list of a slot’s contents. It recurses into nested slots and falls back to the slot’s own children when nothing is assigned.
Slot assignment recalculation
Blink recomputes slot assignments lazily and synchronously. Recomputing them can rewrite or clear the assigned_nodes_ of slots, including the slot currently being walked.
`HeapVector` backing store
The Oilpan-managed out-of-line buffer behind a HeapVector<Member<Node>>. Clearing or reassigning the vector can release that buffer while a reference or iterator into it is still in use.

Root Cause Analysis

In CollectFlattenedAssignedNodes (named in the commit message; the hunk shows only the const HTMLSlotElement& slot parameter and the start of the body), the code bound const HeapVector<Member<Node>>& assigned_nodes = slot.AssignedNodes(). This was a reference straight into the slot’s live assigned_nodes_, and the function then iterated over it. The loop body can synchronously trigger slot assignment recalculation, for example while recursing into nested slots. According to the commit, a nested recalculation can clear the outer slot’s assigned nodes, which frees the vector’s backing store while the outer loop is still iterating over it. The implicit invariant, that the slot’s assigned-node list stays the same for the whole iteration, therefore did not hold, and the loop read freed memory.

The fix replaces the reference with a by-value copy (const HeapVector<Member<Node>> assigned_nodes = slot.AssignedNodes()), so iteration runs over a private backing store whose Member<Node> entries also keep the nodes alive, whatever the nested recalculation does to assigned_nodes_. The commit says the fix was checked against the PoC from the bug report but that no repro test was committed, so the exact DOM sequence is not shown in the patch.

Key insight
The function iterated over a reference to a mutable container that the loop body could cause to be cleared again. Taking a defensive copy separates the iteration from changes to the slot’s assigned_nodes_ made during recursion.

Attack Path

  1. Build nested shadow trees with slots A web page builds a Shadow DOM structure in which slot contents are flattened recursively, so CollectFlattenedAssignedNodes iterates over a slot’s assigned nodes and descends into further slots.
  2. Force recalculation during the iteration The structure is arranged so that, while the outer slot’s list is being iterated, a nested call synchronously triggers slot assignment recalculation. The exact trigger is not shown in the patch.
  3. Outer slot's assigned nodes cleared The nested recalculation clears or replaces the outer slot’s assigned_nodes_, freeing the HeapVector backing store that the outer loop is still reading.
  4. Iteration continues over freed memory Before the fix, the outer loop read Member<Node> entries from the freed backing store, which is a use-after-free in the renderer. Any controlled reuse of that memory leading to code execution is (inferred).

Impact Assessment

The bug is a use-after-free in Blink’s HTML/Shadow DOM code in the renderer process. Slots and shadow roots are standard web platform features, so ordinary web content in a stock build is the likely trigger surface (inferred: the commit says a PoC from the bug report confirmed the fix but includes no repro). It does not appear to need any flag, extension or user gesture. The advisory gives only “Use after free in HTML”. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.8.

Files Changed

  • third_party/blink/renderer/core/html/html_slot_element.cc

Audit Directions

  • References to live containers across calls that can re-enter
    Search Blink for const HeapVector<...>& x = obj.Getter() (and for (... : obj.Getter())) where the loop body can run style or layout updates, slot assignment, mutation events, or script, and check whether the underlying container can be cleared or reallocated.
  • Recursive flat-tree and slot traversals
    Review FlatTreeTraversal, SlotAssignment::RecalcAssignment, HTMLSlotElement::AssignedNodes/FlattenedAssignedNodes and related helpers for other spots where recursion into nested slots can trigger recalculation that changes an outer slot’s state.
  • Lazy recalculation triggered from getters
    Getters that recompute state on demand, such as AssignedNodes(), can quietly change members of other objects. Audit their callers for cached references or iterators that the recomputation would invalidate.
From 4de277c7202e0f7e9a2999decffcb0344b3e883d Mon Sep 17 00:00:00 2001
From: Mason Freed <masonf@chromium.org>
Date: Mon, 03 Aug 2026 10:32:04 -0700
Subject: [PATCH] Add defensive copy in CollectFlattenedAssignedNodes

CollectFlattenedAssignedNodes binds a reference to assigned_nodes_ and
iterates over it by reference while synchronously recursing into slot
assignment recalculation. A nested recalculation can clear assigned
nodes of the outer slot, freeing the backing vector during iteration.

This change creates a defensive copy of assigned_nodes to ensure the
vector backing remains valid throughout the loop iteration.

This fix was validated using the POC provided in the bug description,
which was implemented and executed locally, but no committable repro
was found.

Fixed: 540100588
Change-Id: I39d6e02c7face470cbfe42c6419c5f2ec15519ae
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8175731
Reviewed-by: Joey Arhar <jarhar@chromium.org>
Commit-Queue: Mason Freed <masonf@chromium.org>
Auto-Submit: Mason Freed <masonf@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1672756}
---
diff --git a/third_party/blink/renderer/core/html/html_slot_element.cc b/third_party/blink/renderer/core/html/html_slot_element.cc
index 5da4aab..66b0228 100644
--- a/third_party/blink/renderer/core/html/html_slot_element.cc
+++ b/third_party/blink/renderer/core/html/html_slot_element.cc
@@ -94,7 +94,8 @@
     const HTMLSlotElement& slot) {
   DCHECK(slot.SupportsAssignment());
 
-  const HeapVector<Member<Node>>& assigned_nodes = slot.AssignedNodes();
+  // Copy the array - code inside the loop can modify assigned nodes.
+  const HeapVector<Member<Node>> assigned_nodes = slot.AssignedNodes();
   HeapVector<Member<Node>> nodes;
   if (assigned_nodes.empty()) {
     // Fallback contents.
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Use-After-Free in HTMLSlotElement::CollectFlattenedAssignedNodes

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential Use-After-Free (UAF) vulnerability exists in HTMLSlotElement’s CollectFlattenedAssignedNodes helper due to range-iterating assigned_nodes_ by reference while synchronously recursing into slot assignment recalculations. A nested assignment recalculation can clear the slot’s assigned nodes, prompt-freeing the backing storage of the iterated vector and causing subsequent iterations to dereference freed memory. Four sibling loops in the same file were previously protected against this hazard with defensive copies, but this instance was missed.

Affected files:

  • third_party/blink/renderer/core/html/html_slot_element.cc

Estimated timestamp from git blame: 2018-01-18

Root Cause Analysis

In third_party/blink/renderer/core/html/html_slot_element.cc, the helper function CollectFlattenedAssignedNodes resolves a flattened list of assigned nodes by traversing slot hierarchies.

The function binds a const reference to the slot’s assigned_nodes_ HeapVector<Member<Node>> and iterates over it by reference:

// third_party/blink/renderer/core/html/html_slot_element.cc
HeapVector<Member<Node>> CollectFlattenedAssignedNodes(const HTMLSlotElement& slot) {
  DCHECK(slot.SupportsAssignment());

  const HeapVector<Member<Node>>& assigned_nodes = slot.AssignedNodes(); // Aliases slot.assigned_nodes_
  HeapVector<Member<Node>> nodes;
  if (assigned_nodes.empty()) {
    // ...
  } else {
    for (auto& node : assigned_nodes) { // <-- Iterating by reference
      DCHECK(node->IsSlotable());
      if (auto* assigned_node_slot =
              ToHTMLSlotElementIfSupportsAssignmentOrNull(*node))
        nodes.append_range(CollectFlattenedAssignedNodes(*assigned_node_slot)); // <-- Synchronous recursion
      else
        nodes.push_back(node);
    }
  }
  return nodes;
}

During recursive resolution of nested slots, the loop body can trigger slot assignment recalculations (RecalcAssignment()) on another shadow root. If a cross-tree assignment occurs during this nested recalculation, it can mark the outer slot’s shadow root as dirty (needs_assignment_recalc_ = true).

Upon returning to the outer loop, the subsequent iteration calls AssignedNodes() on the next assigned node. Because its containing shadow root is now marked dirty, it executes a nested RecalcAssignment() on the outer shadow root. This nested recalculation loops over all slots and calls slot->WillRecalcAssignedNodes(), which runs assigned_nodes_.clear(). In Blink’s Oilpan garbage-collected allocator (cppgc), clearing the HeapVector shrinks its capacity to 0 and prompt-frees the backing memory. The outer loop then resumes and dereferences the now-freed HeapVectorBacking<Member<Node>>, resulting in a high-severity Use-After-Free (UAF) condition.

Four sibling loops in html_slot_element.cc (AttachLayoutTreeForSlotChildren, DetachLayoutTree, RebuildDistributedChildrenLayoutTrees, and RecalcStyleForSlotChildren) were previously patched with defensive copies for this exact hazard (referencing crbug.com/497830330 and crbug.com/520167277), but CollectFlattenedAssignedNodes was missed.


Suggested Potential Steps to Reproduce

Note: The following are potential steps derived from logical tracing of the code; our tooling does not currently have the capability to execute proof-of-concept code.

  1. Setup DOM Tree & Shadow Roots:

    • Create a host element host1 and attach an open author shadow root R1 in manual slot assignment mode.
    • Inside R1, append a slot element S1.
    • Append two children to host1: S2 (which is also a slot element) and S3 (a standard element).
    • Assign both S2 and S3 to S1 using manual assignment: S1.assign(S2, S3).
  2. Setup Nested Shadow Root:

    • Attach an open author shadow root R2 in manual slot assignment mode to S2.
    • Dirty R2’s slot assignment flag (e.g., by modifying its slots or appending a child to S2).
  3. Call Flattening API:

    • Invoke the JS API S1.assignedNodes({flatten: true}), which routes to CollectFlattenedAssignedNodes(S1).
    • The loop binds a reference to S1’s assigned nodes: [S2, S3].
  4. Trigger UAF Flow:

    • Iteration 1: Processes S2. Since S2 is a slot, the code recurses into CollectFlattenedAssignedNodes(S2).
    • Inside the recursion, S2.AssignedNodes() invokes R2’s RecalcAssignment().
    • During R2’s recalculation, reassign S3 from S1 (in R1) to a slot in R2 via manual assignment (S2.assign(S3)). This triggers HTMLSlotElement::Assign on S2.
    • HTMLSlotElement::Assign detects that S3’s previous slot was S1 (belonging to R1) and invokes S1->DidSlotChange(), which sets R1.needs_assignment_recalc_ = true.
    • The recursive call completes and returns control to S1’s outer loop.
    • Iteration 2: Processes S3. The loop calls S3.AssignedNodes().
    • Since S3 resides in R1’s scope and R1 is dirty, this triggers a nested R1.RecalcAssignment().
    • R1.RecalcAssignment() calls S1->WillRecalcAssignedNodes(), which runs assigned_nodes_.clear() and prompt-frees S1’s backing storage.
    • The loop continues to the next iteration step, attempting to read or increment the iterator pointing to the now-freed backing store of S1->assigned_nodes_.

Suggested Fix

The fix is identical to the remediations applied to the other four sibling loops in the same file. CollectFlattenedAssignedNodes must create a defensive copy of the slot’s assigned nodes vector instead of holding and iterating a live const reference:

diff --git a/third_party/blink/renderer/core/html/html_slot_element.cc b/third_party/blink/renderer/core/html/html_slot_element.cc
--- a/third_party/blink/renderer/core/html/html_slot_element.cc
+++ b/third_party/blink/renderer/core/html/html_slot_element.cc
@@ -94,7 +94,8 @@ HeapVector<Member<Node>> CollectFlattenedAssignedNodes(
     const HTMLSlotElement& slot) {
   DCHECK(slot.SupportsAssignment());
 
-  const HeapVector<Member<Node>>& assigned_nodes = slot.AssignedNodes();
+  // Defensive copy to prevent UAF from synchronous slot assignment recalculation.
+  const HeapVector<Member<Node>> assigned_nodes = slot.AssignedNodes();
   HeapVector<Member<Node>> nodes;
   if (assigned_nodes.empty()) {
     // Fallback contents.

Evaluated with Chrome root at commit: 94d9235ebe3b7276e5284f0dc5d55577ff949908


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker