@@ -121,6 +121,45 @@ inline size_t full_graph_payload_size(const DSR::LWWGraphSnapshot& sample)
121121{
122122 return sample.nodes .size () + sample.edges .size ();
123123}
124+
125+ // ── Deferred signal emission (fix for the bottle-join heap-corruption data race) ─────────────────
126+ // Remote deltas are applied on the MAIN thread (the subscription functors marshal via
127+ // QMetaObject::invokeMethod) while holding unique_lock(_mutex). Emitting DSR Qt signals there is
128+ // hazardous twice over: (1) a queued std::string payload emitted from a worker thread races the
129+ // receiver's QMetaType marshaling (TSan-confirmed; corrupts the heap, fatal in heavy-heap consumers
130+ // like voxelizer); (2) emitting WHILE holding _mutex lets a same-thread re-entrant slot deadlock
131+ // (EDEADLK). So during a remote apply we DEFER every emit into this thread-local buffer and flush it
132+ // AFTER releasing the lock, on the main thread — no cross-thread payload, no re-entrant deadlock.
133+ // Outside a deferred apply (defer depth 0, e.g. local writes) emits fire immediately as before.
134+ thread_local int tl_defer_depth = 0 ;
135+ thread_local std::vector<std::function<void ()>> tl_deferred_signals;
136+
137+ void defer_or_emit (std::function<void ()> emit_fn)
138+ {
139+ if (tl_defer_depth > 0 )
140+ tl_deferred_signals.push_back (std::move (emit_fn));
141+ else
142+ emit_fn ();
143+ }
144+
145+ // RAII for a remote-delta apply: turn on deferral for the duration, then on destruction (which the
146+ // caller arranges to happen AFTER the unique_lock block has ended) flush the buffered emits. So the
147+ // signal emits fire on the main thread, after the graph lock is released.
148+ struct RemoteApplyScope
149+ {
150+ RemoteApplyScope () { ++tl_defer_depth; }
151+ ~RemoteApplyScope ()
152+ {
153+ if (--tl_defer_depth == 0 )
154+ {
155+ auto pending = std::move (tl_deferred_signals);
156+ tl_deferred_signals.clear ();
157+ for (auto & f : pending) f ();
158+ }
159+ }
160+ RemoteApplyScope (const RemoteApplyScope&) = delete ;
161+ RemoteApplyScope& operator =(const RemoteApplyScope&) = delete ;
162+ };
124163}
125164
126165void print_sample_info (DSR ::GraphSettings::LOGLEVEL log_level, const DSR ::Transport::ReceivedSampleInfo& info);
@@ -340,10 +379,17 @@ DSRGraph::SampleCallback<Sample> DSRGraph::make_node_subscription_functor_impl(c
340379 qDebug () << name << " Received:" << std::to_string (sample.id ).c_str () << " node from: "
341380 << m_info.source_entity_id ;
342381 }
343- tp.spawn_task ([this , sample = std::move (sample)]() mutable {
344- std::unique_lock<std::shared_mutex> lock (_mutex);
345- engine_->apply_remote_node_delta (NodeDeltaMessage{std::move (sample)});
346- });
382+ // Apply on the graph's (main) thread with deferred emit: marshal to main so the apply +
383+ // (deferred) signal emits run on the main/GUI thread — no std::string Qt payload crosses
384+ // a thread boundary (the bottle-join heap-corruption race) — and RemoteApplyScope flushes
385+ // the emits AFTER the unique_lock block ends (no re-entrant EDEADLK). Sample is MOVED in.
386+ QMetaObject::invokeMethod (this , [this , sample = std::move (sample)]() mutable {
387+ RemoteApplyScope defer;
388+ {
389+ std::unique_lock<std::shared_mutex> lock (_mutex);
390+ engine_->apply_remote_node_delta (NodeDeltaMessage{std::move (sample)});
391+ }
392+ }, Qt::QueuedConnection);
347393 }
348394 catch (const std::exception &ex) { std::cerr << ex.what () << std::endl; }
349395 };
@@ -366,10 +412,14 @@ DSRGraph::SampleCallback<Sample> DSRGraph::make_edge_subscription_functor_impl(c
366412 if (!network_compatibility_or_fatal (channel, sample.protocol_version , sample.sync_mode , sync_mode)) {
367413 return ;
368414 }
369- tp.spawn_task ([this , sample = std::move (sample)]() mutable {
370- std::unique_lock<std::shared_mutex> lock (_mutex);
371- engine_->apply_remote_edge_delta (EdgeDeltaMessage{std::move (sample)});
372- });
415+ // Apply on the graph's (main) thread with deferred emit — see apply_remote_node_delta note.
416+ QMetaObject::invokeMethod (this , [this , sample = std::move (sample)]() mutable {
417+ RemoteApplyScope defer;
418+ {
419+ std::unique_lock<std::shared_mutex> lock (_mutex);
420+ engine_->apply_remote_edge_delta (EdgeDeltaMessage{std::move (sample)});
421+ }
422+ }, Qt::QueuedConnection);
373423 }
374424 catch (const std::exception &ex) { std::cerr << ex.what () << std::endl; }
375425 };
@@ -396,11 +446,15 @@ DSRGraph::SampleCallback<Batch> DSRGraph::make_edge_attrs_subscription_functor_i
396446 qDebug () << name << " Received:" << samples.vec .size () << " edge attr from: "
397447 << m_info.source_entity_id ;
398448 }
399- tp_delta_attr.spawn_task ([this , samples = std::move (samples)]() mutable {
449+ // Apply on the graph's (main) thread with deferred emit — see apply_remote_node_delta note.
450+ QMetaObject::invokeMethod (this , [this , samples = std::move (samples)]() mutable {
400451 CORTEX_PROFILE_ZONE_N (" DSRGraph::edge_attrs_subscription_thread apply batch" );
401- std::unique_lock<std::shared_mutex> lock (_mutex);
402- engine_->apply_remote_edge_attr_batch (EdgeAttrDeltaBatchMessage{std::move (samples)});
403- });
452+ RemoteApplyScope defer;
453+ {
454+ std::unique_lock<std::shared_mutex> lock (_mutex);
455+ engine_->apply_remote_edge_attr_batch (EdgeAttrDeltaBatchMessage{std::move (samples)});
456+ }
457+ }, Qt::QueuedConnection);
404458 }
405459 catch (const std::exception &ex) { std::cerr << ex.what () << std::endl; }
406460 };
@@ -427,11 +481,15 @@ DSRGraph::SampleCallback<Batch> DSRGraph::make_node_attrs_subscription_functor_i
427481 qDebug () << name << " Received:" << samples.vec .size () << " node attrs from: "
428482 << m_info.source_entity_id ;
429483 }
430- tp_delta_attr.spawn_task ([this , samples = std::move (samples)]() mutable {
484+ // Apply on the graph's (main) thread with deferred emit — see apply_remote_node_delta note.
485+ QMetaObject::invokeMethod (this , [this , samples = std::move (samples)]() mutable {
431486 CORTEX_PROFILE_ZONE_N (" DSRGraph::node_attrs_subscription_thread apply batch" );
432- std::unique_lock<std::shared_mutex> lock (_mutex);
433- engine_->apply_remote_node_attr_batch (NodeAttrDeltaBatchMessage{std::move (samples)});
434- });
487+ RemoteApplyScope defer;
488+ {
489+ std::unique_lock<std::shared_mutex> lock (_mutex);
490+ engine_->apply_remote_node_attr_batch (NodeAttrDeltaBatchMessage{std::move (samples)});
491+ }
492+ }, Qt::QueuedConnection);
435493 }
436494 catch (const std::exception &ex) { std::cerr << ex.what () << std::endl; }
437495 };
@@ -1052,46 +1110,57 @@ void DSRGraph::for_each_edge_of_type_cache(const std::string& type, std::functio
10521110 }
10531111}
10541112
1113+ // NOTE: these run during a remote-delta apply (on the main thread, under unique_lock(_mutex)).
1114+ // defer_or_emit buffers each emit (args captured by value, produced here on main) so it fires AFTER
1115+ // the lock is released — see the deferred-emission note near the top of this file.
10551116void DSRGraph::on_remote_node_updated (uint64_t id, const std::string& type, uint32_t agent_id)
10561117{
1057- emitter.update_node_signal (id, type, SignalInfo{agent_id});
1118+ defer_or_emit ([ this , id, type, agent_id]{ emitter.update_node_signal (id, type, SignalInfo{agent_id}); });
10581119}
10591120
10601121void DSRGraph::on_remote_node_deleted (uint64_t id, const std::optional<Node>& node, const std::vector<Edge>& removed_edges, uint32_t agent_id)
10611122{
1062- emitter.del_node_signal (id, SignalInfo{agent_id});
1063- if (node.has_value ()) {
1064- emitter.deleted_node_signal (*node, SignalInfo{agent_id});
1065- }
1066- for (const auto & edge : removed_edges) {
1067- emitter.del_edge_signal (edge.from (), edge.to (), edge.type (), SignalInfo{agent_id});
1068- emitter.deleted_edge_signal (edge, SignalInfo{agent_id});
1069- }
1123+ defer_or_emit ([this , id, node, removed_edges, agent_id]{
1124+ emitter.del_node_signal (id, SignalInfo{agent_id});
1125+ if (node.has_value ()) {
1126+ emitter.deleted_node_signal (*node, SignalInfo{agent_id});
1127+ }
1128+ for (const auto & edge : removed_edges) {
1129+ emitter.del_edge_signal (edge.from (), edge.to (), edge.type (), SignalInfo{agent_id});
1130+ emitter.deleted_edge_signal (edge, SignalInfo{agent_id});
1131+ }
1132+ });
10701133}
10711134
10721135void DSRGraph::on_remote_edge_updated (uint64_t from, uint64_t to, const std::string& type, uint32_t agent_id)
10731136{
1074- emitter.update_edge_signal (from, to, type, SignalInfo{agent_id});
1137+ defer_or_emit ([ this , from, to, type, agent_id]{ emitter.update_edge_signal (from, to, type, SignalInfo{agent_id}); });
10751138}
10761139
10771140void DSRGraph::on_remote_edge_deleted (uint64_t from, uint64_t to, const std::string& type, const std::optional<Edge>& edge, uint32_t agent_id)
10781141{
1079- emitter.del_edge_signal (from, to, type, SignalInfo{agent_id});
1080- if (edge.has_value ()) {
1081- emitter.deleted_edge_signal (*edge, SignalInfo{agent_id});
1082- }
1142+ defer_or_emit ([this , from, to, type, edge, agent_id]{
1143+ emitter.del_edge_signal (from, to, type, SignalInfo{agent_id});
1144+ if (edge.has_value ()) {
1145+ emitter.deleted_edge_signal (*edge, SignalInfo{agent_id});
1146+ }
1147+ });
10831148}
10841149
10851150void DSRGraph::on_remote_node_attrs_updated (uint64_t id, const std::string& type, const std::vector<std::string>& attrs, uint32_t agent_id)
10861151{
1087- emitter.update_node_attr_signal (id, attrs, SignalInfo{agent_id});
1088- emitter.update_node_signal (id, type, SignalInfo{agent_id});
1152+ defer_or_emit ([this , id, type, attrs, agent_id]{
1153+ emitter.update_node_attr_signal (id, attrs, SignalInfo{agent_id});
1154+ emitter.update_node_signal (id, type, SignalInfo{agent_id});
1155+ });
10891156}
10901157
10911158void DSRGraph::on_remote_edge_attrs_updated (uint64_t from, uint64_t to, const std::string& type, const std::vector<std::string>& attrs, uint32_t agent_id)
10921159{
1093- emitter.update_edge_attr_signal (from, to, type, attrs, SignalInfo{agent_id});
1094- emitter.update_edge_signal (from, to, type, SignalInfo{agent_id});
1160+ defer_or_emit ([this , from, to, type, attrs, agent_id]{
1161+ emitter.update_edge_attr_signal (from, to, type, attrs, SignalInfo{agent_id});
1162+ emitter.update_edge_signal (from, to, type, SignalInfo{agent_id});
1163+ });
10951164}
10961165
10971166
0 commit comments