← All issues

[9] Highlight Re-entrancy UAF via Cross-Document Ownership Cycle

Severity: High | Component: WebCore CSS Custom Highlight API | e01d254

Highlight::clearFromSetLike에서 ASAN이 감지한 use-after-free가 관측 가능한 결과입니다. 이 버그는 cross-document ownership cycle에 의해 유발됩니다. 재현 가능한 trigger와 WebKit의 잘 알려진 destruction-time exploitation 패턴을 고려하면, heap grooming을 통한 controlled UAF primitive로의 확장은 단순하지 않지만 confidence 0.92로 예측됩니다.

특정 dispose 순서를 구성하면 Highlight::clearFromSetLike()에 재진입을 유발할 수 있습니다. Document→HighlightRegistry→Highlight→HighlightRange→Range→Document로 이어지는 ownership 관계가 원인입니다.

Source/WebCore/Modules/highlight/Highlight.cpp

void Highlight::clearFromSetLike()
{
for (auto& highlightRange : std::exchange(m_highlightRanges, { }))
repaintRange(highlightRange->range());
- m_highlightRanges.clear();
+
+ // m_highlightRanges의 내용이 해제될 때 이 함수가 재진입될 수 있으므로, 빈 vector와 교체합니다.
+ std::exchange(m_highlightRanges, { });
}

LayoutTests/highlight/highlight-crash-2.html

+ function main() {
+ let d = f.contentDocument;
+ let hr = f.contentWindow.CSS.highlights;
+ let h = new Highlight(d.createRange());
+ g.contentWindow.CSS.highlights.set("g", h);
+ f.remove();
+ hr.set("f", h);
+ hr = 0;
+ d = 0;
+ gc();
+ g.remove();
+ }

이 fix는 Highlight::clearFromSetLike() 끝부분의 m_highlightRanges.clear() 호출을 std::exchange(m_highlightRanges, { })로 교체합니다. for-loop에서는 이미 std::exchange를 사용해 m_highlightRanges를 loop-scoped 임시 변수로 이동한 뒤 순회하고 있었습니다. 이 fix를 통해, loop 이후 임시 변수의 Ref<HighlightRange> 원소가 해제되어 re-entrant document teardown이 유발될 수 있는 시점에, m_highlightRanges.clear()로 제자리에서 소거되는 대신 atomic하게 빈 vector로 교체됩니다. 결과적으로 clearFromSetLike()에 대한 재진입 호출은 안전한 no-op이 됩니다.

CSS Custom Highlight API(CSS.highlights)는 웹 콘텐츠가 이름이 있는 Highlight 객체를 등록할 수 있게 합니다. 각 HighlightAbstractRange의 집합을 담고 있습니다. HighlightRegistry는 Document마다 존재하며 Ref<Highlight> 참조를 보유합니다. 하나의 Highlight 객체는 서로 다른 Document나 iframe의 여러 HighlightRegistry 인스턴스에 동시에 등록될 수 있는데, 이 cross-document 등록이 ownership cycle을 만드는 원인입니다.

Range 객체는 자신이 속한 Document에 대한 Ref<Document>를 보유합니다. Document가 Document::commonTeardown()을 통해 소멸될 때, HighlightRegistry가 정리되면서 등록된 각 Highlight의 clearFromSetLike()가 호출됩니다. std::exchange(member, {})는 멤버의 내용을 임시 변수로 atomic하게 이동하고, 멤버를 기본 생성값으로 교체합니다. 이는 WebKit에서 안전한 re-entrancy를 위해 사용하는 일반적인 패턴으로, 재귀 호출 시 멤버가 빈 상태로 보이도록 보장합니다.

ownership chain은 Document → HighlightRegistry → Highlight → HighlightRange → Range → Document의 순환 구조를 형성합니다. chain의 한쪽 끝에서 어떤 link의 소멸이 반대편 Document의 마지막 참조를 해제하면, 전체 cycle이 re-entrant하게 해제될 수 있습니다.

근본 원인은 cyclic ownership chain에서의 re-entrancy hazard입니다. fix 이전에는 clearFromSetLike()std::exchangem_highlightRanges를 loop-scoped 임시 변수로 이동한 뒤 repaintRange()를 호출하며 순회했고, 이후 m_highlightRanges.clear()를 호출했습니다. 문제는 loop-scoped 임시 변수가 소멸되면서 Ref<HighlightRange> 원소가 해제될 때 발생합니다. 이 소멸 chain이 Range의 Document에 대한 마지막 참조를 해제할 수 있고, 이로 인해 Document::commonTeardown()HighlightRegistry::clear() → 동일 Highlight 객체의 Highlight::clearFromSetLike() 호출이 연쇄적으로 이어집니다. 단, 이는 해당 Highlight가 여러 Document의 registry에 등록된 경우에 한합니다.

테스트 케이스가 이 과정을 정확히 보여줍니다. iframe f의 Document에서 가져온 Range를 보유하는 Highlight가 fg 양쪽의 CSS.highlights에 등록됩니다. f를 제거하면 Range의 Document가 분리됩니다. 이후 g를 제거하면 gDocument::commonTeardown()이 실행되어 g의 HighlightRegistry가 정리되고, 공유된 Highlight의 clearFromSetLike()가 호출됩니다. for-loop에서 원소가 소멸되는 과정에서 f의 Document가 마지막 참조를 잃고 소멸되며, 이로 인해 f의 HighlightRegistry가 정리되면서 동일한 Highlight에 대해 clearFromSetLike()가 재진입됩니다.

기존 코드의 .clear() 호출은 vector가 this의 멤버인 상태에서 제자리에서 원소를 소멸시킵니다. 원소 소멸 중 re-entrancy가 발생하여 Highlight 자체가 마지막 참조를 잃게 되면(clear() 도중 HighlightRegistry가 Ref를 해제할 때), 이후의 원소 소멸은 이미 해제된 메모리에 접근하게 됩니다.

🔒

상세 취약점 분석, 공격 가능성 평가, 보안 영향 분석이 포함되어 있습니다

더 확인하려면 구독해 주세요

🔒

이 취약점 패턴의 변종을 찾기 위한 구체적인 탐색 방향이 포함되어 있습니다

더 확인하려면 구독해 주세요