From 7d8b6f52dcea9f2ab385c96090e4acbe66192825 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 17 Jul 2026 08:49:39 +0000 Subject: [PATCH] RunLoopBun: break the DispatchTimer self-ref when stop() disarms a still-active timer RunLoop::dispatchAfter stores a lambda in DispatchTimer::m_function that captures a Ref to the timer itself. That self-ref is only moved out when fired() runs; a caller that stops the timer before it fires (WaiterListManager::clearTimer on Atomics.notify/unregister) orphans the cycle and leaks both the DispatchTimer and the Bun-side WTFTimer box it owns. Add a virtual TimerBase::didStopWhileActive() hook (USE(BUN_EVENT_LOOP) only). DispatchTimer overrides it to clear m_function. TimerBase::stop() calls it when WTFTimer__cancel reports the timer was still in Bun's wtf_timers heap, which means Bun's drain_due_wtf_timers has not popped it (the ACTIVE to FIRED transition is serialised by the heap lock) and so fired() is not executing on any thread. WTFTimer__cancel's signature changes from void to bool; oven-sh/bun#34456 provides the matching Rust side. --- Source/WTF/wtf/RunLoop.h | 12 ++++++++++++ Source/WTF/wtf/bun/RunLoopBun.cpp | 7 +++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Source/WTF/wtf/RunLoop.h b/Source/WTF/wtf/RunLoop.h index 0c67601ff6d02..110163098d3b5 100644 --- a/Source/WTF/wtf/RunLoop.h +++ b/Source/WTF/wtf/RunLoop.h @@ -195,6 +195,11 @@ class WTF_CAPABILITY("is current") RunLoop final : public GuaranteedSerialFuncti #elif USE(BUN_EVENT_LOOP) // WTFTimer in Timer.zig struct Bun__WTFTimer; + // Called from stop() once the backend has removed a still-armed timer + // from its heap, i.e. fired() is not running and will not run. Used by + // DispatchTimer to drop the self-ref dispatchAfter() stores in + // m_function; other subclasses have nothing to release. + virtual void didStopWhileActive() { } #endif ASCIILiteral description() const { return m_description; } @@ -337,6 +342,13 @@ class WTF_CAPABILITY("is current") RunLoop final : public GuaranteedSerialFuncti } private: void fired() final { m_function(); } +#if USE(BUN_EVENT_LOOP) + // dispatchAfter() captures a Ref to this timer inside m_function; + // dropping it here breaks the cycle when a caller stops the timer + // before it fires. stop() only calls this once the Bun-side heap has + // removed the still-armed node, so m_function is not on any stack. + void didStopWhileActive() final { m_function = { }; } +#endif Function m_function; }; diff --git a/Source/WTF/wtf/bun/RunLoopBun.cpp b/Source/WTF/wtf/bun/RunLoopBun.cpp index 6127aff064c78..6315ccb312665 100644 --- a/Source/WTF/wtf/bun/RunLoopBun.cpp +++ b/Source/WTF/wtf/bun/RunLoopBun.cpp @@ -22,7 +22,9 @@ extern "C" __attribute__((weak)) void WTFTimer__update(RunLoop::TimerBase::Bun__ extern "C" __attribute__((weak)) void WTFTimer__deinit(RunLoop::TimerBase::Bun__WTFTimer*); extern "C" __attribute__((weak)) bool WTFTimer__isActive(const RunLoop::TimerBase::Bun__WTFTimer*); extern "C" __attribute__((weak)) double WTFTimer__secondsUntilTimer(const RunLoop::TimerBase::Bun__WTFTimer*); -extern "C" __attribute__((weak)) void WTFTimer__cancel(RunLoop::TimerBase::Bun__WTFTimer*); +// Returns whether the timer was still armed in Bun's wtf_timers heap (i.e. +// fired() has not been and will not be dispatched for it). +extern "C" __attribute__((weak)) bool WTFTimer__cancel(RunLoop::TimerBase::Bun__WTFTimer*); // Weak, so that Bun can override it extern "C" __attribute__((weak)) bool Bun__thisThreadHasVM(); @@ -89,7 +91,8 @@ void RunLoop::TimerBase::stop() return stopGeneric(); case Kind::Bun: if (auto* ref = std::get_if>(&m_impl)) { - WTFTimer__cancel(&ref->get()); + if (WTFTimer__cancel(&ref->get())) + didStopWhileActive(); } return; }