diff --git a/linux/runner/CMakeLists.txt b/linux/runner/CMakeLists.txt index 31a45469c..d72810f50 100644 --- a/linux/runner/CMakeLists.txt +++ b/linux/runner/CMakeLists.txt @@ -232,4 +232,11 @@ if(PLEZY_BUILD_MPV_RELIABILITY_TESTS) target_link_libraries(plane_render_executor_test PRIVATE PkgConfig::GTK Threads::Threads) apply_mpv_reliability_sanitizer(plane_render_executor_test) add_test(NAME plane_render_executor_test COMMAND plane_render_executor_test) + if(PLEZY_MPV_RELIABILITY_SANITIZER STREQUAL "thread") + # GLib's futex-based GMutex is invisible to TSan; suppress reports that + # originate inside libglib. Inert when the sanitizer probe fell back to an + # unsanitized build. See mpv/tsan_suppressions.txt. + set_tests_properties(plane_render_executor_test PROPERTIES ENVIRONMENT + "TSAN_OPTIONS=suppressions=${CMAKE_CURRENT_SOURCE_DIR}/mpv/tsan_suppressions.txt") + endif() endif() diff --git a/linux/runner/mpv/plane_render_executor.cc b/linux/runner/mpv/plane_render_executor.cc index 154b59491..f98f92c00 100644 --- a/linux/runner/mpv/plane_render_executor.cc +++ b/linux/runner/mpv/plane_render_executor.cc @@ -1,5 +1,6 @@ #include "plane_render_executor.h" +#include #include namespace mpv { @@ -9,15 +10,29 @@ namespace { struct CompletionInvocation { PlaneRenderExecutor::Completion completion; bool result; + // The worker hands this struct to the main context through + // g_main_context_invoke_full, whose internal context lock is a GLib futex + // that ThreadSanitizer cannot observe. This release/acquire pair republishes + // that same happens-before edge in a form TSan models; GLib still provides + // the delivery ordering. + std::atomic published{false}; }; gboolean InvokeCompletion(gpointer data) { auto* invocation = static_cast(data); + // Pairs with the release store in Run(); see CompletionInvocation. + (void)invocation->published.load(std::memory_order_acquire); invocation->completion(invocation->result); return G_SOURCE_REMOVE; } -void DestroyCompletionInvocation(gpointer data) { delete static_cast(data); } +void DestroyCompletionInvocation(gpointer data) { + auto* invocation = static_cast(data); + // The destroy notify can also be the first main-thread access when the + // source is torn down without dispatching; take the same edge. + (void)invocation->published.load(std::memory_order_acquire); + delete invocation; +} } // namespace @@ -84,9 +99,10 @@ void PlaneRenderExecutor::Run(const std::shared_ptr& shared) { const bool result = job ? job() : false; if (completion) { + auto* invocation = new CompletionInvocation{std::move(completion), result}; + invocation->published.store(true, std::memory_order_release); g_main_context_invoke_full( - shared->completion_context, G_PRIORITY_DEFAULT, InvokeCompletion, - new CompletionInvocation{std::move(completion), result}, DestroyCompletionInvocation); + shared->completion_context, G_PRIORITY_DEFAULT, InvokeCompletion, invocation, DestroyCompletionInvocation); } { diff --git a/linux/runner/mpv/tsan_suppressions.txt b/linux/runner/mpv/tsan_suppressions.txt new file mode 100644 index 000000000..c4e740eaf --- /dev/null +++ b/linux/runner/mpv/tsan_suppressions.txt @@ -0,0 +1,12 @@ +# ThreadSanitizer suppressions for the focused mpv reliability tests. +# +# GLib's GMutex is a raw futex, not a pthread mutex, so TSan cannot observe +# the happens-before edges GMainContext provides. Any cross-thread handoff +# through g_main_context_invoke_full then reports races inside GLib's own +# source/queue bookkeeping (g_malloc vs free of idle sources, etc.). +# +# This only hides reports whose racing access originates inside libglib; +# races on this repository's memory still fail the run. The executor's own +# completion handoff carries an explicit release/acquire pair (see +# plane_render_executor.cc) precisely so it stays visible to TSan. +called_from_lib:libglib-2.0.so