CVE-2026-19559
Overview
Files Changed
third_party/blink/renderer/core/html/html_slot_element.cc
Patch
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.
Original Bug Report
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.
-
Setup DOM Tree & Shadow Roots:
- Create a host element
host1and attach an open author shadow rootR1in manual slot assignment mode. - Inside
R1, append a slot elementS1. - Append two children to
host1:S2(which is also a slot element) andS3(a standard element). - Assign both
S2andS3toS1using manual assignment:S1.assign(S2, S3).
- Create a host element
-
Setup Nested Shadow Root:
- Attach an open author shadow root
R2in manual slot assignment mode toS2. - Dirty
R2’s slot assignment flag (e.g., by modifying its slots or appending a child toS2).
- Attach an open author shadow root
-
Call Flattening API:
- Invoke the JS API
S1.assignedNodes({flatten: true}), which routes toCollectFlattenedAssignedNodes(S1). - The loop binds a reference to
S1’s assigned nodes:[S2, S3].
- Invoke the JS API
-
Trigger UAF Flow:
- Iteration 1: Processes
S2. SinceS2is a slot, the code recurses intoCollectFlattenedAssignedNodes(S2). - Inside the recursion,
S2.AssignedNodes()invokesR2’sRecalcAssignment(). - During
R2’s recalculation, reassignS3fromS1(inR1) to a slot inR2via manual assignment (S2.assign(S3)). This triggersHTMLSlotElement::AssignonS2. HTMLSlotElement::Assigndetects thatS3’s previous slot wasS1(belonging toR1) and invokesS1->DidSlotChange(), which setsR1.needs_assignment_recalc_ = true.- The recursive call completes and returns control to
S1’s outer loop. - Iteration 2: Processes
S3. The loop callsS3.AssignedNodes(). - Since
S3resides inR1’s scope andR1is dirty, this triggers a nestedR1.RecalcAssignment(). R1.RecalcAssignment()callsS1->WillRecalcAssignedNodes(), which runsassigned_nodes_.clear()and prompt-freesS1’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_.
- Iteration 1: Processes
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.