[1] LazyLoadVideoObserver use-after-free on cloning a video in a torn-down document
A statement-scoped `protect()` kept the document alive for one line; `cloneNode()` on a detached video reached its freed observer on the next.
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
Source/WebCore/html/LazyLoadVideoObserver.cpp
Source/WebCore/dom/Document.h
LayoutTests/fast/dom/lazy-video-clone-after-document-teardown-crash.html
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(...) 호출이 해제된 메모리를 역참조하게 됩니다.
regression 테스트는 재현 방법을 명확히 보여줍니다. Document.parseHTMLUnsafe("")로 분리된 document를 생성하고, createElementNS(...)로 <video>를 만든 뒤 모든 JS 참조를 제거합니다. GCController.collect()를 실행한 후 video.cloneNode(false)를 호출합니다. clone 과정에서 새 video element가 생성되고 그 초기화 과정이 LazyLoadVideoObserver::observe에 도달합니다. 패치 이전 코드에서는 statement 범위의 보호 Ref가 해제된 이후에도 Document 소유 observer에 대한 참조를 사용했습니다.
매번 재현 가능한 트리거로 web-reachable renderer UAF를 유발할 수 있어, exploit 가능성이 높은 취약점입니다. controlled crash를 넘어선 확장을 위해서는 역참조 전에 heap grooming을 통해 해제된 LazyLoadVideoObserver slot(TZone 할당)을 재사용해야 합니다. 재사용 시 m_observer가 읽히는 위치에 attacker 제어 바이트가 들어온다면, 후속 사용을 조종할 가능성이 있습니다. 다만 attacker가 직접 제어할 수 있는 상태는 제한적입니다. 해당 객체의 주요 필드는 내부 RefPtr<IntersectionObserver>이므로, 즉각적인 primitive는 controlled crash에 그칩니다. 확장 가능성은 해제 후 역참조에서 처리되는 내용에 달려 있습니다.
이 취약점은 WebContent renderer process 내부의 메모리 안전성을 약화시킵니다. DOM lifetime 모델은 Document가 소유한 sub-object를 건드리는 모든 작업보다 오래 살아남아야 한다고 가정합니다. 패치 이전에는 이 불변 조건이 위반되어, 소멸된 document의 video element에 attacker가 제어하는 cloneNode()를 호출하면 해제된 LazyLoadVideoObserver에 도달할 수 있었습니다. 시스템 전체에 영향을 미치려면 별도의 sandbox escape가 추가로 필요합니다.
REGRESSION(314874@main) 마커는 해당 버그가 동일한 lazy-video 경로의 이전 변경에서 도입되었음을 나타냅니다. 이는 Document 소유 멤버에 대한 참조를 반환하는 getter가, 호출자가 document 자체의 lifetime을 고정하지 않을 때마다 지속적인 위험 요소가 됨을 재확인시켜 줍니다.
Note: Document::m_lazyLoadObserver 멤버의 nullable 여부, grooming과 관련된 TZone 할당, 그리고 clone에서 observe까지의 정확한 호출 경로는 commit 메시지와 주변 코드 패턴에서 추론한 내용으로, diff에서 직접 확인되는 사항이 아닙니다. 핵심적인 lifetime 오류와 crash 조건은 패치 내용과 일관되게 뒷받침됩니다.
Audit directions
- Document/Node 소유 멤버에 대한 참조를 반환하는 getter 중, statement 범위의 protector 하에서 장기 지역 변수에 바인딩되는 경우. 호출자가 full-expression 동안만 소유자를 고정하면, 소유자가 소멸될 때 반환된 참조가 dangling 상태가 됩니다. 소유 멤버에 대한 참조를 반환하는 다른
Document::xxxObserver()/Document::xxx()getter들(예:lazyLoadImageObserver, intersection/resize observer 데이터)에 대해, 보호Ref의 full-expression이 끝난 뒤에도 반환된 참조를 사용하는 호출 지점이 있는지 점검해야 합니다.protect(element.document())->및protect(document())->패턴 뒤에 저장된 참조가 따라오는 경우를 검색하는 것부터 시작하십시오. document()를 bare reference로 element 초기화를 실행하는 helper에 전달하는 DOM 변경 진입점(clone, adopt, insert, import). 각 진입점이 전 과정에서Ref<Document>를 유지하는지 확인해야 합니다.Node::cloneNodeInternal,Document::adoptNode,Document::importNode, 그리고 elementinsertedIntoAncestor경로에서 document를 보호하지 않은 채 Document 소유 observer를 건드리는 video/lazy-load 등록 코드가 있는지 살펴볼 필요가 있습니다.LIFETIME_BOUND가 붙어야 하는데 누락된 getter.Source/WebCore/dom/Document.h및 관련 헤더에서 소유 멤버의T&를 반환하는 메서드를 검색하고, 분석기가 참조 누출을 탐지할 수 있도록LIFETIME_BOUND가 있는지 확인해야 합니다. 이번 commit에서lazyLoadVideoObserver()에 추가되었으며,lazyLoadModelObserver()와lazyLoadImageObserveraccessor가 즉각적인 검토 대상입니다.