Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in HTML
DescriptionUse after free in HTML
ComponentHTML
Bug ClassUAF
Tracker540100588
Fix commit4de277c7202e (chromium/src) +2/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-11

Files Changed

  • third_party/blink/renderer/core/html/html_slot_element.cc
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