From 2bd9a072a9d43d2569104e0c5a0dad899711f1d6 Mon Sep 17 00:00:00 2001 From: Dennis Klein Date: Wed, 10 Jun 2026 16:14:15 +0200 Subject: [PATCH] test: pre-fill libstdc++ ctype caches before threads exist - std::ctype caches narrow()/widen() results per character in plain char arrays of the global classic-locale facet, written without synchronization from header-inlined code (locale_facets.h); two threads exercising an uncached character concurrently (e.g. compiling a std::regex in Channel::Validate) constitute a true data race that ThreadSanitizer rightfully reports - the stores are real and unsynchronized, so a tsan-instrumented libstdc++ cannot help here; instead fill the caches before any thread is spawned, which turns every later access into a pure read - warm the lazily-installed num_put/num_get caches used by stream insertion/extraction as well, via a small format/parse round-trip - wire the warm-up into the gtest runner main() and, via a static initializer, into the test device runner --- test/CMakeLists.txt | 1 + test/helper/LocaleWarmup.h | 63 +++++++++++++++++++++++++++++++++++ test/helper/runTestDevice.cxx | 5 +++ test/runner.cxx.in | 2 ++ 4 files changed, 71 insertions(+) create mode 100644 test/helper/LocaleWarmup.h diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 52721c3b..779e826d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -32,6 +32,7 @@ endif() add_testhelper(runTestDevice SOURCES helper/runTestDevice.cxx + helper/LocaleWarmup.h helper/devices/TestPairLeft.h helper/devices/TestPairRight.h helper/devices/TestPollIn.h diff --git a/test/helper/LocaleWarmup.h b/test/helper/LocaleWarmup.h new file mode 100644 index 00000000..d73dfc42 --- /dev/null +++ b/test/helper/LocaleWarmup.h @@ -0,0 +1,63 @@ +/******************************************************************************** + * Copyright (C) 2026 GSI Helmholtzzentrum fuer Schwerionenforschung GmbH * + * * + * This software is distributed under the terms of the * + * GNU Lesser General Public Licence (LGPL) version 3, * + * copied verbatim in the file "LICENSE" * + ********************************************************************************/ + +#ifndef FAIR_MQ_TEST_LOCALEWARMUP_H +#define FAIR_MQ_TEST_LOCALEWARMUP_H + +#include +#include + +namespace fair::mq::test { + +/// libstdc++'s std::ctype caches narrow()/widen() results per character +/// in plain char arrays of the global classic-locale facet -- by design with +/// unsynchronized writes, emitted from header-inlined code (locale_facets.h). +/// Whenever two threads exercise an uncached character concurrently (e.g. by +/// compiling a std::regex), ThreadSanitizer rightfully reports the write as a +/// data race. The stores are real and unsynchronized, so instrumenting +/// libstdc++ cannot help; instead, fill the caches before any thread exists, +/// which turns every later access into a pure read. +inline void WarmUpLocaleCaches() +{ + auto const& ct = std::use_facet>(std::locale::classic()); + for (int c = 1; c < 256; ++c) { + auto const ch = static_cast(c); + ct.narrow(ch, '\0'); + ct.widen(ch); + } + + // Only the range overload runs _M_narrow_init(), which sets the + // all-cached flag (_M_narrow_ok); the single-char loop above fills the + // cache without it. (widen() needs no equivalent: the single-char + // overload already runs _M_widen_init().) + char from[256]; + char to[256]; + for (int c = 0; c < 256; ++c) { + from[c] = static_cast(c); + } + ct.narrow(from, from + 256, '?', to); + + // num_put/num_get caches used by stream insertion/extraction + std::ostringstream os; + os << 4711 << ' ' << 3.14; + std::istringstream is("42 3.14"); + int i = 0; + double d = 0.; + is >> i >> d; +} + +/// For binaries that do not own main(): define a namespace-scope instance to +/// run the warm-up during static initialization, before main(). +struct LocaleWarmup +{ + LocaleWarmup() { WarmUpLocaleCaches(); } +}; + +} // namespace fair::mq::test + +#endif /* FAIR_MQ_TEST_LOCALEWARMUP_H */ diff --git a/test/helper/runTestDevice.cxx b/test/helper/runTestDevice.cxx index e94300b2..e757938a 100644 --- a/test/helper/runTestDevice.cxx +++ b/test/helper/runTestDevice.cxx @@ -6,6 +6,7 @@ * copied verbatim in the file "LICENSE" * ********************************************************************************/ +#include "LocaleWarmup.h" #include "devices/TestPairLeft.h" #include "devices/TestPairRight.h" #include "devices/TestPollIn.h" @@ -30,6 +31,10 @@ namespace bpo = boost::program_options; +namespace { +[[maybe_unused]] fair::mq::test::LocaleWarmup const gLocaleWarmup{}; +} + auto addCustomOptions(bpo::options_description& options) -> void { options.add_options() diff --git a/test/runner.cxx.in b/test/runner.cxx.in index 4d426187..2b67a93a 100644 --- a/test/runner.cxx.in +++ b/test/runner.cxx.in @@ -10,6 +10,7 @@ #include #include +#include #include @@ -24,6 +25,7 @@ string runTestDevice = "@RUN_TEST_DEVICE@"; int main(int argc, char** argv) { + fair::mq::test::WarmUpLocaleCaches(); ::testing::InitGoogleTest(&argc, argv); ::testing::FLAGS_gtest_death_test_style = "threadsafe"; setenv("FAIRMQ_PATH", FAIRMQ_TEST_ENVIRONMENT, 0);