Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in HTML
DescriptionUse after free in HTML
ComponentHTML
Bug ClassUAF
Tracker520167277
Fix commit4834d3b1b4da (chromium/src) +44/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-06

Changed Functions

FunctionChangeNotes
TEST_F
third_party/blink/renderer/core/dom/slot_assignment_test.cc
modified
for
third_party/blink/renderer/core/html/html_slot_element.cc
modified

Files Changed

  • third_party/blink/renderer/core/dom/slot_assignment_test.cc
  • third_party/blink/renderer/core/html/html_slot_element.cc
From 4834d3b1b4da5920923d6efa033d6d224907f22b Mon Sep 17 00:00:00 2001
From: Keishi Hattori <[email protected]>
Date: Thu, 23 Jul 2026 22:36:13 -0700
Subject: [PATCH] blink: Copy flat_tree_children_ before iterating in HTMLSlotElement

Copy flat_tree_children_ into a local variable before iterating over
slot children in AttachLayoutTreeForSlotChildren,
RebuildDistributedChildrenLayoutTrees, and RecalcStyleForSlotChildren to
prevent issues if flat_tree_children_ is modified during traversal.

Add a regression test in SlotAssignmentTest.

Bug: 520167277
Change-Id: Ic8988f1caea7c9f2a40cefe18de034ade7467e92
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8130162
Commit-Queue: Keishi Hattori <[email protected]>
Reviewed-by: Mason Freed <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1667650}
---

diff --git a/third_party/blink/renderer/core/dom/slot_assignment_test.cc b/third_party/blink/renderer/core/dom/slot_assignment_test.cc
index c3c261c..ca5f697f 100644
--- a/third_party/blink/renderer/core/dom/slot_assignment_test.cc
+++ b/third_party/blink/renderer/core/dom/slot_assignment_test.cc
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/core/css/style_engine.h"
 #include "third_party/blink/renderer/core/dom/document.h"
 #include "third_party/blink/renderer/core/dom/element.h"
 #include "third_party/blink/renderer/core/dom/flat_tree_traversal.h"
@@ -149,4 +150,38 @@
             GetDocument().Lifecycle().GetState());
 }
 
+// Regression test for crbug.com/520167277.
+// Verifies that slot assignment recalculation works safely when triggered
+// during layout tree rebuild of slot children.
+TEST_F(SlotAssignmentTest, RebuildDistributedChildrenDuringRecalc) {
+  SetBody(R"HTML(
+    <div id="host">
+      <template shadowrootmode="open">
+        <slot></slot>
+      </template>
+      <div id="child1"></div>
+      <div id="child2"></div>
+    </div>
+  )HTML");
+
+  GetDocument().View()->UpdateAllLifecyclePhasesForTest();
+
+  Element* host = GetDocument().QuerySelector(AtomicString("#host"));
+  ASSERT_NE(nullptr, host);
+  ShadowRoot* shadow_root = host->GetShadowRoot();
+  ASSERT_NE(nullptr, shadow_root);
+  Element* child1 = GetDocument().QuerySelector(AtomicString("#child1"));
+  Element* child2 = GetDocument().QuerySelector(AtomicString("#child2"));
+  ASSERT_NE(nullptr, child1);
+  ASSERT_NE(nullptr, child2);
+
+  // Force layout tree rebuild for children and mark slot assignment dirty.
+  child1->SetForceReattachLayoutTree();
+  child2->SetForceReattachLayoutTree();
+  shadow_root->SetNeedsAssignmentRecalc();
+
+  GetDocument().UpdateStyleAndLayoutTree();
+  EXPECT_FALSE(shadow_root->NeedsSlotAssignmentRecalc());
+}
+
 }  // namespace blink
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 ee74445..5da4aab 100644
--- a/third_party/blink/renderer/core/html/html_slot_element.cc
+++ b/third_party/blink/renderer/core/html/html_slot_element.cc
@@ -408,7 +408,9 @@
 }
 
 void HTMLSlotElement::AttachLayoutTreeForSlotChildren(AttachContext& context) {
-  for (Node* child : flat_tree_children_) {
+  // Defensive copy to prevent UAF from sync recalc. See crbug.com/520167277.
+  const HeapVector<Member<Node>> flat_tree_children = flat_tree_children_;
+  for (Node* child : flat_tree_children) {
     child->AttachLayoutTree(context);
   }
 }
@@ -442,7 +444,9 @@
 
   // This loop traverses the nodes from right to left for the same reason as the
   // one described in ContainerNode::RebuildChildrenLayoutTrees().
-  for (const auto& child : base::Reversed(flat_tree_children_)) {
+  // Defensive copy to prevent UAF from sync recalc. See crbug.com/520167277.
+  const HeapVector<Member<Node>> flat_tree_children = flat_tree_children_;
+  for (const auto& child : base::Reversed(flat_tree_children)) {
     RebuildLayoutTreeForChild(child, whitespace_attacher);
   }
 }
@@ -551,7 +555,9 @@
 void HTMLSlotElement::RecalcStyleForSlotChildren(
     const StyleRecalcChange change,
     const StyleRecalcContext& style_recalc_context) {
-  for (auto& node : flat_tree_children_) {
+  // Defensive copy to prevent UAF from sync recalc. See crbug.com/520167277.
+  const HeapVector<Member<Node>> flat_tree_children = flat_tree_children_;
+  for (auto& node : flat_tree_children) {
     if (!change.TraverseChild(*node))
       continue;
     if (auto* element = DynamicTo<Element>(node.Get()))
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/core/dom/slot_assignment_test.cc b/third_party/blink/renderer/core/dom/slot_assignment_test.cc
index c3c261c..ca5f697f 100644
--- a/third_party/blink/renderer/core/dom/slot_assignment_test.cc
+++ b/third_party/blink/renderer/core/dom/slot_assignment_test.cc
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/core/css/style_engine.h"
 #include "third_party/blink/renderer/core/dom/document.h"
 #include "third_party/blink/renderer/core/dom/element.h"
 #include "third_party/blink/renderer/core/dom/flat_tree_traversal.h"
@@ -149,4 +150,38 @@
             GetDocument().Lifecycle().GetState());
 }
 
+// Regression test for crbug.com/520167277.
+// Verifies that slot assignment recalculation works safely when triggered
+// during layout tree rebuild of slot children.
+TEST_F(SlotAssignmentTest, RebuildDistributedChildrenDuringRecalc) {
+  SetBody(R"HTML(
+    <div id="host">
+      <template shadowrootmode="open">
+        <slot></slot>
+      </template>
+      <div id="child1"></div>
+      <div id="child2"></div>
+    </div>
+  )HTML");
+
+  GetDocument().View()->UpdateAllLifecyclePhasesForTest();
+
+  Element* host = GetDocument().QuerySelector(AtomicString("#host"));
+  ASSERT_NE(nullptr, host);
+  ShadowRoot* shadow_root = host->GetShadowRoot();
+  ASSERT_NE(nullptr, shadow_root);
+  Element* child1 = GetDocument().QuerySelector(AtomicString("#child1"));
+  Element* child2 = GetDocument().QuerySelector(AtomicString("#child2"));
+  ASSERT_NE(nullptr, child1);
+  ASSERT_NE(nullptr, child2);
+
+  // Force layout tree rebuild for children and mark slot assignment dirty.
+  child1->SetForceReattachLayoutTree();
+  child2->SetForceReattachLayoutTree();
+  shadow_root->SetNeedsAssignmentRecalc();
+
+  GetDocument().UpdateStyleAndLayoutTree();
+  EXPECT_FALSE(shadow_root->NeedsSlotAssignmentRecalc());
+}
+
 }  // namespace blink
Loading diff…

Original Bug Report

reported by [email protected]

Potential UAF in HTMLSlotElement due to Incomplete Fix for crbug.com/497830330

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: An incomplete fix for crbug.com/497830330 in HTMLSlotElement leaves sibling loops iterating flat_tree_children_ by reference. If style or layout tree traversal re-enters slot assignment recalculation, the backing vector can be prompt-freed while still being iterated, potentially resulting in a use-after-free. This can potentially be reached via standard layout-tree-rebuild or accessibility-enabled paths.

Affected files:

  • third_party/blink/renderer/core/html/html_slot_element.cc

Estimated timestamp from git blame: 2026-04-02

Root Cause

The defensive fix for crbug.com/497830330 added a local copy of the backing vector only in HTMLSlotElement::DetachLayoutTree:

// third_party/blink/renderer/core/html/html_slot_element.cc
void HTMLSlotElement::DetachLayoutTree(bool performing_reattach) {
  if (SupportsAssignment()) {
    auto* host = OwnerShadowHost();
    // 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) { ... }

However, three other sibling functions in the same file still iterate over flat_tree_children_ directly by reference without a defensive copy:

  1. HTMLSlotElement::AttachLayoutTreeForSlotChildren:
void HTMLSlotElement::AttachLayoutTreeForSlotChildren(AttachContext& context) {
  for (Node* child : flat_tree_children_) {
    child->AttachLayoutTree(context);
  }
}
  1. HTMLSlotElement::RebuildDistributedChildrenLayoutTrees:
void HTMLSlotElement::RebuildDistributedChildrenLayoutTrees(
    WhitespaceAttacher& whitespace_attacher) {
  DCHECK(SupportsAssignment());
  for (const auto& child : base::Reversed(flat_tree_children_)) {
    RebuildLayoutTreeForChild(child, whitespace_attacher);
  }
}
  1. HTMLSlotElement::RecalcStyleForSlotChildren:
void HTMLSlotElement::RecalcStyleForSlotChildren(
    const StyleRecalcChange change,
    const StyleRecalcContext& style_recalc_context) {
  for (auto& node : flat_tree_children_) {
    ...
  }
}

If the inner loop body synchronously re-enters SlotAssignment::RecalcAssignment() for the slot’s containing shadow root while needs_assignment_recalc_ == true, the recalculation will call RecalcFlatTreeChildren() which swaps the active vector:

void HTMLSlotElement::RecalcFlatTreeChildren() {
  DCHECK(SupportsAssignment());
  HeapVector<Member<Node>> old_flat_tree_children;
  old_flat_tree_children.swap(flat_tree_children_);
  ...
}

When old_flat_tree_children goes out of scope, its destruction immediately calls cppgc::subtle::FreeUnreferencedObject(). This is a prompt-free operation that immediately reclaims and invalidates the backing memory by returning it to the allocator free-list, completely bypassing conservative stack scanning. On subsequent loop iterations, dereferencing the freed backing to fetch the next node can trigger a use-after-free.

Potential Trigger Path

Blink prevents recursive slot recalculations in debug builds via the SlotAssignmentRecalcForbiddenScope check, but this check compiles out in release builds, leaving production code vulnerable to synchronous re-entry during layout tree updates.

One potential non-AX path for triggering this is through RebuildDistributedChildrenLayoutTrees:

  1. A style/layout tree update triggers RebuildDistributedChildrenLayoutTrees(whitespace_attacher), which loops over flat_tree_children_ by reference.
  2. In the first iteration, RebuildLayoutTreeForChild calls Element::RebuildLayoutTree on a child element which needs reattachment.
  3. RebuildLayoutTree attempts to find the parent layout object via LayoutTreeBuilderTraversal::ParentLayoutObject(*this) -> LayoutTreeBuilderTraversal::LayoutParent -> FlatTreeTraversal::Parent -> FlatTreeTraversal::TraverseParent.
  4. Since the child’s container is a shadow host, TraverseParent returns node.AssignedSlot().
  5. Node::AssignedSlot() determines that the document is not currently in a slot assignment recalculation phase, and calls root->GetSlotAssignment().RecalcAssignment() synchronously on the owner shadow root.
  6. RecalcAssignment() sets needs_assignment_recalc_ = false and calls DidRecalcAssignedNodes -> RecalcFlatTreeChildren(), which swaps and prompt-frees the backing vector currently being iterated.
  7. On the next loop iteration, the outer loop accesses the prompt-freed memory, leading to a UAF and a potential virtual function call hijack.

(Note: These are potential steps based on static analysis; our tooling does not currently have the capability to execute code to produce a fully functional proof-of-concept.)

Suggested Fix

To remediate this issue, apply a defensive copy of flat_tree_children_ or assigned_nodes_ before iterating in the remaining sibling functions (AttachLayoutTreeForSlotChildren, RebuildDistributedChildrenLayoutTrees, and RecalcStyleForSlotChildren), matching the defensive approach already implemented in HTMLSlotElement::DetachLayoutTree.

Evaluated with Chrome root at commit: d8b226a3be7c9c1ac9240c09e14698866c82e4ac


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
Links in the report