[1] LazyLoadVideoObserver use-after-free on cloning a video in a torn-down document
Severity: High | Component: WebCore DOM / HTML lazy-loading | b74177d
Renderer 내부에서 web-reachable use-after-free를 수정하는 패치이기 때문에 High로 평가됩니다. 분리된 GC 대상 document의 video element에 cloneNode()를 호출하면 이미 해제된 Document 소유의 LazyLoadVideoObserver에 도달하며, 역참조는 매번 재현 가능한 attacker 제어 트리거를 통해 발생합니다. 더 강력한 primitive로의 확장을 위해서는 heap grooming 하에서 해제된 TZone slot을 재사용해야 합니다. 해당 객체의 attacker 제어 필드가 제한적이어서 쉬운 작업은 아니지만, 완전히 막혀있지도 않습니다.
REGRESSION(314874@main) crash 보고를 통해, 소유 Document가 조기 해제된 이후 LazyLoadVideoObserver::observe()에서 LazyLoadVideoObserver의 use-after-free가 발생하는 것이 드러났습니다. Document가 LazyLoadVideoObserver를 소유하는 구조이므로, observer를 사용하는 작업은 전 과정에서 document를 살아있는 상태로 유지해야 합니다. 이번 수정에서는 clone 전 과정에서 Document에 대한 Ref를 유지하고, lazyLoadVideoObserver() getter에 LIFETIME_BOUND를 추가하여 정적 분석기가 위험 코드를 탐지할 수 있도록 했으며, LazyLoadVideoObserver를 CanMakeCheckedPtr로 강화했습니다. 코드와 테스트는 Kristian Monsen의 초기 작업을 기반으로 합니다.
Source/WebCore/dom/Node.cpp
Ref<Node> Node::cloneNode(bool deep) const
{
RefPtr registry = CustomElementRegistry::registryForNodeOrTreeScope(*this, treeScope());
- return cloneNodeInternal(document(), deep ? CloningOperation::Everything : CloningOperation::SelfOnly, registry.get());
+ return cloneNodeInternal(protect(document()), deep ? CloningOperation::Everything : CloningOperation::SelfOnly, registry.get());
}
Source/WebCore/html/LazyLoadVideoObserver.cpp
void LazyLoadVideoObserver::observe(HTMLVideoElement& element)
{
- auto& observer = protect(element.document())->lazyLoadVideoObserver();
- RefPtr intersectionObserver = observer.intersectionObserver(protect(element.document()));
- if (!intersectionObserver)
- return;
- intersectionObserver->observe(element);
+ Ref document = element.document();
+ if (RefPtr intersectionObserver = protect(document->lazyLoadVideoObserver())->intersectionObserver(document))
+ intersectionObserver->observe(element);
}
Source/WebCore/dom/Document.h
- LazyLoadVideoObserver& lazyLoadVideoObserver();
+ LazyLoadVideoObserver& lazyLoadVideoObserver() LIFETIME_BOUND;
LayoutTests/fast/dom/lazy-video-clone-after-document-teardown-crash.html
+function createVideoInDetachedDocument() {
+ const document = Document.parseHTMLUnsafe("");
+ return document.createElementNS("http://www.w3.org/1999/xhtml", "video");
+}
+ const video = createVideoInDetachedDocument();
+ await new Promise(resolve => setTimeout(resolve, 0));
+ GCController.collect();
+ video.cloneNode(false);
Patch Details
총 세 곳의 프로덕션 변경과 함께 강화 조치가 적용되었습니다.
Node::cloneNode는 cloneNodeInternal 호출 전에 document를 protect(document())로 감싸도록 변경되어, clone 전 과정에 걸쳐 Ref<Document>를 유지합니다.
LazyLoadVideoObserver::observe는 함수 범위의 단일 Ref document = element.document()를 선언하도록 재작성되었습니다. intersectionObserver(document) 호출 전에 document 소유 observer를 protect(...)로 감싸는데, 타입이 CanMakeCheckedPtr로 변경되었으므로 이제 CheckedRef가 됩니다.
기존 코드는 auto& observer = protect(element.document())->lazyLoadVideoObserver(); 형태로 statement 범위의 임시 Ref 하에서 observer 참조를 획득한 뒤, 다음 statement에서 observer를 사용했습니다.
Document::lazyLoadVideoObserver()에는 LIFETIME_BOUND가 추가되었습니다. LazyLoadVideoObserver는 final과 CanMakeCheckedPtr로 지정되었으며, m_observer는 lazyInitialize로 초기화되는 const RefPtr<IntersectionObserver>로 변경되었습니다.
소유 sub-object를 사용하는 작업 전 과정에서 부모 객체에 대한 owning reference를 유지하지 않아, 부모가 해제될 때 sub-object의 참조가 dangling 상태가 되는 패턴.
Background
WebCore는 Ref<T>/RefPtr<T>로 객체의 lifetime을 관리합니다. 이 smart pointer들은 보유 중인 동안 객체를 살아있는 상태로 유지합니다. 임시 expression에서 생성된 Ref — 인라인으로 사용된 protect(x) — 는 해당 statement가 끝나는 세미콜론까지만 유지됩니다.
Document는 m_lazyLoadObserver(LazyLoadVideoObserver)를 포함한 여러 helper 객체를 멤버로 소유합니다. Document가 해제되면 이 멤버들도 함께 소멸됩니다. LazyLoadVideoObserver는 <video> element를 감시하고 viewport와 교차할 때 로딩을 유발하기 위해 IntersectionObserver(m_observer)를 lazy하게 생성합니다. observe()/intersectionObserver() 진입점은 document별 인스턴스를 조회하여 동작합니다.
Node::cloneNode는 cloneNodeInternal에 위임합니다. cloneNodeInternal은 element의 복사본을 생성하고 초기화를 실행하는데, video element의 경우 clone을 document의 LazyLoadVideoObserver에 등록합니다. LIFETIME_BOUND는 Clang 어노테이션으로, 반환된 참조가 파생된 객체보다 오래 살아남는 호출자를 정적 분석기가 탐지하도록 합니다. CanMakeCheckedPtr/CheckedPtr는 WebKit의 smart pointer 방식으로, 강화된 빌드에서 역참조 시 피참조 객체가 해제되지 않았음을 검증합니다.
Analysis
이번 취약점은 lifetime 관리 오류로 인한 use-after-free입니다. 소유 sub-object에 대한 참조가 소유자보다 오래 살아남는 구조가 근본 원인입니다.
패치 이전에는 Node::cloneNode가 Ref를 취득하지 않은 채 document() — 즉 bare Document& — 를 cloneNodeInternal에 전달했습니다. 따라서 복제된 video element가 생성되고 lazy loading에 등록되는 동안 stack 위에서 document를 살아있는 상태로 유지하는 것이 없었습니다.
video element를 clone하면 LazyLoadVideoObserver::observe(element)에 도달합니다. 이 함수는 auto& observer = protect(element.document())->lazyLoadVideoObserver(); 형태로 observer 참조를 획득했습니다. 여기서 protect(element.document())는 세미콜론에서 해제되는 statement 범위의 임시 Ref<Document>만 생성하는 반면, observer는 document 소유 객체에 대한 장기적인 참조입니다.
보호하던 Ref가 사라진 상태에서, document가 트리거를 통해 분리된 GC 대상이면 Document가 해제되면서 내장된 LazyLoadVideoObserver도 함께 소멸됩니다. 이후 observer.intersectionObserver(...) 호출이 해제된 메모리를 역참조하게 됩니다.
Aaaaaaaaaa Aaaa Aa Aaa Aaa Aaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaaaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaa Aa a Aa Aa Aaa Aaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaa Aaa a Aaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaa Aaaaa Aaaa a Aaaaa Aaaaaaaa Aaaa a Aaa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaa Aa Aa Aaaaa Aaaaaaaaa Aaa Aa Aaaaaa Aaa Aaaa Aaaaaaaa Aa Aaaaaaaaa Aa Aaa Aaaaaaa
Aa Aa Aaa Aaaa Aaaaaaaaaaaaa Aaaaaaaa Aaaa Aaa a Aaa Aaaaaaa Aaaa Aa Aaaaaaa Aaaaaaaaaa Aaaaaa Aaa Aaa Aaaa Aaa Aa Aaaa Aaaaaaaaa Aa Aaa Aaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaa Aaaa Aaaaa Aaaa Aaa a Aaaaaaaaaaaaa Aaa Aaa Aaaaaaaa Aa Aaaa Aaaaaa Aa Aaa Aaa Aaaa Aaaaa Aa Aaaaaaaaa Aa Aaa a Aa Aaa Aaaaaaa Aa Aaa Aa Aaa Aa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaa Aaaaaaaaaa Aaaaaaaaaa Aaaaaa Aaaaa Aa Aaaa Aa a Aaaaa Aaaa Aaa Aa Aaaaa
a Aaaa Aaaaaaaaaa Aaaaaaaa Aaaaaaa Aaa Aaa Aaaa Aaaaaaa Aaa Aaaaaaaa Aaa Aaaaaaaaaaa Aaa Aaaaaaaaaaa Aaaa Aa Aaaa Aa Aaaaa Aaa Aaaaaa Aa Aaaa a Aa Aaa Aaaaa Aaa Aaaaaaaaa Aaaaa Aaaaaaaa Aaaaaaaaa Aaaa Aaaaaaaaaaaaaa Aaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaaa Aaa a Aaaaaa Aaa Aaa Aaa Aaaa Aaa Aaaaaaa Aaaaaaa Aaa Aaaaaa
Aaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aa Aaa Aaa Aaaaaaaaaa Aaa Aa Aaaa Aaaaaa Aaaaaa Aa Aaaaaaaa Aa Aaa Aa Aaa Aaaa Aaaaaaaa Aaaa Aaaaaaaa Aaa Aaaaaaaaa Aaaa Aa Aaa Aaaa Aa Aaa Aa Aaaaa Aaaa
Aaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaaa Aaa Aaaaaaaaa Aaa Aaaaa Aaa Aaa Aaaaaaa Aaaaaaaaaaaa Aaa Aa Aaa Aaaaaa Aaaa Aa Aa Aaaa Aaa Aaaaa Aaaaaa Aa Aaaa Aaa Aaaaa Aaaa Aaaaaaaa Aaa Aaaaa Aaa Aa Aaa Aaaa Aaaaaaa
🔒The ownership and lifetime model behind this crash is traced end to end, with an assessment of whether the freed object can be turned into more than a crash.
더 확인하려면 구독해 주세요
Audit directions
a Aaaaaaaaaaaaaaa Aa Aaa Aa Aaa Aaaa Aaaaaa Aa Aaaaaaaaa Aaa Aaaaaaaaa Aaa Aa Aa Aaa Aaaaa Aaaaa Aaaa Aaaaaaaaaaaaaaa Aaa Aaaa Aaaaa Aaaa Aaa a Aaa Aaa Aaaaaaaa Aaa Aaaa Aa Aaa Aa Aaa Aaaa Aa Aaaaaaaaaaaaaaaaaaaaaaaaa a Aaaaaaaaaaaaaaaaa Aaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaa Aaaaaaaa Aaaaa Aaa Aa Aaaaaa Aaaaaaaaaaaaaaaa Aa Aaa Aaa Aaa Aaaa Aa Aaa Aaa Aaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa a Aaaaaaaaaaaaaaaaaaaaaaa Aa Aa Aaa Aaa Aaaa Aaa Aaaa Aaa Aaaaaaa
a Aaaaaaaaaaaaaaa Aaaa Aaaaaaaaaa Aaaaaaa Aaaa Aaaa Aaaaaaa Aaaa Aaa Aa Aaaaaaaaaa Aaaaaa Aaaaaaa Aaaaaaaaaa a Aaaa a Aaaa Aaaaaaaaaaaaaaaa Aaaaa Aaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaa Aaaaaaaaaaaaaaaaaaaaaa Aaaa Aaaaaaaaa Aaaa Aa a Aaaaaaaa Aa Aaaaaaaaa Aaaa Aaaaaaaaaaaaaaa Aa Aaa Aaa Aaa Aaa Aaaaa
a Aaaaaaaaaaaaaaaaaaa Aaa Aaa Aaa Aaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa a Aa Aaaa Aa Aaa Aaaaa Aaaa Aaaa Aaaaa Aaaa Aa Aaa Aaa a Aaa Aaaaaaaaaaaaaaaaa Aaa Aaaa Aaaa Aa Aaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaa Aaaa Aa Aaaaaa
🔒Multiple reusable audit patterns identified for finding sibling lifetime bugs across the DOM and lazy-loading subsystems, with concrete starting points.
더 확인하려면 구독해 주세요