From 5411bf906ded4f6df09624d2a34072b3df25b04f Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Wed, 15 Apr 2026 17:04:55 -0700 Subject: [PATCH 1/2] Protect test hook callback with mutex for TSan safety The beforeWaitCallback global std::function was accessed from multiple threads without synchronization. Add a dedicated mutex and use copy-under-lock, invoke-after-unlock pattern to avoid holding the lock during arbitrary callback execution. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../threading/blocking_concurrent_queue.h | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Source/Shared/arcana/threading/blocking_concurrent_queue.h b/Source/Shared/arcana/threading/blocking_concurrent_queue.h index 24d5276..b01dd8b 100644 --- a/Source/Shared/arcana/threading/blocking_concurrent_queue.h +++ b/Source/Shared/arcana/threading/blocking_concurrent_queue.h @@ -21,6 +21,7 @@ namespace arcana { namespace detail { + inline std::mutex callbackMutex; inline std::function beforeWaitCallback{[]() {}}; } @@ -29,6 +30,7 @@ namespace arcana // lost-wakeup race conditions. Pass an empty lambda [](){} to reset. inline void set_before_wait_callback(std::function callback) { + std::lock_guard lock{detail::callbackMutex}; detail::beforeWaitCallback = std::move(callback); } } @@ -121,7 +123,14 @@ namespace arcana while (!cancel.cancelled() && m_data.empty()) { #ifdef ARCANA_TEST_HOOKS - test_hooks::blocking_concurrent_queue::detail::beforeWaitCallback(); + { + std::function cb; + { + std::lock_guard cbLock{test_hooks::blocking_concurrent_queue::detail::callbackMutex}; + cb = test_hooks::blocking_concurrent_queue::detail::beforeWaitCallback; + } + cb(); + } #endif m_dataReady.wait(lock); } @@ -145,7 +154,14 @@ namespace arcana while (!cancel.cancelled() && m_data.empty()) { #ifdef ARCANA_TEST_HOOKS - test_hooks::blocking_concurrent_queue::detail::beforeWaitCallback(); + { + std::function cb; + { + std::lock_guard cbLock{test_hooks::blocking_concurrent_queue::detail::callbackMutex}; + cb = test_hooks::blocking_concurrent_queue::detail::beforeWaitCallback; + } + cb(); + } #endif m_dataReady.wait(lock); } From 761f88f914a27566c6ad0f271d357918a7e5e1b9 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Thu, 16 Apr 2026 09:00:46 -0700 Subject: [PATCH 2/2] Factor invoke_before_wait_callback into detail helper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../threading/blocking_concurrent_queue.h | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/Source/Shared/arcana/threading/blocking_concurrent_queue.h b/Source/Shared/arcana/threading/blocking_concurrent_queue.h index b01dd8b..15eda2d 100644 --- a/Source/Shared/arcana/threading/blocking_concurrent_queue.h +++ b/Source/Shared/arcana/threading/blocking_concurrent_queue.h @@ -23,6 +23,18 @@ namespace arcana { inline std::mutex callbackMutex; inline std::function beforeWaitCallback{[]() {}}; + + // Copy the callback under the lock and invoke after releasing, avoiding + // potential deadlocks if the callback interacts with other locks. + inline void invoke_before_wait_callback() + { + std::function cb; + { + std::lock_guard lock{callbackMutex}; + cb = beforeWaitCallback; + } + cb(); + } } // Set a callback to be invoked while holding the queue mutex, right before @@ -123,14 +135,7 @@ namespace arcana while (!cancel.cancelled() && m_data.empty()) { #ifdef ARCANA_TEST_HOOKS - { - std::function cb; - { - std::lock_guard cbLock{test_hooks::blocking_concurrent_queue::detail::callbackMutex}; - cb = test_hooks::blocking_concurrent_queue::detail::beforeWaitCallback; - } - cb(); - } + test_hooks::blocking_concurrent_queue::detail::invoke_before_wait_callback(); #endif m_dataReady.wait(lock); } @@ -154,14 +159,7 @@ namespace arcana while (!cancel.cancelled() && m_data.empty()) { #ifdef ARCANA_TEST_HOOKS - { - std::function cb; - { - std::lock_guard cbLock{test_hooks::blocking_concurrent_queue::detail::callbackMutex}; - cb = test_hooks::blocking_concurrent_queue::detail::beforeWaitCallback; - } - cb(); - } + test_hooks::blocking_concurrent_queue::detail::invoke_before_wait_callback(); #endif m_dataReady.wait(lock); }