CVE-2026-19144
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fthird_party/blink/renderer/core/dom/slot_assignment_test.cc |
modified | |
forthird_party/blink/renderer/core/html/html_slot_element.cc |
modified |
Files Changed
third_party/blink/renderer/core/dom/slot_assignment_test.ccthird_party/blink/renderer/core/html/html_slot_element.cc
Patch
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()))
Regression Test / PoC
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
Original Bug Report
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:
HTMLSlotElement::AttachLayoutTreeForSlotChildren:
void HTMLSlotElement::AttachLayoutTreeForSlotChildren(AttachContext& context) {
for (Node* child : flat_tree_children_) {
child->AttachLayoutTree(context);
}
}
HTMLSlotElement::RebuildDistributedChildrenLayoutTrees:
void HTMLSlotElement::RebuildDistributedChildrenLayoutTrees(
WhitespaceAttacher& whitespace_attacher) {
DCHECK(SupportsAssignment());
for (const auto& child : base::Reversed(flat_tree_children_)) {
RebuildLayoutTreeForChild(child, whitespace_attacher);
}
}
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:
- A style/layout tree update triggers
RebuildDistributedChildrenLayoutTrees(whitespace_attacher), which loops overflat_tree_children_by reference. - In the first iteration,
RebuildLayoutTreeForChildcallsElement::RebuildLayoutTreeon a child element which needs reattachment. RebuildLayoutTreeattempts to find the parent layout object viaLayoutTreeBuilderTraversal::ParentLayoutObject(*this)->LayoutTreeBuilderTraversal::LayoutParent->FlatTreeTraversal::Parent->FlatTreeTraversal::TraverseParent.- Since the child’s container is a shadow host,
TraverseParentreturnsnode.AssignedSlot(). Node::AssignedSlot()determines that the document is not currently in a slot assignment recalculation phase, and callsroot->GetSlotAssignment().RecalcAssignment()synchronously on the owner shadow root.RecalcAssignment()setsneeds_assignment_recalc_ = falseand callsDidRecalcAssignedNodes->RecalcFlatTreeChildren(), which swaps and prompt-frees the backing vector currently being iterated.- 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.