RsEventsService: replace global dispatch barrier with a per-handler one (follow-up to #330)#335
Open
jolavillette wants to merge 1 commit into
Open
Conversation
…ne (follow-up to RetroShare#330) RetroShare#330 fixed the shutdown use-after-free (a widget's event callback firing against a dangling `this` -> SIGSEGV in qobject_cast<QThread*> inside RsQThreadUtils::postToObject) by holding a single recursive mutex (mDispatchMtx) across the whole handleEvent() dispatch, so unregisterEventsHandler() could fence on it. That barrier is correct for the UAF but couples unrelated handlers and can deadlock. Two event handlers are deliberately synchronous and blocking: the passphrase request (rsserver/rsloginhandler.cc, "Call the RsEvent blocking API") and the plugin-confirmation dialog (plugins/pluginmanager.cc, "needs to be synchroneous"). They are delivered via sendEvent() -- so handleEvent() runs on the caller's thread -- and their GUI handler blocks that caller with Qt::BlockingQueuedConnection (gui/RsGUIEventManager.cpp) until the GUI thread answers a modal. With one global dispatch lock: 1. a background thread requests the passphrase -> holds mDispatchMtx -> blocks waiting on the GUI thread (BlockingQueuedConnection); 2. the GUI thread destroys any unrelated widget -> its destructor calls unregisterEventsHandler() -> blocks acquiring mDispatchMtx; 3. background waits for GUI, GUI waits for the lock background holds -> hard hang. Replace the global barrier with a per-handler one. At snapshot time (still under mHandlerMapMtx) handleEvent() records each handler id it is about to run, together with the dispatching thread, in mHandlersInFlight, and clears each entry as its callback returns. unregisterEventsHandler() erases the handler from the map (no future dispatch can pick it up) and then waits on a condition variable until that specific handler is no longer running on any OTHER thread. Recording under the same lock as the snapshot makes it race-free: unregister either erases before the snapshot (never run) or after (in-flight mark already visible, so it waits). Callbacks now run with no events-service lock held again (as before RetroShare#330), so a slow or blocking handler only ever delays unregister of that same handler, never of an unrelated widget being torn down on another thread -> the deadlock above cannot occur. The dispatching thread is never waited on, so a handler that unregisters itself (or re-enters via a synchronous sendEvent) does not deadlock on itself. The per-callback in-flight cleanup is exception-safe: if a callback throws, the marks it and the not-yet-run handlers still hold are cleared before the exception propagates, so a later unregisterEventsHandler() cannot block forever. Touches only src/services/rseventsservice.{cc,h}. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
RsEventsService: replace global dispatch barrier with a per-handler one (follow-up to #330)
To fix the shutdown use-after-free, #330 makes
handleEvent()hold a single recursive mutex (mDispatchMtx) for the whole dispatch, and makesunregisterEventsHandler()wait on it. So that mutex is now held while the event callbacks run - and two of those callbacks are synchronous and blocking (the passphrase request and the plugin-confirmation dialog). The mutex is held while the callback blocks, so two threads end up waiting on each other:sendEvent()runs the handleron that thread holding
mDispatchMtx, and blocks on the GUI thread viaQt::BlockingQueuedConnection;unregisterEventsHandler()blocks acquiring
mDispatchMtx;Before #330, step 2 returned immediately, so no cycle existed.
This PR keeps #330's UAF fix but makes the unregister barrier per-handler instead
of global, so callbacks run with no events-service lock held again and a blocking
handler only ever delays unregister of itself:
handleEvent()records each handler it is about to run (with the dispatchingthread) in
mHandlersInFlight, in the samemHandlerMapMtxcritical section asthe snapshot;
unregisterEventsHandler(h)erasesh, then waits only untilhis no longerrunning on another thread.
Race-free (mark and erase serialize on
mHandlerMapMtx), deadlock-free (lock ordermHandlerMapMtx → mDispatchStateMtx, callbacks hold no lock), exception-safe.Touches only
rseventsservice.{cc,h}.