Skip to content

Commit c711cb7

Browse files
jolavilletteclaude
andcommitted
RsEventsService: replace global dispatch barrier with a per-handler one (follow-up to #330)
#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 #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>
1 parent dc75a6c commit c711cb7

2 files changed

Lines changed: 120 additions & 33 deletions

File tree

src/services/rseventsservice.cc

Lines changed: 78 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -183,14 +183,30 @@ std::error_condition RsEventsService::unregisterEventsHandler(
183183
}
184184
}
185185

186-
/* At this point no *future* dispatch can pick up this handler. But an
187-
* ongoing handleEvent() may still hold a copy of it in flight (callbacks are
188-
* run outside mHandlerMapMtx). Fence on mDispatchMtx so that, once we return,
189-
* the handler is guaranteed not to be executing either: callers that
190-
* unregister from their destructor (most GUI widgets) can then be destroyed
191-
* safely. Recursive mutex => when called from within a callback on the
192-
* dispatching thread this is a cheap no-op instead of a self-deadlock. */
193-
{ std::lock_guard<std::recursive_mutex> dispatchFence(mDispatchMtx); }
186+
/* The handler can no longer be picked up by a *future* dispatch (removed from
187+
* the map above under mHandlerMapMtx, which also serialises with the snapshot
188+
* in handleEvent()). It may still be running in an *ongoing* dispatch though,
189+
* because callbacks run outside mHandlerMapMtx. Wait until this specific
190+
* handler is no longer executing on any *other* thread, so a caller that
191+
* unregisters from its destructor can then be destroyed safely. We never wait
192+
* on our own thread: a handler that unregisters itself (or is re-entered via a
193+
* synchronous sendEvent) would otherwise deadlock waiting on itself, and in
194+
* that case the owner is running its own code, not being destroyed
195+
* concurrently. Only fence when we actually removed a handler. See
196+
* mHandlersInFlight doc. */
197+
if(!retval)
198+
{
199+
const std::thread::id self = std::this_thread::get_id();
200+
std::unique_lock<std::mutex> lock(mDispatchStateMtx);
201+
mDispatchStateCv.wait(lock, [&]()
202+
{
203+
auto it = mHandlersInFlight.find(hId);
204+
if(it == mHandlersInFlight.end()) return true;
205+
for(const std::thread::id& tid: it->second)
206+
if(tid != self) return false;
207+
return true;
208+
});
209+
}
194210

195211
return retval;
196212
}
@@ -238,16 +254,15 @@ void RsEventsService::handleEvent(std::shared_ptr<const RsEvent> event)
238254
return;
239255
}
240256

241-
/* Hold mDispatchMtx across the whole dispatch so unregisterEventsHandler()
242-
* can fence on it and guarantee a handler is not running once it returns
243-
* (see mDispatchMtx doc). Recursive: a callback re-entering on this same
244-
* thread (self-unregister or synchronous sendEvent) does not deadlock.
245-
* Safe against GUI teardown because handlers only post asynchronously (Qt
246-
* QueuedConnection) and never block waiting on the thread that unregisters,
247-
* so there is no lock-order cycle. */
248-
std::lock_guard<std::recursive_mutex> dispatchLock(mDispatchMtx);
257+
const std::thread::id self = std::this_thread::get_id();
249258

250-
std::list<std::function<void(std::shared_ptr<const RsEvent>)> > callbacks;
259+
/* (hId, callback) pairs to run for this event. The snapshot and the
260+
* "in-flight" bookkeeping below are both done under mHandlerMapMtx, so they
261+
* are atomic with respect to unregisterEventsHandler()'s erase: that is what
262+
* lets unregister reliably wait for an already-snapshotted callback instead
263+
* of racing it (see mHandlersInFlight doc). */
264+
std::list< std::pair< RsEventsHandlerId_t,
265+
std::function<void(std::shared_ptr<const RsEvent>)> > > callbacks;
251266
{
252267
RS_STACK_MUTEX(mHandlerMapMtx);
253268
/* It is important to NOT call the callback under mHandlerMapMtx
@@ -256,14 +271,56 @@ void RsEventsService::handleEvent(std::shared_ptr<const RsEvent> event)
256271

257272
// Call all clients that registered a callback for this event type
258273
for(auto& cbit: mHandlerMaps[static_cast<uint32_t>(event->mType)])
259-
callbacks.push_back(cbit.second);
274+
callbacks.push_back(std::make_pair(cbit.first, cbit.second));
260275

261276
/* Also call all clients that registered with NONE, meaning that they
262277
* expect all events */
263278
for(auto& cbit: mHandlerMaps[static_cast<uint32_t>(RsEventType::__NONE)])
264-
callbacks.push_back(cbit.second);
279+
callbacks.push_back(std::make_pair(cbit.first, cbit.second));
280+
281+
/* Mark every handler we are about to run as in-flight on this thread,
282+
* still under mHandlerMapMtx. */
283+
std::lock_guard<std::mutex> stateLock(mDispatchStateMtx);
284+
for(auto& cb: callbacks)
285+
mHandlersInFlight[cb.first].insert(self);
265286
}
266287

267-
for(auto& cb: callbacks)
268-
cb(event);
288+
/* Remove one in-flight mark for hId on this thread. Caller must hold
289+
* mDispatchStateMtx. */
290+
auto clearInFlight = [this, self](RsEventsHandlerId_t hId)
291+
{
292+
auto it = mHandlersInFlight.find(hId);
293+
if(it == mHandlersInFlight.end()) return;
294+
auto tit = it->second.find(self);
295+
if(tit != it->second.end()) it->second.erase(tit);
296+
if(it->second.empty()) mHandlersInFlight.erase(it);
297+
};
298+
299+
auto cbit = callbacks.begin();
300+
try
301+
{
302+
for(; cbit != callbacks.end(); ++cbit)
303+
{
304+
cbit->second(event);
305+
306+
/* Clear this handler's in-flight mark and wake any
307+
* unregisterEventsHandler() that is waiting for it. */
308+
std::lock_guard<std::mutex> stateLock(mDispatchStateMtx);
309+
clearInFlight(cbit->first);
310+
mDispatchStateCv.notify_all();
311+
}
312+
}
313+
catch(...)
314+
{
315+
/* A callback threw: clear the in-flight marks it and the not-yet-run
316+
* handlers still hold, otherwise a later unregisterEventsHandler() for
317+
* one of them would block forever. Then propagate as before: the ticking
318+
* thread installs no handler, so an async throw still terminates the
319+
* process exactly as it did previously. */
320+
std::lock_guard<std::mutex> stateLock(mDispatchStateMtx);
321+
for(; cbit != callbacks.end(); ++cbit)
322+
clearInFlight(cbit->first);
323+
mDispatchStateCv.notify_all();
324+
throw;
325+
}
269326
}

src/services/rseventsservice.h

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@
2525
#include <cstdint>
2626
#include <deque>
2727
#include <array>
28+
#include <map>
2829
#include <mutex>
30+
#include <condition_variable>
31+
#include <set>
32+
#include <thread>
2933

3034
#include "retroshare/rsevents.h"
3135
#include "util/rsthreads.h"
@@ -71,18 +75,44 @@ class RsEventsService :
7175

7276
RsMutex mHandlerMapMtx;
7377

74-
/** Held by handleEvent() for the whole duration of the callbacks dispatch
75-
* loop, so that unregisterEventsHandler() can act as a barrier: after it
76-
* returns, the removed handler is guaranteed to be neither running nor about
77-
* to start. Without this, unregister only removes the handler from the map,
78-
* but handleEvent() runs callbacks on a *copy* taken outside mHandlerMapMtx
79-
* (on purpose, to let callbacks re-enter), so a callback whose owner is
80-
* being destroyed on another thread could still fire against a dangling
81-
* object -> use-after-free (typically a SIGSEGV in qobject_cast<QThread*>
82-
* inside RsQThreadUtils::postToObject at shutdown). Recursive so that a
83-
* callback re-entering (self-unregister or synchronous sendEvent) on the
84-
* dispatching thread does not deadlock. */
85-
std::recursive_mutex mDispatchMtx;
78+
/** Per-handler in-flight barrier for unregisterEventsHandler().
79+
*
80+
* handleEvent() runs callbacks on a *copy* taken outside mHandlerMapMtx (on
81+
* purpose, so a callback may send events or unregister itself). Without a
82+
* barrier, a callback whose owner is being destroyed on another thread could
83+
* still fire against a dangling object -> use-after-free (typically a SIGSEGV
84+
* in qobject_cast<QThread*> inside RsQThreadUtils::postToObject at shutdown).
85+
*
86+
* At snapshot time (still under mHandlerMapMtx) handleEvent() records each
87+
* handler id it is about to run, together with the dispatching thread, in
88+
* mHandlersInFlight, and clears each entry as the corresponding callback
89+
* returns. unregisterEventsHandler() erases the handler from the map (so no
90+
* *future* dispatch can pick it up) and then waits on mDispatchStateCv until
91+
* that specific handler is no longer running on any *other* thread. Once it
92+
* returns, the handler is guaranteed neither running nor about to start, so a
93+
* caller that unregisters from its destructor can be destroyed safely.
94+
*
95+
* Recording under the same lock as the snapshot is what makes this race-free:
96+
* unregister either erases a handler before handleEvent() snapshots it (it is
97+
* never run) or after (its in-flight mark is already visible and unregister
98+
* waits for it).
99+
*
100+
* Unlike a single mutex held across the whole dispatch, callbacks still run
101+
* with NO events-service lock held, and a slow or deliberately blocking
102+
* handler (e.g. the synchronous passphrase / plugin-confirmation dialogs sent
103+
* through sendEvent(), which block the caller via Qt::BlockingQueuedConnection)
104+
* only ever delays unregister of *that same* handler, never of an unrelated
105+
* one -> no cross-thread deadlock between a widget teardown and a blocking
106+
* handler.
107+
*
108+
* The dispatching thread is intentionally not waited on: a callback that
109+
* unregisters itself (or is re-entered through a synchronous sendEvent) on
110+
* the dispatching thread must not deadlock waiting on itself, and in that
111+
* case the owner is running its own code, not being destroyed concurrently. */
112+
std::mutex mDispatchStateMtx;
113+
std::condition_variable mDispatchStateCv;
114+
std::map< RsEventsHandlerId_t, std::multiset<std::thread::id> >
115+
mHandlersInFlight;
86116

87117
RsEventsHandlerId_t mLastHandlerId;
88118

0 commit comments

Comments
 (0)