From 6f9d513cdf9287b5ede7b3a3be6761f6932ebb8a Mon Sep 17 00:00:00 2001 From: Pia Baltazar Date: Mon, 13 Jul 2026 03:20:59 -0400 Subject: [PATCH 1/2] Scenario: guard TimeSync execution component against use after cleanup cleanup() resets m_ossia_node while the component's destruction is deferred through the execution queue. Model edits arriving in that window (autotrigger toggles, trigger/expression changes, quantization changes, GUI trigger) would then dereference a null node in the exec lambda, or trip SCORE_ASSERT in updateTrigger(). Bail out early when m_ossia_node is null. --- .../Document/TimeSync/TimeSyncExecution.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/plugins/score-plugin-scenario/Scenario/Document/TimeSync/TimeSyncExecution.cpp b/src/plugins/score-plugin-scenario/Scenario/Document/TimeSync/TimeSyncExecution.cpp index 40829e71a1..e3de4b4f54 100644 --- a/src/plugins/score-plugin-scenario/Scenario/Document/TimeSync/TimeSyncExecution.cpp +++ b/src/plugins/score-plugin-scenario/Scenario/Document/TimeSync/TimeSyncExecution.cpp @@ -35,8 +35,13 @@ TimeSyncComponent::TimeSyncComponent( con(element, &Scenario::TimeSyncModel::activeChanged, this, &TimeSyncComponent::updateTrigger); - con(element, &Scenario::TimeSyncModel::autotriggerChanged, this, - [this](bool b) { in_exec([ts = m_ossia_node, b] { ts->set_autotrigger(b); }); }); + con(element, &Scenario::TimeSyncModel::autotriggerChanged, this, [this](bool b) { + // cleanup() resets m_ossia_node while destruction is deferred through the + // exec queue, so model edits can still arrive afterwards: ignore them. + if(!m_ossia_node) + return; + in_exec([ts = m_ossia_node, b] { ts->set_autotrigger(b); }); + }); con(element, &Scenario::TimeSyncModel::triggerChanged, this, &TimeSyncComponent::updateTrigger); @@ -173,7 +178,8 @@ void TimeSyncComponent::updateTrigger() start = m_score_node->isStartPoint(); } - SCORE_ASSERT(m_ossia_node); + if(!m_ossia_node) + return; in_exec([e = m_ossia_node, exp_ptr, autotrigger, start] { bool was_observing = e->is_observing_expression(); if(was_observing) @@ -191,6 +197,8 @@ void TimeSyncComponent::updateTrigger() void TimeSyncComponent::updateTriggerTime() { OSSIA_ENSURE_CURRENT_THREAD_KIND(ossia::thread_type::Ui); + if(!m_ossia_node) + return; ossia::musical_sync quantRate = m_score_node->musicalSync(); if(quantRate < 0) { @@ -222,6 +230,8 @@ void TimeSyncComponent::updateTriggerTime() void TimeSyncComponent::on_GUITrigger() { OSSIA_ENSURE_CURRENT_THREAD_KIND(ossia::thread_type::Ui); + if(!m_ossia_node) + return; in_exec([e = m_ossia_node] { OSSIA_ENSURE_CURRENT_THREAD_KIND(ossia::thread_type::Audio); e->start_trigger_request(); From 99835c50f14c309f94a31fc73fe642775be96da0 Mon Sep 17 00:00:00 2001 From: Pia Baltazar Date: Sun, 26 Jul 2026 18:59:17 -0400 Subject: [PATCH 2/2] Scenario: sever model connections in TimeSync cleanup instead of guarding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review rework: restore SCORE_ASSERT(m_ossia_node) and drop the null guards. Root cause of the post-cleanup calls: cleanup() resets m_ossia_node immediately, but the component's destruction is deferred through the exec queue and then the gc queue while the model stays alive and editable — so a trigger/autotrigger/quantization edit after stop still fired the ctor connections into the half-dead component. Disconnecting the model from the component in cleanup() severs every such path (all guarded methods are reachable only through those connections), keeping the invariant instead of widening the code paths. Co-Authored-By: Claude --- .../Document/TimeSync/TimeSyncExecution.cpp | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/plugins/score-plugin-scenario/Scenario/Document/TimeSync/TimeSyncExecution.cpp b/src/plugins/score-plugin-scenario/Scenario/Document/TimeSync/TimeSyncExecution.cpp index e3de4b4f54..31a234afca 100644 --- a/src/plugins/score-plugin-scenario/Scenario/Document/TimeSync/TimeSyncExecution.cpp +++ b/src/plugins/score-plugin-scenario/Scenario/Document/TimeSync/TimeSyncExecution.cpp @@ -35,13 +35,8 @@ TimeSyncComponent::TimeSyncComponent( con(element, &Scenario::TimeSyncModel::activeChanged, this, &TimeSyncComponent::updateTrigger); - con(element, &Scenario::TimeSyncModel::autotriggerChanged, this, [this](bool b) { - // cleanup() resets m_ossia_node while destruction is deferred through the - // exec queue, so model edits can still arrive afterwards: ignore them. - if(!m_ossia_node) - return; - in_exec([ts = m_ossia_node, b] { ts->set_autotrigger(b); }); - }); + con(element, &Scenario::TimeSyncModel::autotriggerChanged, this, + [this](bool b) { in_exec([ts = m_ossia_node, b] { ts->set_autotrigger(b); }); }); con(element, &Scenario::TimeSyncModel::triggerChanged, this, &TimeSyncComponent::updateTrigger); @@ -53,6 +48,11 @@ TimeSyncComponent::TimeSyncComponent( void TimeSyncComponent::cleanup(const std::shared_ptr& self) { OSSIA_ENSURE_CURRENT_THREAD_KIND(ossia::thread_type::Ui); + // Destruction is deferred through the exec then gc queues while the model + // may stay alive and editable: sever the model connections so no edit can + // reach this component once m_ossia_node is reset. + if(m_score_node) + QObject::disconnect(m_score_node, nullptr, this, nullptr); in_exec([self, ts = m_ossia_node, gcq_ptr = weak_gc] { OSSIA_ENSURE_CURRENT_THREAD_KIND(ossia::thread_type::Audio); ts->cleanup(); @@ -178,8 +178,7 @@ void TimeSyncComponent::updateTrigger() start = m_score_node->isStartPoint(); } - if(!m_ossia_node) - return; + SCORE_ASSERT(m_ossia_node); in_exec([e = m_ossia_node, exp_ptr, autotrigger, start] { bool was_observing = e->is_observing_expression(); if(was_observing) @@ -197,8 +196,6 @@ void TimeSyncComponent::updateTrigger() void TimeSyncComponent::updateTriggerTime() { OSSIA_ENSURE_CURRENT_THREAD_KIND(ossia::thread_type::Ui); - if(!m_ossia_node) - return; ossia::musical_sync quantRate = m_score_node->musicalSync(); if(quantRate < 0) { @@ -230,8 +227,6 @@ void TimeSyncComponent::updateTriggerTime() void TimeSyncComponent::on_GUITrigger() { OSSIA_ENSURE_CURRENT_THREAD_KIND(ossia::thread_type::Ui); - if(!m_ossia_node) - return; in_exec([e = m_ossia_node] { OSSIA_ENSURE_CURRENT_THREAD_KIND(ossia::thread_type::Audio); e->start_trigger_request();