Skip to content

RsEventsService: replace global dispatch barrier with a per-handler one (follow-up to #330)#335

Open
jolavillette wants to merge 1 commit into
RetroShare:masterfrom
jolavillette:fix/rsevents-per-handler-barrier
Open

RsEventsService: replace global dispatch barrier with a per-handler one (follow-up to #330)#335
jolavillette wants to merge 1 commit into
RetroShare:masterfrom
jolavillette:fix/rsevents-per-handler-barrier

Conversation

@jolavillette

Copy link
Copy Markdown
Contributor

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 makes unregisterEventsHandler() 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:

  1. a background thread requests the PGP passphrase → sendEvent() runs the handler
    on that thread holding mDispatchMtx, and blocks on the GUI thread via
    Qt::BlockingQueuedConnection;
  2. the GUI thread destroys any widget → its destructor's unregisterEventsHandler()
    blocks acquiring mDispatchMtx;
  3. background waits for the GUI thread, GUI waits for the lock it holds → hard hang.

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 dispatching
    thread) in mHandlersInFlight, in the same mHandlerMapMtx critical section as
    the snapshot;
  • unregisterEventsHandler(h) erases h, then waits only until h is no longer
    running on another thread.

Race-free (mark and erase serialize on mHandlerMapMtx), deadlock-free (lock order
mHandlerMapMtx → mDispatchStateMtx, callbacks hold no lock), exception-safe.
Touches only rseventsservice.{cc,h}.

…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant