[6] Data race in JSStyleSheet::visitAdditionalChildren during GC leading to use-after-free
GC 스레드에서 owner pointer를 읽는 시점과 main thread에서 backing WeakPtrImpl을 해제하는 시점 사이의 cross-thread UAF를 직렬화하는 패치이며, Severity는 Medium으로 평가됩니다. 다만 DOM teardown과 GC 사이클을 공격자가 정확히 맞출 수 있는지는 이번 변경만으로는 확인되지 않습니다.
JSStyleSheet::visitAdditionalChildren은 JSC garbage collector가 GC 스레드에서 호출하는 함수입니다. 이 함수는 root(StyleSheet*)를 통해 addWebCoreOpaqueRoot(visitor, wrapped())를 호출하며, 이 과정에서 styleSheet->ownerNode()와 styleSheet->ownerRule()을 읽습니다. 두 멤버는 모두 WeakPtr 타입이었고, .get()은 raw pointer를 반환합니다. 문제는 backing WeakPtrImpl이 main thread에 의해 해제될 수 있다는 점입니다. Read와 그 이후의 dereference 사이에 해제가 일어나면 heap use-after-free가 발생합니다. 동일한 root(StyleSheet*)는 JSCSSRule과 JSCSSStyleDeclaration의 visitor에서도 수렴 지점으로 사용되므로, GC visitor 경로 세 곳 모두 영향을 받습니다. 이번 패치는 pure virtual opaqueRootForGCThread()를 추가하고, 새로운 per-stylesheet lock으로 보호하며, owner 멤버를 WeakPtr에서 CheckedPtr로 전환합니다. 부가적인 GC 정확성 수정으로는 @import 자식의 parent stylesheet wrapper를 살아있는 상태로 유지하는 변경도 포함되었습니다.
Source/WebCore/bindings/js/JSStyleSheetCustom.h
Source/WebCore/css/CSSStyleSheet.cpp
Source/WebCore/css/CSSStyleSheet.h
LayoutTests/fast/dom/StyleSheet/gc-import-rule-stylesheet.html
Patch Details
기존의 인라인 root(StyleSheet*) 체인은 GC 스레드에서 styleSheet->ownerRule(), styleSheet->ownerNode() 순으로 순회하는 방식이었습니다. 이번 패치는 이 체인을 새로운 pure virtual StyleSheet::opaqueRootForGCThread()에 대한 단일 virtual dispatch로 대체했습니다. 구현은 CSSStyleSheet와 XSLStyleSheet 양쪽에 각각 제공됩니다. 각 구현은 owner pointer를 읽기 전에 새로운 per-stylesheet lock(m_opaqueRootLockForGC)을 획득하며, 변경 함수인 clearOwnerNode()와 clearOwnerRule() 역시 동일한 lock을 취득하도록 수정되었습니다. 멤버 타입은 m_ownerNode/m_ownerRule 모두 WeakPtr에서 CheckedPtr<Node>/CheckedPtr<CSSImportRule>로 전환되었고, CSSImportRule에는 CanMakeCheckedPtr이 추가되었습니다. 부가적인 GC 정확성 수정으로, m_ownerNode가 없고 m_ownerRule만 있는 @import 자식의 경우 opaqueRootForGCThread()가 ownerRule->parentStyleSheet()->opaqueRootForGCThread()로 재귀하여 parent wrapper를 동일한 opaque root로 묶어 살아있는 상태를 유지합니다.
GC 스레드에서 WeakPtr 간접 참조를 통한 owner pointer를 읽는 동안, main thread에서 backing WeakPtrImpl을 해제할 수 있는 비동기 cross-thread read.
Background
WebKit의 concurrent GC는 전용 GC 스레드에서 live object를 marking하는 동시에 main thread도 계속 실행됩니다. DOM wrapper는 collector가 호출하는 visitAdditionalChildren을 구현하여 다른 객체로의 edge를 보고합니다. WebCoreOpaqueRoot/addWebCoreOpaqueRoot는 DOM 객체를 공통 opaque root 아래 묶어, 일부라도 reachable하면 전체 subtree를 살아있는 상태로 유지합니다. root(StyleSheet*)는 stylesheet의 owner를 순회하여 해당 root를 계산합니다. WeakPtr은 non-owning smart pointer로, .get()은 heap에 별도로 할당된 WeakPtrImpl control block을 통해 현재 pointee 주소를 로드합니다. 반면 CheckedPtr은 pointer 값을 inline으로 저장하며, CanMakeCheckedPtr과 함께 pointee에 assertion counter를 유지합니다. Lock/Locker는 상호 배제를 제공합니다. @import 규칙을 위해 생성된 CSSStyleSheet는 m_ownerNode가 없고 m_ownerRule(CSSImportRule)만 가지며, JS에서는 자식 sheet의 parentStyleSheet 프로퍼티를 통해 접근할 수 있습니다.
Analysis
이 취약점은 cross-thread data race로 인한 heap use-after-free입니다. 패치 이전에는 JSStyleSheet::visitAdditionalChildren(그리고 JSCSSRule, JSCSSStyleDeclaration의 수렴 visitor들)이 GC 스레드에서 root(StyleSheet*)를 실행하며 styleSheet->ownerNode() / ownerRule()을 호출했습니다. 이 호출들은 WeakPtr::get()을 통해 raw pointer를 반환하는데, 내부적으로 heap에 별도 할당된 WeakPtrImpl control block을 역참조하여 pointee 주소를 로드합니다. owner Node/CSSImportRule에 대한 WeakPtrImpl backing store는 두 스레드 간 동기화가 전혀 없기 때문에, GC 스레드가 이를 통해 로드하는 바로 그 순간에 main thread가 해제하거나 weak reference를 초기화할 수 있습니다.
GC 스레드는 해제되거나 불완전하게 쓰여진 WeakPtrImpl 메모리를 읽게 되며, 이후 stale pointer를 Node* 또는 CSSImportRule*로 역참조할 가능성이 있습니다. 좀 더 정확히 말하면, WeakPtr은 자신의 WeakPtrImpl에 대한 Ref를 보유하므로 실제 race는 WeakPtr 멤버 자체의 재할당 또는 초기화 시점에 발생합니다. 단, diff에는 lock과 CheckedPtr 변경만 드러나고 정확한 해제 순서는 나타나지 않습니다. 이번 패치는 GC 스레드의 read와 main thread의 owner pointer clear를 per-stylesheet lock으로 직렬화하고, pointer를 stylesheet 내부에 CheckedPtr로 직접 저장합니다. 이로써 별도 해제 가능한 간접 참조가 제거됩니다. lock이 유지되는 동안 값은 안정적이며, checked-pointer counter가 lifetime을 보장합니다. counter의 소유자인 clearOwnerNode()/clearOwnerRule()은 원소 파괴 전에 실행됩니다.
exploit 가능성은 timing에 달려 있습니다. <style>/<link> owner를 제거하거나 @import 규칙을 파괴하는 방식으로 DOM/CSSOM teardown 시점을 GC 사이클에 맞출 수 있는 공격자라면, marking 중 해제된 heap의 UAF read를 유도할 수 있습니다. controlled heap 조건 하에서 이 read는 renderer 내부의 information leak 또는 추가 corruption으로 발전할 가능성이 있습니다. 다만 timing을 안정적으로 제어할 수 있다는 주장은 이론적으로 가능하지만 diff에서 검증된 내용은 아닙니다.
이 취약점은 GC marking 스레드가 main thread의 CSSOM mutation과 경쟁함으로써 WebContent process 내 memory safety를 약화시킵니다. concurrent marking 중에 접촉하는 객체는 불변이거나 동기화되어야 한다는 보안 모델이 전제로 존재합니다. 그러나 GC 스레드의 owner pointer read는 main thread가 WeakPtrImpl을 해제하는 동작에 대해 어떤 동기화도 없었으므로, marking 중 읽은 pointer가 live 메모리를 가리킨다는 불변 조건이 깨질 수 있었습니다.
이는 WebCore↔JSC concurrent GC 경계에서 반복적으로 나타나는 hazard 패턴입니다. visitAdditionalChildren/opaque-root helper는 GC 스레드에서 실행되므로, 동기화 없이 main thread가 변경 가능한 상태를 건드려서는 안 됩니다. WeakPtr은 plain pointer read처럼 보이지만 실제로는 owning thread가 해제할 수 있는 별도 heap 할당 WeakPtrImpl을 역참조하기 때문에 특히 위험합니다. 이 race는 단순한 scalar 찢김이 아니라 실질적인 UAF입니다. CheckedPtr과 명시적 lock의 조합이 올바른 해결 방향입니다. pointer 값을 inline으로 유지하고 read와 clear를 직렬화하는 구조가 핵심입니다.
Note: visitor 진입 지점, 정확한 WeakPtrImpl 해제 순서, timing 기반 exploit 가능성, 모든 owner destructor에 걸친 CheckedPtr lifetime 불변 조건은 commit message와 코드 패턴으로부터 추론한 내용이며, diff에서 직접 확인되는 사항이 아닙니다. lock/CheckedPtr 변경과 opaque-root 재귀는 패치에서 직접 확인됩니다.
Audit directions
- 동기화 없이 main thread가 변경 가능한 WeakPtr 멤버를 읽는 GC 스레드 코드 점검.
Source/WebCore/bindings/js의 모든visitAdditionalChildren,visitOutputConstraints,root(...)/addWebCoreOpaqueRoothelper에서 main thread가 초기화할 수 있는 멤버에 대해WeakPtr::get()을 호출하는 경로가 있는지 살펴봐야 합니다.bindings/js에서visitAdditionalChildren을 검색한 뒤, 각 함수가 접촉하는 멤버의 선언 타입에WeakPtr이 있는지 교차 확인하는 방식으로 시작하는 것이 효과적입니다. - lock 없이 cross-thread pointer로 사용되는 WeakPtr 점검. WebCore 전체에서 GC/marking 스레드 또는 compositing/scrolling 스레드에서 역참조되는
WeakPtr<멤버를 검색합니다. 각 항목이 marking 중 불변이거나 lock으로 보호되는지 확인해야 합니다. 이러한 멤버에 대한.get()호출은 plain pointer read가 아니라WeakPtrImplcontrol block에 대한 concurrent load임을 인지해야 합니다. - opaque-root grouping 재귀 검증. 순환하거나 깊이 중첩된
@import체인이 있을 때ownerRule->parentStyleSheet()->opaqueRootForGCThread()재귀가 deadlock(동일 per-sheet lock 재획득)을 일으키거나 marking 중 stack overflow를 유발할 가능성이 있는지 점검해야 합니다. - CheckedPtr lifetime 불변 조건 확인.
HTMLStyleElement,HTMLLinkElement,SVGStyleElement,ProcessingInstruction모두가Nodebase destructor 이전에clearOwnerNode()를 호출하는지, 그리고CSSImportRule/StyleRuleImport가 해제 전에clearOwnerRule()을 호출하는지 확인해야 합니다.CheckedPtr<Node>또는CheckedPtr<CSSImportRule>가 pointee보다 오래 살아남는 상황이 없어야 합니다.