[6] Race-condition UAF in JSSubscriber GC marking
Severity: Medium | Component: WebCore DOM Observable/Subscriber | db743cc
Rated Medium because the diff fixes a genuine race in which the GC thread can dereference a freed VoidCallback via a stale raw pointer; escalation to a usable UAF primitive requires reliably winning a narrow main-thread-versus-GC-thread timing window and reclaiming the freed allocation, neither of which the diff establishes.
This PR fixes a race condition in JSSubscriber::visitAdditionalChildren that results in a use-after-free of VoidCallback objects. While Subscriber::teardownCallbacksConcurrently grabs a lock and creates a Vector of VoidCallback*, the main thread can still go ahead and destroy a VoidCallback while a GC thread calls visitJSFunction on it. No new tests, since there is no reliable reproduction.
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)
-{
- // We cannot ref `teardown` here as this may get called from a GC thread.
- SUPPRESS_UNRETAINED_ARG for (auto* teardown : teardownCallbacksConcurrently())
- teardown->visitJSFunctionInGCThread(visitor);
- ...
-}
+template<typename Visitor>
+void Subscriber::visitAdditionalChildrenInGCThread(Visitor& visitor)
+{
+ // Do not ref anything in this function, which runs in a GC thread concurrently to the main thread.
+ {
+ 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
The patch deletes teardownCallbacksConcurrently() and observerConcurrently() and consolidates a single templated Subscriber::visitAdditionalChildrenInGCThread into Subscriber.cpp. The new method holds Locker locker { m_teardownsLock } across the entire iteration over m_teardowns (a Vector<Ref<VoidCallback>>), calling teardown->visitJSFunctionInGCThread(visitor) while the lock is held, then visits m_observer after the locked block. JSSubscriber::visitAdditionalChildrenInGCThread now simply forwards to wrapped().visitAdditionalChildrenInGCThread(visitor).
Use-after-free from snapshotting refcounted objects into raw pointers under a lock, then dereferencing them after the lock is released.
Background
The Observable API exposes a Subscriber to JS; subscriber.addTeardown(callback) registers VoidCallback JS functions that run when the subscription ends, stored as Vector<Ref<VoidCallback>> m_teardowns guarded by m_teardownsLock. JSC uses a concurrent garbage collector: marking can run on a separate GC thread at the same time the main thread executes JS, so custom mark hooks (the visitAdditionalChildrenInGCThread family) must be thread-safe and must not call ref()/deref() — refcount churn from the collector thread is unsafe — which is why the code uses raw access guarded only by a lock. ActiveDOMObject::stop() is the lifecycle teardown the runtime invokes (e.g. on context shutdown) and here clears m_teardowns under the same lock. Ref<VoidCallback> is a sole-ownership smart pointer; clearing the vector drops the last reference and destroys the callback.
Analysis
This is a race-condition use-after-free of the classic snapshot-then-use-after-unlock (TOCTOU) shape. Before the fix, the lock protecting m_teardowns was held only long enough to copy the callbacks into a Vector<VoidCallback*> of unowned raw pointers; the lock was then dropped before the GC thread dereferenced those pointers via visitJSFunctionInGCThread. The invariant that must hold — every VoidCallback* visited by the GC thread is still alive — was enforced only at snapshot time, not at use time. Because the GC thread deliberately does not ref() the callbacks, nothing kept them alive across the gap.
Aaa Aaaa Aaaaaa Aaa Aaaaaaa Aaaaa Aaaaaaaaaaaaaa Aaaaaaa Aa Aaa Aaaaaa Aaaaaaa Aaaa Aaaaaaa Aaa Aaaaaaaaa Aaaaaaaaaa Aaaaaaaaaaaaaaaaaaaa Aaaaa Aaaaaaaaaaaaaaaaa Aaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaa Aaaaaaaa Aaa Aaaa Aaaaaaaaaaaaaaaaaaa Aaa Aaaa Aaaaa Aaa Aaaaaaa Aaa Aaaaaaaaaaa Aaa Aa Aaaaaaa Aaaaaaa Aaaa Aaaaa Aaa Aaaaaaaaa Aaaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaa Aa Aaaaa Aaaaaaa Aaaaaaa Aaaaaaaa Aaaaaaaa Aaaaaaaa Aaaaaaaaaaaa Aaaa Aa Aaaaaaa Aa Aaaaaaaa a Aaaaaa Aaaa Aaa Aaaa Aaaaa Aaaaaa Aaaaaaaaaaa a Aa Aaa Aaaaaaaa Aaaaaaaaaaaa Aaaa Aaa Aaaaa Aaa Aaa Aaaaaaaa Aaaa Aa Aaa Aaaaaaaa Aaaaaaaaaaaaa Aaaaaaaa Aaaaa Aa Aaaaaaaa Aaa Aaaaaaaa Aaaa Aaa Aaaa Aaaaa Aaaa Aa Aa Aaaaaa Aaaaaaaaaaaaaaaaaaaaa Aaaa Aaaaaa Aaaaaaa Aaa Aaaaaaaa Aaaaa Aaaaa Aaaaa Aaaaaaaaaa Aaaa Aaaaaaaaaa Aaaaa Aa Aaaaaaaaa Aaaaaa a Aaaaaaaaaaaaaa Aaaaaaaaa Aa Aaa Aaaaaaaaa Aaaa Aaaaaaaa Aaaaaa Aaa Aaaaaaaaaaa Aa Aaa Aaaaaaaaaaa Aaaaa
Aaaa Aaaaaaaaaaaaa Aaaaaaa Aaaaaa Aaaaaa Aaaaaa Aaa Aaaaaaaaaa Aaaaaaa Aa Aaaaaaaa a Aaaaaaaaaaa Aaaaaa Aaaaaaaaaaa Aa Aaaa Aaaaaaa Aaaaaaaaaa Aa Aaaaaaaa Aaa Aaaaa Aaaaaaa Aaa Aaaaaa Aaaaaaaaa Aaaa a Aaaaaa Aa Aaaa Aaaa Aaaaa Aaaaa Aaa Aaa Aaaaaaaa Aa Aaaa Aaaaa Aaaaaa Aaa Aaa Aaaa Aaaa Aaaa Aa Aaa Aaaaaa Aaa Aaaaaaaaaaa Aaaaaaaa Aaa Aaaaaa Aaa Aaa Aaaaa Aaaaaaaaaaaaaaaaa Aaaaaa Aaa Aaaaa Aaaaaa Aa Aaaaaaaaaa Aaaaaaaaa Aaaaaa Aaa Aaaaaaaaaaaa Aaaa Aaa Aaaaa Aaa Aaa Aaaaaaaaaaaaaaaaaaa Aaaaaaa Aaaaaa Aaaaa Aaa Aaa Aaaaaaaaa Aaaa Aa Aaa Aaaaaa Aa Aaaaaaaaa Aaaaa Aaaaa Aaa Aaaaa Aaaaaaa Aaaa Aaa Aaaaaaaa Aaaaaaaaaaaa Aaaaaa Aaaaaaaaa a Aaa Aaaaaaa Aaaaaaaaaaa Aa Aaaaaaaaaa Aa Aaaaaaa Aaaaaaa Aaa Aaaaa Aaaaaa Aaaa Aa Aaa Aaaa Aaaa Aaaaa Aaa Aaaaaa Aaaaaaaaaaa Aaaaaaa
Aaaaaaaaa Aaaaaaa Aaa Aaaaaaa Aaaa Aaaaaaaa Aaaaa Aa Aaaaaaa Aa Aaaaaaaaa Aaa Aaaaaaa Aaa Aaaa Aa Aaaaaaaa Aaaaaaaa Aaa a Aaaaaa Aaaaaaaaaa Aaa Aaaaaaaaa Aaa Aaaaaaaaaa Aaaaaa Aaa Aaa Aaa Aaa Aaaaaaaa Aaaaaaaaa Aa 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.
Subscribe to read more
Audit directions
a Aaaaaaaaaaaaaa Aaaaaaaaaa Aaaaaaaaaa Aaaaaaaaaaa Aaaa Aaa Aaaaaaaaaaa a Aaaaaaaaaaaa Aaaaaaaaa Aa Aaaaaaaaaa Aaaaaaa Aa Aaaaaaaaaaa Aaaa Aaa Aaaaaaaa Aaaaa Aaa Aaaaa Aaa Aaaa Aaaaaaaaa Aaaa Aaa Aaa Aaaaaaaa Aaaaaaaaaaaaa Aaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaa Aaa Aaaaaa Aaa Aaaaaaaa Aaaaaaaa Aaaaa Aaa Aaaaa Aaaaa Aaaaa Aaa Aaaa a Aaaaaaaaaaaaaaaaaaaa Aaaaaaaaa
a Aaaaaaaaaaaa Aa Aaaa Aaaa Aaaaaaaaaaaaa Aaaaaa Aaaaaaaaaa Aaaa Aaaaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaa Aa Aaaaaaaaaa Aaaaaa Aaaaaaaa Aa a Aaaa Aaa Aaaaaaaaa Aaaaaa Aaaa Aaaa Aaa Aaaaaaa Aaa Aaaaaaaaaaaaa Aaaaaaaaaaaa Aaaaaaaa Aaaaaaaa Aaaaaaaaa Aaaaaaaaaaaa Aaaaa Aaa Aaaa Aaaaa Aaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaa Aaaaaaa Aaaaaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaaaaa Aaaaa Aaaa Aaaaaa Aaaaa
a Aaaaaaaaa Aaaaaaaa Aaaaaaaaaaaaa Aaaaaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaa Aaaaaaaaaa Aaaaaaaaa Aaaaaa Aaaaaaaa Aaaaaa Aaaa Aaaaa Aaaa Aaaaaa Aa Aaaaaaaaaaa Aaaaaaa Aaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaa Aaa Aaaaaaa Aaa Aaaaaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaa Aaaaaaa Aa a Aaaa Aaa Aaa Aaaa Aaaaaaaaaaaaaaaaaa Aaaa
🔒Several reusable audit patterns for concurrent-GC mark hooks are identified, with concrete starting points across WebCore DOM callback registries.
Subscribe to read more