Branch: refs/heads/main
  Home:   https://github.com/WebKit/WebKit
  Commit: f53d1f284bc24887cb00783ef8aee9e35809b8bc
      
https://github.com/WebKit/WebKit/commit/f53d1f284bc24887cb00783ef8aee9e35809b8bc
  Author: Chris Dumez <[email protected]>
  Date:   2026-07-02 (Thu, 02 Jul 2026)

  Changed paths:
    M Source/JavaScriptCore/runtime/WaiterListManager.h
    M Source/WTF/wtf/FunctionDispatcher.h
    M Source/WTF/wtf/RunLoop.h
    M Source/WTF/wtf/cf/RunLoopCF.cpp
    M Source/WTF/wtf/generic/RunLoopGeneric.cpp
    M Source/WTF/wtf/glib/RunLoopGLib.cpp
    M Source/WTF/wtf/win/RunLoopWin.cpp
    M 
Source/WebCore/page/scrolling/ThreadedScrollingTreeScrollingNodeDelegate.cpp
    M Source/WebCore/page/scrolling/ThreadedScrollingTreeScrollingNodeDelegate.h
    M Source/WebCore/platform/ScrollingEffectsController.h
    M Source/WebCore/platform/graphics/ShadowBlur.cpp
    M Source/WebCore/platform/graphics/cg/IOSurfacePool.cpp
    M
Source/WebCore/platform/graphics/cg/IOSurfacePool.h
    M Source/WebCore/platform/mac/ScrollingEffectsController.mm
    M Source/WebCore/platform/mock/MockRealtimeVideoSource.cpp
    M Source/WebCore/platform/mock/MockRealtimeVideoSource.h
    M 
Source/WebKit/UIProcess/RemoteLayerTree/mac/RemoteLayerTreeEventDispatcher.h
    M 
Source/WebKit/UIProcess/RemoteLayerTree/mac/RemoteLayerTreeEventDispatcher.mm
    M 
Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/AcceleratedSurface.cpp
    M Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/AcceleratedSurface.h
    M 
Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/ThreadedCompositor.cpp
    M Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/ThreadedCompositor.h

  Log Message:
  -----------
  Assert RunLoop::Timer is stopped and destroyed on its own run loop's thread
https://bugs.webkit.org/show_bug.cgi?id=318088

Reviewed by Darin Adler.

Assert RunLoop::Timer is stopped and destroyed on its own run loop's
thread
to help find thread safety bugs. Use ASSERT_WITH_SECURITY_IMPLICATION
to help the fuzzers find such bugs.

Starting (and re-arming) a timer from another thread is safe and supported 
(that is
how dispatch()/dispatchAfter() and cross-thread schedulers like JSRunLoopTimer 
work),
so only assert thread affinity on stop() and destruction, and only when the 
timer is
active (tearing down a never-started or already-stopped timer off-thread is 
harmless).
On Cocoa, start() re-arms an already-fired one-shot by invalidating the old
CFRunLoopTimer in place; this does so without the affinity assert (it does not 
free the
TimerBase, so it cannot cause the cross-thread use-after-free stop()/the 
destructor
guard against).

This immediately caught a real teardown race in MockRealtimeVideoSource,
whose m_emitFrameTimer fires on a dedicated "generateFrame" run loop but
was being stopped directly from the main thread (in stopProducingData()
and setIsInterrupted()), racing
CFRunLoopTimerInvalidate() against an
in-flight timer callback. Fix it by marshaling the timer start and stop
onto m_runLoop (matching what the destructor already does), routing both
through startCaptureTimer()/stopCaptureTimer() so they stay correctly
ordered relative to each other.

It also caught a bug in RemoteLayerTreeEventDispatcher, which owns timers on two
different run loops: m_delayedRenderingUpdateDetectionTimer fires on the 
scrolling
thread, while m_wheelEventActivityHysteresis's timer fires on the main run loop.
The object is ThreadSafeRefCounted and was destroyed on whichever thread dropped
the last reference, so one of the two timers was always torn down off its run
loop's thread. Fix it by (a) destroying m_delayedRenderingUpdateDetectionTimer 
on
the scrolling thread from invalidate(), after m_scrollingTree has been cleared 
so
any in-flight didRefreshDisplay() returns early and cannot recreate it, and (b)
making the dispatcher
DestructionThread::MainRunLoop so it (and its main-run-loop
timers) is always destroyed on the main run loop, even when invalidate()'s
scrolling-thread dispatch drops the last reference. invalidate() also cancels 
the
hysteresis so its timer does not fire a spurious state change during teardown.

It caught IOSurfacePool, whose m_collectionTimer fires on the main run loop but
whose discardAllSurfaces() can run on another thread when an ImageBuffer backend
is destroyed in the GPU process. Stop the timer on the main run loop in that 
case.

It also caught the scroll-snap timers in ScrollingEffectsController, which are
created against the scrolling thread's run loop 
(ThreadedScrollingTreeScrollingNodeDelegate::createTimer)
but were stopped on the main thread from stopAllTimers() during 
commitTreeState().
Hand those timers to the client for destruction instead of stopping them inline,
and have the threaded delegate destroy them on the scrolling thread. The timer's
callback
holds a Ref to the scrolling node (and ScrollerPairMac already releases
its main-thread resources via ensureOnMainThread()), so the node stays alive 
until
the timer is gone and a late-firing callback remains safe.

It also caught ScratchBuffer (ShadowBlur), whose m_purgeTimer fires on the main
run loop but was restarted from scheduleScratchBufferPurge() on a paint worker 
thread
(RemoteGraphicsContext in the GPU process). Restarting an active timer 
off-thread is
unsafe because start() invalidates the existing CFRunLoopTimer; schedule the 
purge on
the main run loop via ensureOnMainRunLoop() so m_purgeTimer is only ever 
touched there.

The assertions are not Cocoa-specific: every RunLoop backend (CF, GLib, generic,
Windows) reaches the TimerBase via the run loop's own thread, so they all 
assert.
On GTK this caught ThreadedCompositor::m_renderTimer, which is bound to the 
compositor
thread's run loop (m_workQueue) but whose start/stop was driven from the main 
thread
via
suspend()/resume()/invalidate(). Route the render-timer start/stop through a new
updateRenderTimer() that hops to the compositor thread; the desired state stays 
tracked
synchronously under m_state.lock by m_state.isRenderTimerActive. The same 
suspend()/
resume() path also reaches AcceleratedSurface::m_releaseUnusedBuffersTimer (via
visibilityDidChange()) and AcceleratedSurface tears it down in 
willDestroyCompositingRunLoop();
that timer is likewise bound to the compositing run loop, so confine its 
start/stop and
destruction to that run loop too.

It also caught JSC's Atomics.waitAsync timeout timer (WaiterListManager): the 
timeout
is a RunLoop::dispatchAfter() timer created on the waiting agent's thread, but
Waiter::clearTimer() stop()s it from the notifying agent's thread 
(Atomics.notify) or a
GC thread. A dispatchAfter() timer is fire-and-forget -- its callback holds a 
self-
reference, so it stays alive, fires once on its own thread, and tears itself 
down
there;
stopping it early both raced cross-thread and leaked it (the self-reference was 
never
consumed). Since the timeout handler is idempotent (the waiter's ticket is 
already
cleared), clearTimer() now just drops the waiter's reference and lets the timer 
fire
harmlessly on its own thread.

* Source/JavaScriptCore/runtime/WaiterListManager.h:
* Source/WTF/wtf/FunctionDispatcher.h:
(WTF::WTF_ASSERTS_ACQUIRED_CAPABILITY):
* Source/WTF/wtf/RunLoop.h:
(WTF::WTF_ASSERTS_ACQUIRED_CAPABILITY):
* Source/WTF/wtf/cf/RunLoopCF.cpp:
(WTF::RunLoop::TimerBase::start):
(WTF::RunLoop::TimerBase::stop):
* Source/WTF/wtf/generic/RunLoopGeneric.cpp:
(WTF::RunLoop::TimerBase::~TimerBase):
(WTF::RunLoop::TimerBase::stop):
* Source/WTF/wtf/glib/RunLoopGLib.cpp:
(WTF::RunLoop::TimerBase::~TimerBase):
(WTF::RunLoop::TimerBase::stop):
* Source/WTF/wtf/win/RunLoopWin.cpp:
(WTF::RunLoop::TimerBase::stop):
*
Source/WebCore/page/scrolling/ThreadedScrollingTreeScrollingNodeDelegate.cpp:
(WebCore::ThreadedScrollingTreeScrollingNodeDelegate::destroyTimer):
* Source/WebCore/page/scrolling/ThreadedScrollingTreeScrollingNodeDelegate.h:
* Source/WebCore/platform/ScrollingEffectsController.h:
(WebCore::ScrollingEffectsControllerClient::destroyTimer):
* Source/WebCore/platform/graphics/ShadowBlur.cpp:
* Source/WebCore/platform/graphics/cg/IOSurfacePool.cpp:
(WebCore::IOSurfacePool::stopCollectionTimer):
(WebCore::IOSurfacePool::discardAllSurfacesInternal):
* Source/WebCore/platform/graphics/cg/IOSurfacePool.h:
* Source/WebCore/platform/mac/ScrollingEffectsController.mm:
(WebCore::ScrollingEffectsController::stopAllTimers):
*
Source/WebCore/platform/mock/MockRealtimeVideoSource.cpp:
(WebCore::MockRealtimeVideoSource::applyFrameRateAndZoomWithPreset):
(WebCore::MockRealtimeVideoSource::startCaptureTimer):
(WebCore::MockRealtimeVideoSource::stopCaptureTimer):
(WebCore::MockRealtimeVideoSource::stopProducingData):
(WebCore::MockRealtimeVideoSource::setIsInterrupted):
* Source/WebCore/platform/mock/MockRealtimeVideoSource.h:
* Source/WebKit/UIProcess/RemoteLayerTree/mac/RemoteLayerTreeEventDispatcher.h:
* Source/WebKit/UIProcess/RemoteLayerTree/mac/RemoteLayerTreeEventDispatcher.mm:
(WebKit::RemoteLayerTreeEventDispatcher::~RemoteLayerTreeEventDispatcher):
(WebKit::RemoteLayerTreeEventDispatcher::invalidate):
* Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/ThreadedCompositor.cpp:
(WebKit::ThreadedCompositor::startRenderTimer):
(WebKit::ThreadedCompositor::stopRenderTimer):
(WebKit::ThreadedCompositor::updateRenderTimer):
*
Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/AcceleratedSurface.cpp:
(WebKit::AcceleratedSurface::visibilityDidChange):
(WebKit::AcceleratedSurface::didCreateCompositingRunLoop):
(WebKit::AcceleratedSurface::willDestroyCompositingRunLoop):
* Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/AcceleratedSurface.h:
* Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/ThreadedCompositor.h:

Canonical link: 
https://flagged.apple.com:443/proxy?t2=Ds5D5W3ZH6&o=aHR0cHM6Ly9jb21taXRzLndlYmtpdC5vcmcvMzE2NDU3QG1haW4=&emid=a569ece2-cf55-436c-9dbc-dc7a8ad1437d&c=11



To unsubscribe from these emails, change your notification settings at 
https://github.com/WebKit/WebKit/settings/notifications

Reply via email to