b374b0cf7bdfc43b53032006f49af793f41477a6 [M153] Fix slot assignment recalculation depth check and add defensive copies
The bug at a glance
An off-by-one depth check (== 1 instead of > 0) let re-entrant slot-assignment recalculation at depth >= 2 escape the IsInSlotAssignmentRecalc() guard and clear HTMLSlotElement::assigned_nodes_ while it was being iterated, a use-after-free in core DOM slot/flat-tree handling reachable from script via shadow DOM; this is a cherry-picked (M153) fix referencing a security bug, consistent with a renderer-exploitable UAF.
An attacker-controlled page using shadow DOM slots can drive synchronous re-entrant slot assignment recalc to free/clear the assigned_nodes_ collection mid-iteration, yielding a use-after-free the defender must treat as a potential renderer RCE primitive.
Root cause
Document::IsInSlotAssignmentRecalc() tested slot_assignment_recalc_depth_ == 1 (with a DCHECK that it is <= 1), so when recalc recursed to depth 2 or more the function wrongly returned false. Code relying on that guard to forbid re-entrant recalc was then permitted to run, allowing a nested recalculation to clear HTMLSlotElement::assigned_nodes_ while an outer loop was still iterating over it, causing a use-after-free. The fix changes the test to depth > 0 so any nested recalc is correctly reported as in-progress, and HTMLSlotElement takes const defensive copies of assigned_nodes_ before iterating in UpdateFlatTreeNodeDataForAssignedNodes and the detach loop, so synchronous recalc that mutates the member cannot invalidate the iteration.
Key code
Correct depth guard plus defensive copy of assigned_nodes_ before iterating
bool IsInSlotAssignmentRecalc() const {
return slot_assignment_recalc_depth_ > 0;
}
...
const HeapVector<Member<Node>> assigned_nodes = assigned_nodes_;
for (auto& current : assigned_nodes) {
Patch walkthrough
third_party/blink/renderer/core/dom/document.h— IsInSlotAssignmentRecalc() changed from an exact== 1check (which returned false at depth >= 2 and carried a DCHECK_LE(..,1)) to> 0. The old check failed to report re-entrant recalc, letting guarded code run during nested recalculation and mutate assigned_nodes_ during iteration.third_party/blink/renderer/core/html/html_slot_element.cc— Two loops over assigned_nodes_ (UpdateFlatTreeNodeDataForAssignedNodes and the detach-layout-tree loop) now iterate over a local const HeapVector copy. This defends against synchronous re-entrant recalc clearing/reallocating the member mid-iteration, directly preventing the use-after-free.
Background
Slot assignment recalc — Blink process that recomputes which shadow-DOM nodes are assigned to elements and rebuilds flat-tree data.
HTMLSlotElement::assigned_nodes_ — A HeapVector of nodes currently assigned to a slot, iterated while updating flat-tree/layout state.
Re-entrant recalc guard — IsInSlotAssignmentRecalc() is meant to signal that a recalc is already running so code avoids triggering nested, overlapping recalculation.
Flat tree — The composed tree formed by distributing shadow host children into slots, used for style and layout.
Vulnerability window
- Reachability — Reachable from untrusted script that creates shadow roots with slots and mutates assignments.
- Trigger — Script induces a synchronous nested slot-assignment recalc so recalc depth reaches 2+, where the old
== 1guard returned false. - Primitive — The nested recalc clears/reallocates HTMLSlotElement::assigned_nodes_ while an outer loop still iterates over it, producing a dangling Member access (use-after-free).
- Escalation — A controlled UAF in the DOM heap is a classic stepping stone to type confusion / arbitrary read-write and renderer RCE (inferred).
Proof of concept
Conceptually: build a shadow tree with slotted nodes, then from within a callback that fires during flat-tree update (e.g. a mutation that forces slot reassignment synchronously) trigger a second slot-assignment recalc so depth exceeds 1; the inner recalc clears assigned_nodes_ while the outer UpdateFlatTreeNodeDataForAssignedNodes loop is iterating it, dereferencing freed node storage. No working exploit is provided.
Exploitation
- Setup — Attacker page constructs shadow DOM with slots and arranges a synchronous re-entrancy point during flat-tree recalculation.
- Trigger UAF — Nested recalc clears assigned_nodes_ mid-iteration, freeing node backing while it is still referenced (inferred specifics).
- Shape heap — Reclaim the freed slot/node allocation with attacker-controlled data to convert the dangling access into a controlled read/write (inferred).
- Escalate — Leverage the primitive toward renderer code execution and then sandbox-escape chaining (inferred).
Detection & hunting
For defenders and detection engineers:
- grep for
== 1depth/recursion guards and reentrancy counters compared with exact equality — Audit guards that should mean ‘any nesting’ but use exact-equality; prefer> 0. - weggli for
for (auto& x : member_vector_)where the loop body can trigger recalc/callbacks that mutate member_vector_ — Flag iteration over heap member collections across code that can re-enter and mutate them; recommend const local copies.
Audit directions
- Other iterations of assigned_nodes_ / flat-tree collections in HTMLSlotElement and SlotAssignment — Find remaining loops over slot/flat-tree member vectors that lack defensive copies and could re-enter recalc.
- Other depth/recursion guards in DOM and style engine — Review similar depth counters (style recalc, layout, event dispatch) for the same exact-equality mistake that misreports nested state.