← All issues

[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가 발생하는 것이 드러났습니다. DocumentLazyLoadVideoObserver를 소유하는 구조이므로, observer를 사용하는 작업은 전 과정에서 document를 살아있는 상태로 유지해야 합니다. 이번 수정에서는 clone 전 과정에서 Document에 대한 Ref를 유지하고, lazyLoadVideoObserver() getter에 LIFETIME_BOUND를 추가하여 정적 분석기가 위험 코드를 탐지할 수 있도록 했으며, LazyLoadVideoObserverCanMakeCheckedPtr로 강화했습니다. 코드와 테스트는 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);

총 세 곳의 프로덕션 변경과 함께 강화 조치가 적용되었습니다.

Node::cloneNodecloneNodeInternal 호출 전에 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가 추가되었습니다. LazyLoadVideoObserverfinalCanMakeCheckedPtr로 지정되었으며, m_observerlazyInitialize로 초기화되는 const RefPtr<IntersectionObserver>로 변경되었습니다.

소유 sub-object를 사용하는 작업 전 과정에서 부모 객체에 대한 owning reference를 유지하지 않아, 부모가 해제될 때 sub-object의 참조가 dangling 상태가 되는 패턴.

WebCore는 Ref<T>/RefPtr<T>로 객체의 lifetime을 관리합니다. 이 smart pointer들은 보유 중인 동안 객체를 살아있는 상태로 유지합니다. 임시 expression에서 생성된 Ref — 인라인으로 사용된 protect(x) — 는 해당 statement가 끝나는 세미콜론까지만 유지됩니다.

Documentm_lazyLoadObserver(LazyLoadVideoObserver)를 포함한 여러 helper 객체를 멤버로 소유합니다. Document가 해제되면 이 멤버들도 함께 소멸됩니다. LazyLoadVideoObserver<video> element를 감시하고 viewport와 교차할 때 로딩을 유발하기 위해 IntersectionObserver(m_observer)를 lazy하게 생성합니다. observe()/intersectionObserver() 진입점은 document별 인스턴스를 조회하여 동작합니다.

Node::cloneNodecloneNodeInternal에 위임합니다. cloneNodeInternal은 element의 복사본을 생성하고 초기화를 실행하는데, video element의 경우 clone을 document의 LazyLoadVideoObserver에 등록합니다. LIFETIME_BOUND는 Clang 어노테이션으로, 반환된 참조가 파생된 객체보다 오래 살아남는 호출자를 정적 분석기가 탐지하도록 합니다. CanMakeCheckedPtr/CheckedPtr는 WebKit의 smart pointer 방식으로, 강화된 빌드에서 역참조 시 피참조 객체가 해제되지 않았음을 검증합니다.

이번 취약점은 lifetime 관리 오류로 인한 use-after-free입니다. 소유 sub-object에 대한 참조가 소유자보다 오래 살아남는 구조가 근본 원인입니다.

패치 이전에는 Node::cloneNodeRef를 취득하지 않은 채 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(...) 호출이 해제된 메모리를 역참조하게 됩니다.

🔒

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.

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

🔒

Multiple reusable audit patterns identified for finding sibling lifetime bugs across the DOM and lazy-loading subsystems, with concrete starting points.

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