CVE-2026-8518
Overview
Background
- `HTMLSlotElement`
- the Blink element (
<slot>) that projects a shadow host’s light-DOM children into a shadow tree via slot assignment. - `assigned_nodes_`
- a
HeapVector<Member<Node>>member ofHTMLSlotElementholding the nodes currently assigned to the slot in flat-tree order. - `DetachLayoutTree`
- the method that tears down the layout objects for a slot and its assigned nodes, optionally as part of a reattach.
- Synchronous style/layout recalc
- layout-tree work triggered inside detach that can run script-observable callbacks or slot reassignment, which may mutate (and reallocate the backing store of)
assigned_nodes_.
Root Cause Analysis
In HTMLSlotElement::DetachLayoutTree, when SupportsAssignment() is true, the code bound a const reference (const HeapVector<Member<Node>>& flat_tree_children = assigned_nodes_;) directly to the live assigned_nodes_ member and then iterated over it with a range-based for loop. Detaching each assigned node’s layout tree can trigger a synchronous recalc (per the added comment, “sync recalc”) that re-runs slot assignment and mutates assigned_nodes_; because a HeapVector reallocates its backing buffer when its contents change, the loop’s iterators and the Member<Node> references derived from the reference are invalidated mid-iteration, so the loop continues to dereference freed/moved storage — a use-after-free.
The fix removes the &, making flat_tree_children a by-value copy (const HeapVector<Member<Node>> flat_tree_children = assigned_nodes_;) taken before the loop, so the iteration walks a stable snapshot that is unaffected by any reallocation of the member during detach.
assigned_nodes_ member by reference while the loop body could reentrantly reallocate that same vector; the fix snapshots it into a local copy so iteration is decoupled from any concurrent mutation.Attack Path
- Set up a slotted shadow tree
Author a page with a shadow host and a
<slot>so thatSupportsAssignment()holds andassigned_nodes_is populated with several assigned nodes. - Trigger layout-tree detach
Cause
HTMLSlotElement::DetachLayoutTreeto run (e.g. via reattach/removal of the host subtree), entering the loop over the reference toassigned_nodes_. - Reenter and reallocate during the loop
Detaching one assigned node’s layout tree drives a synchronous recalc that re-runs slot assignment and mutates
assigned_nodes_, reallocating its backing store and freeing the buffer the reference and iterators point at (inferred: exact reentrant callback and mutation not shown in the diff). - Use freed storage
The still-running range loop continues dereferencing the invalidated iterator / stale
Member<Node>, reading freed heap memory (use-after-free).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
forthird_party/blink/renderer/core/html/html_slot_element.cc |
modified |
Files Changed
third_party/blink/renderer/core/html/html_slot_element.cc
Audit Directions
- Reference-to-member iterationFlag loops that bind a
const&(or iterator) to aHeapVector/Vectormember and then call code (detach, recalc, script, observers) inside the loop that can mutate or reallocate that same member. - Reentrant slot/flat-tree mutationAudit shadow-DOM and slot-assignment paths where detach/attach can synchronously re-run assignment and edit
assigned_nodes_or similar collections mid-traversal. - Snapshot-vs-live collectionsLook for detach/teardown routines that assume a collection is stable across the loop; confirm they copy the collection first when the body can trigger layout or style recalc.
Patch
From 9674640f1813e9f10b97a9270108079d2fe525aa Mon Sep 17 00:00:00 2001
From: Keishi Hattori <keishi@google.com>
Date: Thu, 02 Apr 2026 19:17:37 -0700
Subject: [PATCH] Fix realloc while iterating in HTMLSlotElement::DetachLayoutTree
Bug: 497830330
Change-Id: Id29714983245bcb58f3f3c2174f4b098c963d948
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7718078
Commit-Queue: Keishi Hattori <keishi@chromium.org>
Reviewed-by: Takashi Sakamoto <tasak@google.com>
Cr-Commit-Position: refs/heads/main@{#1609626}
---
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 9347ee4..deb9ad3e 100644
--- a/third_party/blink/renderer/core/html/html_slot_element.cc
+++ b/third_party/blink/renderer/core/html/html_slot_element.cc
@@ -365,7 +365,8 @@
void HTMLSlotElement::DetachLayoutTree(bool performing_reattach) {
if (SupportsAssignment()) {
auto* host = OwnerShadowHost();
- const HeapVector<Member<Node>>& flat_tree_children = assigned_nodes_;
+ // Defensive copy to prevent UAF from sync recalc. See crbug.com/497830330.
+ const HeapVector<Member<Node>> flat_tree_children = assigned_nodes_;
for (auto& node : flat_tree_children) {
// Don't detach the assigned node if the node is no longer a child of the
// host.
Original Bug Report
Use-After-Free in HTMLSlotElement::DetachLayoutTree during AX update
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A potential Use-After-Free exists in HTMLSlotElement::DetachLayoutTree when iterating over assigned nodes. On platforms where AriaModalPrunesAXTree is enabled (macOS/Android), detaching a node can trigger an accessibility update that synchronously recalculates slot assignments. This recalculation clears the vector being iterated, leaving a dangling iterator that leads to a Use-After-Free.
Affected files:
third_party/blink/renderer/core/html/html_slot_element.ccthird_party/blink/renderer/core/dom/node.ccthird_party/blink/renderer/core/dom/slot_assignment.ccthird_party/blink/renderer/modules/accessibility/ax_object_cache_impl.ccthird_party/blink/renderer/core/dom/flat_tree_traversal.cc
Estimated timestamp from git blame: 2021-02-16
Disclaimer
Please note: This vulnerability report is based on a static analysis of the Chromium codebase using an automated agent. While the code paths and logic have been meticulously verified step-by-step, we have not yet developed or executed a working Proof of Concept (PoC) exploit. Therefore, this issue is described as a ‘potential’ vulnerability.
Vulnerability Details
A potential Use-After-Free (UAF) exists in HTMLSlotElement::DetachLayoutTree(). The method uses a C++ range-based for loop to iterate over the assigned_nodes_ vector:
void HTMLSlotElement::DetachLayoutTree(bool performing_reattach) {
if (SupportsAssignment()) {
auto* host = OwnerShadowHost();
const HeapVector<Member<Node>>& flat_tree_children = assigned_nodes_;
for (auto& node : flat_tree_children) {
if (host == node->parentNode())
node->DetachLayoutTree(performing_reattach);
}
}
// ...
}
The range-based for loop implicitly caches the begin() and end() iterators of the assigned_nodes_ backing buffer. However, the call to node->DetachLayoutTree() can trigger re-entrant code that clears this vector.
Specifically, when detaching a node, Node::DetachLayoutTree calls cache->RemoveSubtree(this) to update the accessibility (AX) tree. On macOS and Android, the AriaModalPrunesAXTree setting is enabled by default. If the node being detached is currently tracked as the active_aria_modal_dialog_, AXObjectCacheImpl::Remove() invokes UpdateActiveAriaModalDialog(FocusedNode()) to find the new active modal.
If the newly focused node is a slottable element in the light DOM, UpdateActiveAriaModalDialog uses FlatTreeTraversal::Parent() to walk up the flat tree. This traversal calls Node::AssignedSlot(), which checks if (GetDocument().IsInSlotAssignmentRecalc()).
The Crux of the Vulnerability:
During DOM removal (ContainerNode::RemoveChild), Blink uses a SlotAssignmentRecalcForbiddenScope to prevent recalcs. However, in Release builds, this scope is defined as an empty class and does not increment the document’s slot_assignment_recalc_depth_ counter (it only enforces safety via DCHECKs in Debug builds).
Consequently, IsInSlotAssignmentRecalc() returns false in Release builds. If the slot assignment is marked as dirty, AssignedSlot() proceeds to synchronously call root->GetSlotAssignment().RecalcAssignment().
This recalculation invokes HTMLSlotElement::WillRecalcAssignedNodes(), which calls assigned_nodes_.clear(). In Blink’s HeapVector, clear() calls ShrinkCapacity(0), immediately freeing the backing buffer via cppgc::subtle::FreeUnreferencedObject().
When the stack unwinds back to the range-based for loop in HTMLSlotElement::DetachLayoutTree, the iterator is left dangling. Accessing the next element reads from freed memory, leading to a virtual method call (node->DetachLayoutTree) on a potentially attacker-controlled object, providing a primitive for Remote Code Execution (RCE).
Potential Attack Scenario
An attacker could theoretically trigger this sequence via the following steps:
- Create a shadow host with an open
ShadowRootcontaining a<slot name="s1">. - In the host’s light DOM, insert two children assigned to the slot:
Child A(<div role="dialog" aria-modal="true">) andChild B(<div tabindex="0">). - Focus
Child A. Accessibility updates track it as the activearia-modaldialog. - Remove the shadow host from the DOM (
host.remove()). - During the removal setup,
RemoveFocusedElementOfSubtreefires a synchronousblurevent onChild A. - The attacker intercepts the
blurevent. Inside the handler, they append a new element to the host (settingneeds_assignment_recalc_ = trueon the shadow root) and focusChild B. - When the detachment phase begins,
Child Ais detached first in the vulnerable loop. - The AX tree update fires, searching for the new modal (now
Child B), triggering a flat tree traversal. - The traversal forces a synchronous slot recalculation, which clears the
assigned_nodes_vector and frees its backing buffer. - The loop in
HTMLSlotElement::DetachLayoutTreeadvances toChild B, dereferencing the dangling iterator and using the freed memory.
Suggested Fix
The most robust fix is to iterate over a copy of the assigned_nodes_ vector within HTMLSlotElement::DetachLayoutTree, rather than maintaining iterators over a collection that might be modified synchronously during detachment:
void HTMLSlotElement::DetachLayoutTree(bool performing_reattach) {
if (SupportsAssignment()) {
auto* host = OwnerShadowHost();
HeapVector<Member<Node>> assigned_nodes_copy = assigned_nodes_;
for (auto& node : assigned_nodes_copy) {
if (host == node->parentNode())
node->DetachLayoutTree(performing_reattach);
}
}
// ...
}
Alternatively, SlotAssignmentRecalcForbiddenScope could be modified to increment the slot_assignment_recalc_depth_ counter in Release builds as well, preventing the re-entrant recalculation entirely.
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.