fix(linux): make the render-executor completion handoff visible to TSan

GLib's GMutex is a raw futex, so ThreadSanitizer cannot see the
happens-before edge g_main_context_invoke_full provides and fails the
thread lane on plane_render_executor_test. Publish the completion
payload through an explicit release/acquire pair, and suppress the
remaining reports that originate wholly inside libglib's own source
bookkeeping. Races on repository memory still fail the run; verified
0/100 failing TSan runs in an ubuntu:24.04 container (20/20 before).
This commit is contained in:
edde746
2026-08-24 12:42:23 +02:00
parent fb85bb279a
commit 0fdc22b4fa
3 changed files with 38 additions and 3 deletions
+7
View File
@@ -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()
+19 -3
View File
@@ -1,5 +1,6 @@
#include "plane_render_executor.h"
#include <atomic>
#include <chrono>
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<bool> published{false};
};
gboolean InvokeCompletion(gpointer data) {
auto* invocation = static_cast<CompletionInvocation*>(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<CompletionInvocation*>(data); }
void DestroyCompletionInvocation(gpointer data) {
auto* invocation = static_cast<CompletionInvocation*>(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>& 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);
}
{
+12
View File
@@ -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