[6] Race-condition UAF in JSSubscriber GC marking
Severity: Medium | Component: WebCore DOM Observable/Subscriber | db743cc
GC thread가 stale raw pointer를 통해 해제된 VoidCallback을 역참조할 수 있는 실제 race condition이 수정된 사안입니다. 다만 실용적인 UAF primitive로 확장되려면 main thread와 GC thread 간의 좁은 타이밍 window를 안정적으로 제어하고, 해제된 allocation을 재사용해야 합니다. 이 두 조건 모두 diff만으로는 확인되지 않으므로 Medium으로 평가됩니다.
이 PR은 JSSubscriber::visitAdditionalChildren의 race condition을 수정합니다. 해당 결함은 VoidCallback 객체의 use-after-free로 이어집니다. Subscriber::teardownCallbacksConcurrently가 lock을 획득해 Vector<VoidCallback*>을 생성하는 동안, main thread는 VoidCallback을 해제할 수 있습니다. 이 시점에 GC thread가 동일 객체에 대해 visitJSFunction을 호출하면 문제가 됩니다. 안정적인 재현 방법이 없어 새로운 테스트는 추가되지 않았습니다.
Source/WebCore/dom/Subscriber.cpp
-Vector<VoidCallback*> Subscriber::teardownCallbacksConcurrently()
-{
- Locker locker { m_teardownsLock };
- return m_teardowns.map([](auto& callback) {
- return callback.ptr();
- });
-}
-
-void Subscriber::visitAdditionalChildrenInGCThread(JSC::AbstractSlotVisitor& visitor)
-{
- // GC thread에서 호출될 수 있으므로 여기서 `teardown`을 ref할 수 없습니다.
- SUPPRESS_UNRETAINED_ARG for (auto* teardown : teardownCallbacksConcurrently())
- teardown->visitJSFunctionInGCThread(visitor);
- ...
-}
+template<typename Visitor>
+void Subscriber::visitAdditionalChildrenInGCThread(Visitor& visitor)
+{
+ // 이 함수는 main thread와 동시에 GC thread에서 실행되므로, 어떤 것도 ref해서는 안 됩니다.
+ {
+ Locker locker { m_teardownsLock };
+ SUPPRESS_UNCOUNTED_LOCAL for (auto& teardown : m_teardowns)
+ SUPPRESS_UNCOUNTED_ARG teardown->visitJSFunctionInGCThread(visitor);
+ }
+ SUPPRESS_UNRETAINED_ARG m_observer->visitAdditionalChildrenInGCThread(visitor);
+}
Source/WebCore/dom/Subscriber.h
// Vector<Ref<VoidCallback>> m_teardowns WTF_GUARDED_BY_LOCK(m_teardownsLock);
// void stop() final { Locker locker { m_teardownsLock }; m_teardowns.clear(); }
Patch Details
teardownCallbacksConcurrently()와 observerConcurrently()를 제거하고, template으로 작성된 단일 Subscriber::visitAdditionalChildrenInGCThread를 Subscriber.cpp에 통합했습니다. 새로운 메서드는 m_teardowns(Vector<Ref<VoidCallback>>) 전체를 순회하는 동안 Locker locker { m_teardownsLock }을 유지합니다. lock이 유지된 상태에서 teardown->visitJSFunctionInGCThread(visitor)를 호출하고, locked block 이후에 m_observer를 방문합니다. JSSubscriber::visitAdditionalChildrenInGCThread는 이제 단순히 wrapped().visitAdditionalChildrenInGCThread(visitor)로 전달합니다.
Lock 획득 중 refcount 객체를 raw pointer로 snapshot한 뒤, lock 해제 후 역참조하는 과정에서 발생하는 use-after-free.
Background
Observable API는 JS에 Subscriber를 노출합니다. subscriber.addTeardown(callback)으로 등록된 VoidCallback JS 함수는 subscription이 종료될 때 실행되며, m_teardownsLock으로 보호되는 Vector<Ref<VoidCallback>> m_teardowns에 저장됩니다.
JSC는 concurrent garbage collector를 사용합니다. marking이 별도의 GC thread에서 실행될 수 있으며, 이때 main thread는 JS를 동시에 실행할 수 있습니다. 따라서 custom mark hook(visitAdditionalChildrenInGCThread 계열)은 thread-safe해야 하며, ref()/deref()를 호출해서는 안 됩니다. collector thread에서의 refcount 변경은 안전하지 않기 때문입니다. 이러한 이유로 코드는 lock으로만 보호된 raw access를 사용합니다.
ActiveDOMObject::stop()은 런타임이 호출하는 lifecycle teardown으로, context 종료 시 동일한 lock 하에 m_teardowns를 초기화합니다. Ref<VoidCallback>은 sole-ownership smart pointer입니다. vector를 초기화하면 마지막 reference가 dropped되어 callback이 소멸됩니다.
Analysis
이 결함은 snapshot-then-use-after-unlock(TOCTOU) 형태의 전형적인 race-condition use-after-free입니다.
패치 이전에는 m_teardowns를 보호하는 lock이 callback을 unowned raw pointer 형태의 Vector<VoidCallback*>으로 복사하는 동안만 유지되었습니다. GC thread가 visitJSFunctionInGCThread를 통해 해당 pointer를 역참조하기 전에 lock은 이미 해제된 상태였습니다. GC thread가 방문하는 모든 VoidCallback*이 여전히 살아있어야 한다는 invariant는 snapshot 시점에만 보장되었고, 실제 사용 시점에는 보장되지 않았습니다. GC thread가 의도적으로 callback을 ref()하지 않기 때문에, 두 시점 사이의 간격 동안 객체를 살아있게 유지할 수단이 없었습니다.
Aaaa Aa Aa Aa Aaaaaaa Aaa Aaaa Aa Aaaaaaaaa Aaaa Aaaaaaa Aa Aaaaaaaaaaaaaa Aaa Aaaa a Aaaaa Aaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaa Aaaaa a Aaa Aaa Aaaaaaaaaaaaaaaaaaaa Aaaaaaaaa Aaaaaaaaaaa Aaaaaa Aa Aa Aaaaaaa Aaaaa Aaa Aaaaaaaa Aaa a Aa Aaa Aaaa Aa Aaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaa
Aa Aaaaaaaaa Aaa Aa Aaaaaaaa Aaa Aaaaaa a a Aaaaa Aaa Aaa Aa Aaa Aaaaaa Aaaaaaa Aa Aaa a Aaaaa Aaaaaaa Aaaaa Aa Aa Aaa Aaa Aa Aaaaaa
Aaaaa Aaaaa Aaa a Aa Aaaaaa Aa Aaa Aa Aaaa Aaaa Aaaa Aaaaa a Aaa Aaa Aaaa Aaaaa Aaaa Aaa Aaa a Aa Aaaaa Aa Aaaaaaaa Aa Aaaaaaaaaaaaaa Aaaaaaaaaa Aaaa Aaaa Aaa a Aaaaa Aa Aaaa Aaa Aaa Aaaaaaaaaa Aaaa Aa Aaa Aaa Aaaaaa
a Aaaaaaaaaaaaaa Aaaa Aaaaaaa Aa Aaa Aaaaaaaaaa Aa Aaaaaaaa Aaaaa Aaaaaaa Aaaaaaaaaa Aaaaaaa Aaa Aaaaaa Aaaaaaa Aaaaaaa Aaaaaa Aa Aaaa Aaaaaa Aa Aaa Aaa Aa Aaaaa Aaaa Aa Aaaaa Aaa Aaa Aaaaa Aa Aaaa Aaa Aaaaaaa Aaaaaaaaa Aaaa Aaaa a Aaa Aaaaaaa
Aa Aaa Aa Aa Aaaa Aaaaaaaaaaaaaaaaaa Aaaaaa Aaaaaaaaa Aaaaaaaaaa Aa a Aaa Aaa a Aaaaa Aaaaa Aaaaaaaaaaaaaaaaaaa Aaaa Aaa Aaaa Aa Aaaa Aaa Aaaaaa
Aaaaa Aa Aaaaaaaaa Aaaaa Aaaa Aa a Aaaaa Aaa Aaaa Aa Aaa Aaaaa Aaaaaaaaa Aaaaa Aaaaaaaaaa Aaaa Aaa Aaaaaaaaaa Aa Aaaaaaaa Aaaaaaa Aaaaa Aaaa Aaaaa Aaaaaa Aaa Aaaaa Aaaa Aaaa Aaaa Aa Aaaaaaa Aaaa Aaaa
Aaaaaaaaa Aa Aaa Aaa Aa Aaaaaaaa Aaaaaa Aaa Aaaaa Aaaaa Aaa Aaaa Aaaaaaaaaa Aa a Aaaa Aaa Aaaaaa Aaaa Aa Aaa Aa Aaa Aaaaa Aaaa Aaa Aaaaa
🔒The concurrency and lifetime implications of this GC-thread race are examined in depth, including how far the resulting memory-safety issue could realistically be pushed.
더 확인하려면 구독해 주세요
Audit directions
a Aaaaaaaa Aaa Aaaaaaaa Aaaaa Aaa Aaaaaaaa Aaaaaaaaaa Aaaaa Aaaaaa Aaa Aaaaaaaa Aa Aaaaa Aaaa Aa Aaa Aaaaaaaa Aaaaaaaaa a Aaaaa Aaaaa Aa Aa Aaa Aaaaaaaa Aaaaa Aaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaa Aaa Aaaaaaaaa Aaaaaaaaaaaaaaaaaaaa Aaaaaaaaaa Aaaa a Aaa Aa Aa Aaa Aaaaa Aaaa Aaaa
a Aaaaaaa Aaaaaaaa Aaaa Aaaaaaaaaa Aa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaa Aa Aaaaaaaaaaa Aaa Aaa Aaaaaaaaa Aaaa Aaaaa Aaaaaa a Aaa Aaaaa Aaaa Aaaaaaa a Aaaaaaaaa Aaa Aaaaaaaaaaaa Aaa Aaaaa Aaaaa Aaaa Aaaa Aaaaaaaaaaaaaaaaaaaaa Aaa Aa Aaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaaa a Aaaaaaaaaa Aaa Aa Aaaa Aaaa
a Aaaaa Aaaaaaaa Aaaaaaaa Aa Aaaaaaaaaaaaaaaaaaaaaa Aaaa Aaaaaaaa Aaaaaaaaaaaaaa Aaa Aaaaaaaa Aaaa Aa Aa a Aaaa Aaaaa Aaaaa Aaaa Aaaa Aaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaa Aaa Aaaaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aa Aaa Aaa Aaa Aaaaa Aaa Aaaaaaaaaaaaaaaaaa Aaa Aaaaa Aaaa Aaaa
🔒Several reusable audit patterns for concurrent-GC mark hooks are identified, with concrete starting points across WebCore DOM callback registries.
더 확인하려면 구독해 주세요