← 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

HashMap rehash-during-iteration UAF를 수정하는 패치이므로 High로 평가합니다. for (auto& slot : m_slots.values()) 루프 내부에서 호출된 lazy-cache helper가 m_slots에 새 항목을 다시 삽입해 루프의 value reference를 무효화합니다. 이후 slot->seenFirstElement = true 저장과 WTF::move(slot->element) 호출은 해제된 메모리를 통해 쓰기를 수행합니다.

NamedSlotAssignment::resolveSlotsAfterSlotMutationm_slots.values()를 순회합니다. 루프 내부에서 hasAssignedNodes(shadowRoot, *slot)이 lazy하게 assignSlots(shadowRoot)를 호출하며, 이 함수는 아직 등록되지 않은 slot 이름에 대해 m_slots에 새 항목을 삽입합니다. 삽입이 발생할 때마다 rehash가 일어나 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 호출이 순회 시작 전 한 번만 실행되도록 앞으로 이동되었습니다(m_slotAssignmentsIsValid가 false일 때만). 이를 통해 순회가 진행되는 동안 map이 최종 레이아웃을 유지합니다. 루프 내부의 hasAssignedNodes 호출은 m_slots를 변경하지 않는 !slot->assignedNodes.isEmpty() 필드 직접 읽기로 대체되었습니다.

순회 도중 컨테이너의 mutation 경로로 재진입하는 helper에 의한 HashMap iterator 무효화.

NamedSlotAssignmentattachShadow({mode: 'open' | 'closed'})로 선언된 shadow tree의 기본 slot-assignment 전략입니다. m_slots라는 HashMap<AtomString, std::unique_ptr<Slot>>을 관리하며, 각 Slot에는 활성 <slot> element, oldElement, assignedNodes 목록, 그리고 slot별 플래그가 기록됩니다. m_slotAssignmentsIsValid는 캐시가 오래된 상태일 수 있을 때 초기화되는 dirty bit입니다. assignSlots(shadowRoot)는 host child를 순회하며 새로운 slot 이름에 해당하는 m_slots 항목을 삽입합니다. HashMap 삽입 시 rehash가 발생하면 iterator와 reference가 모두 무효화됩니다.

hasAssignedNodes는 이름만 보면 읽기 전용처럼 보이지만, 실제로는 assignSlots를 호출해 m_slots에 항목을 삽입하는 mutation 동작을 수행합니다. 컨테이너를 직접 변경해 캐시를 lazy하게 채우는 predicate는, 동일한 컨테이너를 순회하는 도중 호출될 경우 위험합니다.

회귀 테스트 기반의 exploit 구성은 다음과 같습니다. 먼저 default named-slot assignment 방식으로 shadow root를 attach합니다. 이후 slot element와 매칭되는 host child를 추가해 m_slotAssignmentsIsValid를 false로 만듭니다. 다음으로 m_slots에 아직 등록되지 않은 새로운 slot 이름을 가진 host child를 대량으로 추가합니다. shadow subtree에서 container.replaceChildren()을 호출해 resolveSlotsAfterSlotMutation을 발생시킵니다. 루프 내부에서 첫 번째 hasAssignedNodes 호출이 assignSlots를 실행하고, 이 함수가 새 이름마다 HashMap::add를 반복 호출해 rehash를 유발합니다. 이 시점에서 루프의 slot reference와 values() iterator는 이미 해제된 table storage를 가리키게 됩니다. 이후 slot->seenFirstElement = true 접근과 WTF::move(slot->element) 호출은 재배치된 unique_ptr<Slot> storage에 대한 UAF입니다. heap grooming을 통해 해제된 backing storage를 공격자가 제어하는 데이터로 재점유하면, RefPtr<HTMLSlotElement> element를 포함한 Slot 타입 필드에 대한 read/write로 전환할 수 있습니다.

이 vulnerability는 WebContent renderer의 memory safety를 약화시킵니다. Shadow DOM 명세는 slot resolution이 임의의 DOM mutation entry point에서 재진입 방식으로 호출되더라도 내부 상태에 위험한 영향을 주지 않는다고 가정합니다.