← All issues

[4] DocumentThreadableLoader null-WeakPtr dereference on m_document

`DocumentThreadableLoader` deref'd its `WeakPtr<Document>` directly — async preflight callbacks after iframe detach became a renderer kill switch.

Severity: Medium | Component: WebCore loader — DocumentThreadableLoader / CrossOriginPreflightChecker | 8384754

Medium으로 평가된 이유는, originating document가 detach된 이후에도 CORS preflight / redirect / failure callback에서 m_document의 null WeakPtr을 역참조하는 경로가 존재하기 때문입니다. WeakPtr::operator*RELEASE_ASSERT는 매번 재현 가능한 renderer abort를 유발하며, web content에서 직접 트리거할 수 있습니다. assert가 null storage에 대한 pointer 연산 이전에 발생하므로, read/write primitive는 노출되지 않습니다.

이 패치는 DocumentThreadableLoaderCrossOriginPreflightChecker에서 m_document를 역참조하기 전에 liveness 확인을 추가했습니다. 기존에는 document() / protectedDocument()를 통해 WeakPtr을 직접 역참조하는 방식이었습니다. m_document가 null일 수 있는 상황에서, 기존 패턴은 null 시 프로세스를 종료하는 WeakPtr::operator*RELEASE_ASSERT에 의존하고 있었습니다. 이번 수정에서는 각 접근 지점을 로컬 RefPtr로 변환하여 call window 동안 document의 lifetime을 연장하고, 역참조 전에 명시적인 null 확인을 삽입했습니다. 또한 document()의 반환 타입이 Document*로 변경되어, null 케이스가 타입 시스템에 드러납니다. 아울러 헤더 파일이 UncheckedCallArgsCheckerExpectations에서 제거되었는데, 이는 WebKit의 safer-CPP unchecked-arg static check를 통과함을 나타냅니다.

Source/WebCore/loader/DocumentThreadableLoader.h

- Document& document() { return *m_document; }
+ Document* document() { return m_document; }

Source/WebCore/loader/DocumentThreadableLoader.cpp

void DocumentThreadableLoader::makeCrossOriginAccessRequest(ResourceRequest&& request) {
...
- Ref document = *m_document;
+ RefPtr document = m_document;
+ if (!document)
+ return;
...
void DocumentThreadableLoader::preflightFailure(...) {
- RefPtr frame = m_document->frame();
+ RefPtr document = m_document;
+ if (!document)
+ return;
+ RefPtr frame = document->frame();

Source/WebCore/loader/CrossOriginPreflightChecker.cpp

void CrossOriginPreflightChecker::validatePreflightResponse(...) {
- RefPtr frame = loader.document().frame();
+ RefPtr loaderDocument = loader.document();
+ if (!loaderDocument) { ASSERT_NOT_REACHED(); return; }
+ RefPtr frame = loaderDocument->frame();

이번 수정은 DocumentThreadableLoader.cppCrossOriginPreflightChecker.cpp에서 기존에 m_document를 역참조하던 모든 지점을 대상으로 합니다. 구체적으로는 shouldSetHTTPHeadersToKeep, makeCrossOriginAccessRequest, cancel, didReceiveResponse, didFail, preflightFailure, loadRequest, securityOrigin, contentSecurityPolicy, crossOriginEmbedderPolicy, logErrorAndFail이 해당되며, preflight checker 쪽에서는 validatePreflightResponse, notifyFinished, startPreflight, doPreflight가 포함됩니다. accessor 시그니처 변경(Document& document()Document* document())으로 인해 null 케이스가 모든 호출 지점에서 타입 시스템에 반영됩니다. 비즈니스 로직의 재구성은 없으며, 수정 방식은 일관되게 "RefPtr로 캡처 → null 확인 → 역참조" 패턴을 따릅니다.

Null 시 RELEASE_ASSERT를 유발하는 stale WeakPtr 역참조가 web content 기반 loader callback에서 도달 가능했던 패턴.

WeakPtr<T>는 WebKit에서 사용하는 non-owning smart pointer로, 참조 대상이 소멸되면 null이 됩니다. null 상태의 WeakPtroperator*operator->를 호출하면 RELEASE_ASSERT가 발생하고 프로세스가 종료됩니다. 이 assert는 release 빌드에서도 항상 활성화됩니다. 한편 RefPtr<T>는 reference count 기반의 owning smart pointer입니다. WeakPtrRefPtr에 할당하면, 참조 대상이 살아 있는 경우 strong reference를 캡처하여 scope 동안 lifetime을 연장합니다. 반대로 참조 대상이 이미 소멸된 경우에는 null로 평가됩니다.

DocumentThreadableLoaderDocument를 대신해 비동기·동기 로드를 수행하는 WebCore 클래스입니다. fetch(), XMLHttpRequest, EventSource 등의 backend 역할을 담당하며, CrossOriginPreflightChecker를 통해 CORS preflight를 처리합니다. 이 loader는 RefCounted이므로 originating Document보다 오래 살아남을 수 있습니다. 프레임이 detach되거나 document가 교체되면 document는 소멸되지만, 진행 중인 loader(network/CORS state machine이 소유)는 계속 실행되다가 완료 또는 오류 callback을 전달합니다. loader는 document의 lifetime을 연장하지 않기 위해, document를 WeakPtr<Document, WeakPtrImplWithEventTargetData> m_document로 저장합니다.

패치 이전에는 DocumentThreadableLoader::document()operator*m_document를 역참조하여 Document&를 반환했습니다. loader가 document보다 오래 살아남는 시나리오는 여럿입니다. 비동기 CORS preflight 진행 중, redirect callback 처리, 프레임 detach 이후 오류 보고, service worker 경유 경로 등이 해당됩니다. 이 파일의 다수 code path는 liveness 확인 없이 m_document를 역참조했으며, 그 결과 어느 경로에서든 WeakPtr::operator*RELEASE_ASSERT가 발생하여 WebContent 프로세스가 종료될 수 있었습니다.

이는 WebKit에서 반복적으로 나타나는 패턴입니다. Document보다 오래 살아남는 컴포넌트가 lifetime 연장을 피하기 위해 WeakPtr로 document를 저장하면서도, RefPtr 변환과 null 확인 대신 operator* / operator->로 직접 역참조하는 유형입니다. WeakPtr::operator*RELEASE_ASSERT는 이런 잠재적 UAF 형태의 버그를 항상 동일한 프로세스 종료로 전환합니다. memory corruption 관점에서는 defense-in-depth 이점이 있지만, web content에서 도달 가능한 crash surface가 넓게 남는다는 문제가 있습니다.

Web content 관점에서 경로는 직접적입니다. 공격자가 제어하는 iframe 또는 window에서 CORS preflight가 필요한 cross-origin fetch()를 호출합니다. custom header가 있거나 non-simple method를 사용하는 요청이 해당됩니다. preflight가 진행되는 동안, document를 동기적으로 detach합니다. iframe을 제거하거나, 프레임을 navigate하거나, window를 닫는 방식이 가능합니다. preflight 완료, redirect, 또는 failure callback이 도달하면, 해당 DocumentThreadableLoader 또는 CrossOriginPreflightChecker 메서드가 이미 null이 된 m_document WeakPtr을 역참조합니다. 패치 이전의 호출 지점인 preflightFailure(m_document->frame()), cancel(m_document->identifier()), loadRequest(m_document->frame()), validatePreflightResponse(loader.document().frame())는 모두 이 경로에서 도달 가능합니다.

헤더 레벨 수정(document()의 반환 타입을 Document*로 변경)이 더 지속적인 완화책입니다. null 케이스를 타입 시스템으로 밀어넣어 모든 호출자가 처리하도록 강제하기 때문입니다. WeakPtr<Document>를 보유하는 다른 장수 loader/observer 클래스에도 동일한 패턴을 적용할 수 있습니다. UncheckedCallArgsCheckerExpectations에서 제거된 사실은 WebKit의 static checker가 이 패턴을 능동적으로 감지하고 있음을 나타냅니다.

이 vulnerability는 WebContent 프로세스의 가용성을 저하시킵니다. HTML loading lifecycle에서는 DocumentThreadableLoader callback이 도달하는 시점에 Document가 살아 있음을 전제합니다. 패치 이전에는 이 불변 조건이 graceful null 확인이 아닌 RELEASE_ASSERT로만 보장되었습니다. 따라서 요청 시작과 callback 사이에 document가 detach되는 모든 code path에서 renderer가 종료될 수 있었습니다. cross-origin fetch를 발행하면서 loader callback이 완료되기 전에 originating document를 소멸시키는 페이지를 공격자가 제어할 수 있다면, WebContent 프로세스를 안정적으로 crash시킬 수 있습니다. 이는 memory safety primitive가 아닌, renderer에 대한 denial-of-service primitive에 해당합니다.

Note: WeakPtr::operator*RELEASE_ASSERT 동작, DocumentThreadableLoaderDocument보다 오래 살아남는 구체적 시나리오, preflight 진행 중 iframe detach 재현 경로 등 일부 사항은 diff에 직접 드러나지 않으며, patch의 완화 패턴과 표준 WTF 관용구에서 추론한 내용입니다. WeakPtrRefPtr 변환과 모든 호출 지점의 null 확인이라는 수정 방식 자체는 patch에서 직접 확인됩니다.