forked from WebKit/WebKit
-
Notifications
You must be signed in to change notification settings - Fork 49
RunLoopBun: break the DispatchTimer self-ref when stop() disarms a still-active timer #305
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
robobun
wants to merge
1
commit into
main
Choose a base branch
from
farm/dispatchtimer-stop-break-cycle
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 This change makes
~TimerBase/WTFTimer__deinitnewly reachable on a foreign thread: in theAtomics.notify-before-timeout path,clearTimeron thread B now drops the self-ref insidestop()and then the last ref viam_timer = nullptr, soWTFTimer__deinitruns on thread B against aBun__WTFTimercreated for thread A's event loop. Pre-PR this destructor was unreachable here (leak), so this is a new cross-thread lifecycle path — the PR's safety argument coversWTFTimer__cancelunder the heap lock but doesn't mentionWTFTimer__deinit. Worth confirming bun#34456 makeswtf_deinitsafe off the owning thread, or deferring the self-ref release back to the timer's RunLoop.Extended reasoning...
What changed
didStopWhileActive()now clearsDispatchTimer::m_functioninsidestop()whenWTFTimer__cancelreturnstrue.m_functionholds thetimer.copyRef()self-reference thatRunLoop::dispatchAftercaptures (RunLoop.cpp:176), so clearing it drops one ref on theDispatchTimer. That is exactly the leak fix — but it also moves where the last ref can fall to zero.Step-by-step trace (the exact scenario the PR targets)
Atomics.waitAsync(buf, i, v, timeout).WaiterListManager::waitAsyncImplcallsRunLoop::currentSingleton().dispatchAfter(...)on thread A.dispatchAfterdoesadoptRef(*new DispatchTimer)(refcount = 1), thensetFunction([timer = timer.copyRef(), ...])(refcount = 2), and returns the outerRef, which is moved intoWaiter::m_timer. Net: refcount = 2 —Waiter::m_timer+ the self-ref insidem_function. TheBun__WTFTimerwas created viaWTFTimer__createagainst thread A's event loop.Atomics.notifyon the same address before the timeout.WaiterListManager::notifyWaiter→notifyWaiterImpl→Waiter::scheduleWorkAndClear→clearTimer(listLocker)all run synchronously on thread B while holdinglist->lock.clearTimer(WaiterListManager.h:98) doesm_timer->stop(). InRunLoopBun.cpp,WTFTimer__cancelreturnstrue(node was stillACTIVEin the heap), sodidStopWhileActive()runs and doesm_function = { }. Destroying the lambda destroys its capturedRef<DispatchTimer>: refcount 2 → 1.clearTimerthen doesm_timer = nullptron the very next line: refcount 1 → 0.~DispatchTimer→~TimerBaseruns on thread B and callsWTFTimer__deinit(&ref->get())on aBun__WTFTimerthat was created for thread A's event loop.There are no other refs that would keep the timer alive past step 4:
RunLoop::m_registeredTimersis aHashSet<TimerBase*>of raw pointers, and the Zig-sideBun__WTFTimerholds a rawTimerBase*back-pointer, not aRef.Why this is new behavior
stop()only calledWTFTimer__cancel; the self-ref inm_functionsurvived, so afterm_timer = nullptrthe refcount was still 1 and the timer leaked.~TimerBase/WTFTimer__deinitwere unreachable on this path.fired()runs on thread A,protectedTimeron thread A's stack is the last ref, and~TimerBaseruns on thread A.WTFTimer__deinitdid not exist before this PR and is introduced here.Why the PR's own argument doesn't cover it
The description says:
That's true for the duration of
stop()itself (sodidStopWhileActive()can't destroy*thismid-call), butm_timer = nullptris the immediately following statement inclearTimer, still on thread B. The description and bun#34456 discuss makingWTFTimer__cancelreturnbooland be lock-safe, but say nothing aboutWTFTimer__deinitbeing called from a thread other than the one that calledWTFTimer__create.Impact and fix
If the Zig-side
wtf_deinittouches thread-A-local state (the owningVirtualMachine/ event-loop'swtf_timersbookkeeping, a per-thread allocator, etc.) without the same lockwtf_disarmtakes, this trades a leak for cross-thread corruption. It may well already be safe —WTFTimer__cancelwas already called cross-thread pre-PR, so the Zig side already locks the heap, anddeinitafter a successful cancel may just be a locked unlink + free — but that's worth confirming explicitly since it isn't stated. If it isn't safe, one option is to post the self-ref release back tom_runLoop(e.g.m_runLoop->dispatch([f = WTF::move(m_function)] { })) so the last deref andWTFTimer__deinithappen on the owning thread.