← All issues

[13] NamedSlotAssignment iterator-invalidation UAF

A predicate named hasAssignedNodes lazy-filled m_slots and rehashed it mid-iteration — the loop kept writing through freed Slot storage.

Severity: High | Component: WebCore Shadow DOM | 20beac1

Rated High because the diff fixes a HashMap rehash-during-iteration UAF: a lazy-cache helper called from inside a for (auto& slot : m_slots.values()) loop reinserts into m_slots, invalidating the loop's value reference; subsequent stores like slot->seenFirstElement = true and WTF::move(slot->element) write through freed memory.

NamedSlotAssignment::resolveSlotsAfterSlotMutation iterated m_slots.values(). Inside the loop, hasAssignedNodes(shadowRoot, *slot) lazily called assignSlots(shadowRoot), which inserts new entries into m_slots for unseen slot names — every insertion can rehash and relocate value storage.

Source/WebCore/dom/SlotAssignment.cpp

+ if (!m_slotAssignmentsIsValid)
+ assignSlots(shadowRoot);
+
for (auto& slot : m_slots.values()) {
if (slot->seenFirstElement)
continue;
...
slot->seenFirstElement = true;
ASSERT(slot->element);
- if (hasAssignedNodes(shadowRoot, *slot))
+ if (!slot->assignedNodes.isEmpty())
slot->oldElement = WTF::move(slot->element);

assignSlots is hoisted to run once before the iteration (only when m_slotAssignmentsIsValid is false), so the map is in its final layout for the duration. The in-loop hasAssignedNodes call is replaced with a direct !slot->assignedNodes.isEmpty() field read that cannot mutate m_slots.

HashMap iterator invalidation by a helper that re-enters the container's mutation path during traversal.

NamedSlotAssignment is the default slot-assignment strategy for shadow trees declared attachShadow({mode: 'open' | 'closed'}). It maintains m_slots, a HashMap<AtomString, std::unique_ptr<Slot>>. Each Slot records the active <slot> element, an oldElement, an assignedNodes list, and per-slot flags. m_slotAssignmentsIsValid is a dirty bit cleared when the cache may be stale. assignSlots(shadowRoot) walks host children and inserts m_slots entries for new slot names. HashMap insertion can rehash, invalidating iterators and references.

The query-named helper (hasAssignedNodes) was actually mutation-shaped — it called assignSlots which inserts into m_slots. Any predicate that lazily fills a cache by mutating a container is a hazard when invoked during iteration of that same container.

Exploit shape (from the regression test): attach a shadow root with default named-slot assignment; add a slot element + matching host child to set m_slotAssignmentsIsValid = false; append many host children with fresh slot names not yet keys in m_slots; trigger resolveSlotsAfterSlotMutation (the PoC uses container.replaceChildren() on the shadow subtree). Inside the loop, the first hasAssignedNodes call invokes assignSlots, which calls HashMap::add repeatedly for each fresh name and triggers a rehash. The loop's slot reference and values()-iterator now point into freed table storage; the next field access (slot->seenFirstElement = true, then WTF::move(slot->element)) is a UAF on the relocated unique_ptr<Slot> storage. With heap grooming an attacker could attempt to reclaim the freed backing with attacker-shaped data, converting the UAF into a read/write of Slot-typed fields including RefPtr<HTMLSlotElement> element.

This vulnerability weakens memory safety in the WebContent renderer. The Shadow DOM spec assumes slot resolution can be invoked reentrantly from arbitrary DOM-mutation entry points without producing unsafe internal state.