Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in DOM
DescriptionUse after free in DOM
ComponentDOM
Sub-componentBlink>DOM
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
Reviewer notes: Core mechanism matches commit message. Overstates detail: diff shows assigned_nodes_ being cleared during iteration (backing store hazard), not Node objects themselves being freed; the specific re-entry path is not identified in the diff.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker553121008
Fix commit8e20d98d7df7 (chromium/src) +5/-6
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-08-26 (per vendor advisory)
Fix landed2026-08-31 (public git)
Disclosed2026-09-15
Patch gap15 days — fix public in source before the advisory

Background

Slot assignment recalc
the Blink pass that recomputes which light-DOM nodes are distributed into each HTMLSlotElement of a shadow tree, tracked by the per-document counter slot_assignment_recalc_depth_.
`assigned_nodes_`
the HeapVector<Member<Node>> member of HTMLSlotElement holding the nodes currently assigned to that slot, which is rebuilt (cleared and repopulated) whenever assignment is recalculated.
`Document::IsInSlotAssignmentRecalc()`
a guard queried by slot/style code to decide whether a slot-assignment recalc is already in progress, used to forbid re-entrant recalculation.
Use-after-free (UAF)
dereferencing memory (here, Node pointers or the vector backing store) after it has been freed or reallocated, producing undefined behavior an attacker can steer.

Root Cause Analysis

Document::IsInSlotAssignmentRecalc() returned slot_assignment_recalc_depth_ == 1, so at depth 2 or greater it returned false, and a DCHECK_LE(slot_assignment_recalc_depth_, 1u) encoded the (false) assumption that the depth could never exceed 1. Because the guard reported “not in recalc” during a re-entrant (depth-2) recalculation, code that relies on it to forbid recursive slot assignment failed to block a synchronous, nested recalc. Meanwhile HTMLSlotElement::UpdateFlatTreeNodeDataForAssignedNodes() and a second layout-tree detach loop iterated directly over the member assigned_nodes_, whose elements can trigger style/layout work that re-enters slot assignment and clears or reallocates assigned_nodes_ mid-iteration — invalidating the loop’s references and freeing the Node objects still being dereferenced.

The fix changes the guard to slot_assignment_recalc_depth_ > 0 so any in-progress recalc (including nested ones) is correctly reported, and additionally snapshots assigned_nodes_ into a local const HeapVector<Member<Node>> before each loop so iteration is stable even if a synchronous recalc mutates the member. Together these close both the missing re-entrancy guard and the direct-iteration hazard it exposed.

Key insight
The single core mistake was an exact-equality depth check (== 1) that silently failed for depth ≥ 2, letting re-entrant slot assignment recalc mutate assigned_nodes_ while it was being iterated; the fix generalizes the check to > 0 and iterates over defensive copies so the container cannot be freed out from under the loop.

Attack Path

  1. Set up a slotted shadow tree The attacker builds a shadow host with HTMLSlotElements and assigned light-DOM nodes so that assigned_nodes_ is populated and a slot-assignment recalc will run.
  2. Trigger the outer recalc A DOM/style mutation causes Blink to enter slot assignment recalc, incrementing slot_assignment_recalc_depth_ and beginning to iterate assigned_nodes_ in HTMLSlotElement::UpdateFlatTreeNodeDataForAssignedNodes() (or the layout-tree detach loop).
  3. Force a re-entrant recalc Work performed on an assigned node during iteration (style recalc / layout-tree detach side effects) synchronously re-enters slot assignment at depth 2, where the pre-patch IsInSlotAssignmentRecalc() wrongly returned false and so failed to forbid the nested recalculation.
  4. Free the container mid-iteration The nested recalc clears and rebuilds assigned_nodes_, freeing the Node elements and/or reallocating the vector while the outer loop still holds references into it.
  5. Use the freed memory The outer loop dereferences the now-dangling Node pointer (current->...), yielding a use-after-free that an attacker can shape into a read/write primitive and, from there, renderer compromise (inferred).

Impact Assessment

An attacker who can run script in a page gains a use-after-free in the Blink DOM within the renderer process, exploitable by scripting shadow-DOM slot assignment to re-enter recalculation and free assigned_nodes_ during iteration. The only precondition is the ability to construct and mutate a shadow tree with slotted content, which is available to any web page. The advisory classifies this as a use-after-free in DOM; escalation beyond the renderer (sandbox escape or remote code execution) is not demonstrated by the patch and is marked (inferred).

Changed Functions

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

Files Changed

  • third_party/blink/renderer/core/html/html_slot_element.cc
  • third_party/blink/renderer/core/dom/document.h

Audit Directions

  • Exact-equality depth/counter guards
    Search for state checks written as == 1 (or == N) on re-entrancy or recursion counters where a >= / > 0 comparison is intended; a nested increment silently defeats the guard.
  • Direct iteration over mutable heap containers
    Flag loops that iterate a member HeapVector/HeapHashSet (e.g. assigned_nodes_) while the loop body can synchronously re-enter code that clears or reallocates that same container; prefer a local defensive copy.
  • DCHECK-encoded invariants that can actually break
    Review DCHECK_LE/DCHECK-asserted bounds on depth/size counters, since release builds do not enforce them and a violated assumption (here depth ≤ 1) can turn into a memory-safety bug rather than a crash.
From 8e20d98d7df75cd3b6c721cf0eac302f5495dfd2 Mon Sep 17 00:00:00 2001
From: Mason Freed <masonf@chromium.org>
Date: Mon, 31 Aug 2026 13:37:41 -0700
Subject: [PATCH] Fix slot assignment recalculation depth check and add defensive copies

Document::IsInSlotAssignmentRecalc() previously checked for exact
equality with 1, returning false when slot assignment recalculation
depth was 2 or greater. This could allow re-entrant recalculation to
clear HTMLSlotElement::assigned_nodes_ during iteration.

Update IsInSlotAssignmentRecalc() to check if depth is greater than 0,
and add defensive copies of assigned_nodes_ before iterating in
HTMLSlotElement to prevent use-after-free issues from synchronous
recalculations.

Fixed: 553121008
Change-Id: I172d5f346e3905a18fd9648f5ea1aa97a5330325
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8312942
Auto-Submit: Mason Freed <masonf@chromium.org>
Reviewed-by: David Baron <dbaron@chromium.org>
Commit-Queue: Mason Freed <masonf@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1689217}
---
diff --git a/third_party/blink/renderer/core/dom/document.h b/third_party/blink/renderer/core/dom/document.h
index a16eb0c9..ebbc80e6 100644
--- a/third_party/blink/renderer/core/dom/document.h
+++ b/third_party/blink/renderer/core/dom/document.h
@@ -1992,10 +1992,7 @@
     return slot_assignment_recalc_depth_;
   }
   bool IsInSlotAssignmentRecalc() const {
-    // Since we forbid recursive slot assignement recalc, the depth should be
-    // <= 1.
-    DCHECK_LE(slot_assignment_recalc_depth_, 1u);
-    return slot_assignment_recalc_depth_ == 1;
+    return slot_assignment_recalc_depth_ > 0;
   }
 
   bool IsVerticalScrollEnforced() const { return is_vertical_scroll_enforced_; }
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 589d1a27..c12ef59 100644
--- a/third_party/blink/renderer/core/html/html_slot_element.cc
+++ b/third_party/blink/renderer/core/html/html_slot_element.cc
@@ -302,7 +302,8 @@
 
 void HTMLSlotElement::UpdateFlatTreeNodeDataForAssignedNodes() {
   Node* previous = nullptr;
-  for (auto& current : assigned_nodes_) {
+  const HeapVector<Member<Node>> assigned_nodes = assigned_nodes_;
+  for (auto& current : assigned_nodes) {
     bool mark_parent_slot_changed = false;
     if (!current->NeedsStyleRecalc() && !current->GetLayoutObject()) {
       if (current->IsTextNode() ||
@@ -349,7 +350,8 @@
   // detach the node's layout tree.
   StyleEngine& style_engine = GetDocument().GetStyleEngine();
   StyleEngine::DetachLayoutTreeScope detach_scope(style_engine);
-  for (auto& current : assigned_nodes_) {
+  const HeapVector<Member<Node>> assigned_nodes = assigned_nodes_;
+  for (auto& current : assigned_nodes) {
     if (current->GetForceReattachLayoutTree()) {
       current->DetachLayoutTree();
       // Restore the force-reattach state for when it's no longer display
Loading diff…

Original Bug Report

reported by vm...@google.com

UAF in Document::IsInSlotAssignmentRecalc via strict equality check at depth >= 2

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: Document::IsInSlotAssignmentRecalc() tests strict equality against 1 rather than checking for a depth of at least 1, causing it to return false when the recalc depth is 2 or greater. When release builds encounter nested recalculations, Node::AssignedSlot() fails open and re-enters RecalcAssignment(). This can allow nested recalculations to clear and prompt-free the backing buffer of HTMLSlotElement::assigned_nodes_ while an outer frame is iterating over it by reference, potentially resulting in a use-after-free.

Affected files:

  • third_party/blink/renderer/core/dom/document.h
  • third_party/blink/renderer/core/dom/node.cc
  • third_party/blink/renderer/core/dom/flat_tree_traversal.cc
  • third_party/blink/renderer/core/dom/slot_assignment.cc
  • third_party/blink/renderer/core/html/html_slot_element.cc

Estimated timestamp from git blame: 2018-12-03

Root Cause Analysis

In third_party/blink/renderer/core/dom/document.h (lines 1992-1997), Document::IsInSlotAssignmentRecalc() is defined as:

bool IsInSlotAssignmentRecalc() const {
  // Since we forbid recursive slot assignement recalc, the depth should be <= 1.
  DCHECK_LE(slot_assignment_recalc_depth_, 1u);
  return slot_assignment_recalc_depth_ == 1;
}

The predicate checks slot_assignment_recalc_depth_ == 1 rather than >= 1 (contrast with IsFlatTreeTraversalForbidden() at lines 1985-1987, which checks > 0). The recursion depth counter (Document::slot_assignment_recalc_depth_, line 3140) is unconditionally compiled and incremented whenever SlotAssignment::RecalcAssignment() runs (third_party/blink/renderer/core/dom/slot_assignment.cc:236-237). However, recursion-blocking scopes such as SlotAssignmentRecalcForbiddenScope (third_party/blink/renderer/core/dom/slot_assignment_recalc_forbidden_scope.h:32-43) and the anti-recursion checks in SlotAssignment::RecalcAssignment() (slot_assignment.cc:239-241) are no-ops or DCHECK-only in release builds.

Mechanism of the Failure

When a nested slot assignment recalculation reaches depth 2 (for instance, via FlatTreeTraversal::TraverseParent’s fallback-slot branch at flat_tree_traversal.cc:180, which calls HTMLSlotElement::AssignedNodes() without a depth check), Document::IsInSlotAssignmentRecalc() evaluates to false because 2 == 1 is false.

This causes Node::AssignedSlot() (third_party/blink/renderer/core/dom/node.cc:3546-3553) to fail open:

if (GetDocument().IsInSlotAssignmentRecalc()) {
  // FlatTreeNodeData is not reliable here. Entering slow path.
  return root->AssignedSlotFor(*this);
}
root->GetSlotAssignment().RecalcAssignment(); // Reaches here at depth >= 2

If a shadow root is re-dirtied during nested recalculation (e.g. cross-tree reassignment triggered from HTMLDetailsElement::ManuallyAssignSlots / HTMLSlotElement::Assign), RecalcAssignment() does not early-return and enters depth 3. The re-entrant recalculation calls HTMLSlotElement::WillRecalcAssignedNodes() -> ClearAssignedNodes() (third_party/blink/renderer/core/html/html_slot_element.cc:294-296), which calls assigned_nodes_.clear().

In wtf/vector.h:1688-1691, Vector::clear() executes ShrinkCapacity(0), which triggers an immediate prompt-free of the HeapVectorBacking<Member<Node>> in Oilpan via cppgc::subtle::FreeUnreferencedObject (third_party/blink/renderer/platform/heap/collection_support/heap_vector_backing.h:58-60). Because HTMLSlotElement::DetachDisplayLockedAssignedNodesLayoutTreeIfNeeded() (html_slot_element.cc:352-359) and UpdateFlatTreeNodeDataForAssignedNodes() (:305-336) iterate over assigned_nodes_ by direct reference without a defensive copy, returning to the outer frame resumes iteration over freed memory.

Potential Trigger Steps

(Note: These are potential steps deduced from static analysis; our tooling agent does not have the ability to run dynamic code.)

  1. Environment Setup: Execute in a context where an active AXObjectCacheImpl is maintained (such as on macOS or Android where AriaModalPrunesAXTree is enabled, or with accessibility active).
  2. DOM Configuration:
    • Construct host element H1 with author shadow root R1 containing an assigned slot in a display-locked subtree (content-visibility: hidden).
    • Attach a <details> element D (having UA shadow root R2) under H1.
    • Assign a light child X of D to an R1 slot using r1slot.assign(X).
    • Set up an aria-modal="true" dialog and focus such that accessibility removal triggers an AncestorAriaModalDialog walk across the fallback slot in R2 and the D -> H1 host-child boundary.
  3. Lifecycle Trigger: Initiate slot assignment resolution (e.g., during layout/style update):
    • Depth 1: R1.RecalcAssignment() starts. In DetachDisplayLockedAssignedNodesLayoutTreeIfNeeded(), current->DetachLayoutTree() triggers AX removal.
    • Depth 2: The AX walk calls parent_slot->AssignedNodes(), starting R2.RecalcAssignment(). Inside R2, HTMLDetailsElement::ManuallyAssignSlots() reassigns X, calling DidSlotChange() on R1’s slot and marking R1 dirty again.
    • Depth 3: AX removal in R2 traverses the host-child boundary to D.AssignedSlot(). At depth 2, IsInSlotAssignmentRecalc() returns false, invoking R1.RecalcAssignment() re-entrantly.
    • UAF: Depth 3 prompt-frees R1’s assigned_nodes_ backing buffer. When the stack unwinds to depth 1, the range-for loop in DetachDisplayLockedAssignedNodesLayoutTreeIfNeeded() dereferences freed memory.

Impact

Use-after-free in the sandboxed Blink renderer process on a prompt-freed HeapVectorBacking<Member<Node>>. MiraclePtr does not protect Oilpan heap vectors. This presents a potential remote code execution (RCE) primitive in the renderer process.

Suggested Fix

  1. Correct the predicate in third_party/blink/renderer/core/dom/document.h to check for depth >= 1:
    bool IsInSlotAssignmentRecalc() const {
      DCHECK_LE(slot_assignment_recalc_depth_, 1u);
      return slot_assignment_recalc_depth_ >= 1;
    }
    
  2. As defense-in-depth, take defensive copies before iterating over assigned_nodes_ in HTMLSlotElement::DetachDisplayLockedAssignedNodesLayoutTreeIfNeeded() and HTMLSlotElement::UpdateFlatTreeNodeDataForAssignedNodes(), matching the existing defensive copies in AttachLayoutTreeForSlotChildren(), DetachLayoutTree(), RebuildDistributedChildrenLayoutTrees(), and RecalcStyleForSlotChildren() (html_slot_element.cc:408, 418, 444, 555).

Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c


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