CVE-2026-8518
Overview
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
Patch
From 9674640f1813e9f10b97a9270108079d2fe525aa Mon Sep 17 00:00:00 2001 From: Keishi Hattori <[email protected]> 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 <[email protected]> Reviewed-by: Takashi Sakamoto <[email protected]> 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.