← All issues

[4] DocumentThreadableLoader null-WeakPtr dereference on m_document

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가 넓게 남는다는 문제가 있습니다.

🔒

How does a cross-origin fetch plus a well-timed frame teardown turn into a deterministic renderer crash, and what is — and isn't — possible beyond the crash itself?

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

🔒

Several reusable audit patterns identified across WebKit's loader and DOM lifecycle code, with concrete starting points for variant discovery.

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