CVE-2026-19559
Overview
Background
- Slots and assigned nodes
- In Shadow DOM, an
HTMLSlotElementholds the light-DOM nodes assigned to it.slot.AssignedNodes()returns a reference to the slot’s internalassigned_nodes_vector. - Flattened assigned nodes
CollectFlattenedAssignedNodesbuilds 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.
assigned_nodes_ made during recursion.Attack Path
- Build nested shadow trees with slots
A web page builds a Shadow DOM structure in which slot contents are flattened recursively, so
CollectFlattenedAssignedNodesiterates over a slot’s assigned nodes and descends into further slots. - 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.
- Outer slot's assigned nodes cleared
The nested recalculation clears or replaces the outer slot’s
assigned_nodes_, freeing theHeapVectorbacking store that the outer loop is still reading. - 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
Files Changed
third_party/blink/renderer/core/html/html_slot_element.cc
Audit Directions
- References to live containers across calls that can re-enterSearch Blink for
const HeapVector<...>& x = obj.Getter()(andfor (... : 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 traversalsReview
FlatTreeTraversal,SlotAssignment::RecalcAssignment,HTMLSlotElement::AssignedNodes/FlattenedAssignedNodesand related helpers for other spots where recursion into nested slots can trigger recalculation that changes an outer slot’s state. - Lazy recalculation triggered from gettersGetters 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.
Patch
From 4de277c7202e0f7e9a2999decffcb0344b3e883d Mon Sep 17 00:00:00 2001 From: Mason Freed <[email protected]> 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 <[email protected]> Commit-Queue: Mason Freed <[email protected]> Auto-Submit: Mason Freed <[email protected]> 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.