From 331c50ab0ec139d9e7b1314bedebda6ef0cc8ef8 Mon Sep 17 00:00:00 2001 From: Dennis Klein Date: Wed, 10 Jun 2026 16:15:31 +0200 Subject: [PATCH] ci: add spack package for a tsan-instrumented libstdc++ - gcc ships no supported switch to build libstdc++ with -fsanitize=thread, and spack's gcc recipe filters all flags out of the target-library build (CXXFLAGS_FOR_TARGET is owned by its generated --with-build-config=spack makefile), so provide a dedicated libstdcxx-tsan package in a custom repo - build only the libstdc++-v3 subtree from the matching gcc release tarball, configured standalone against the already-installed toolchain (recipe modeled on https://iree.dev/developers/debugging/sanitizers/), instead of rebuilding all of gcc - the result is a drop-in runtime replacement for the compiler's libstdc++ (same soname and symbol versions), to be loaded only by the instrumented test executables - normalize the install layout after make install: the standalone build puts the runtime libraries into the multilib os dir (lib64 on x86_64) regardless of --libdir, and --with-toolexeclibdir only applies to cross builds - register the repo in the setup-deps action before creating the env --- .github/actions/setup-deps/action.yml | 1 + .../packages/libstdcxx_tsan/package.py | 90 +++++++++++++++++++ test/ci/spack_repo/fairmq_ci/repo.yaml | 3 + 3 files changed, 94 insertions(+) create mode 100644 test/ci/spack_repo/fairmq_ci/packages/libstdcxx_tsan/package.py create mode 100644 test/ci/spack_repo/fairmq_ci/repo.yaml diff --git a/.github/actions/setup-deps/action.yml b/.github/actions/setup-deps/action.yml index 2a78282d..9c7c0eeb 100644 --- a/.github/actions/setup-deps/action.yml +++ b/.github/actions/setup-deps/action.yml @@ -69,6 +69,7 @@ runs: shell: spack-bash {0} run: | echo "::group::Install dependencies" + spack repo add "$GITHUB_WORKSPACE/test/ci/spack_repo/fairmq_ci" spack env create fairmq test/ci/spack-${{ inputs.env }}.yaml spack -e fairmq add gcc@${{ inputs.gcc }} spack -e fairmq config add "packages:all:require:'%gcc@${{ inputs.gcc }}'" diff --git a/test/ci/spack_repo/fairmq_ci/packages/libstdcxx_tsan/package.py b/test/ci/spack_repo/fairmq_ci/packages/libstdcxx_tsan/package.py new file mode 100644 index 00000000..e91b9a0f --- /dev/null +++ b/test/ci/spack_repo/fairmq_ci/packages/libstdcxx_tsan/package.py @@ -0,0 +1,90 @@ +import os +import shutil + +from spack_repo.builtin.build_systems.generic import Package + +from spack.package import * + + +class LibstdcxxTsan(Package): + """ThreadSanitizer-instrumented build of GCC's libstdc++. + + GCC ships no supported way to produce a tsan-instrumented libstdc++ (and + spack's gcc package offers no hook into the target-library flags), so this + package builds only the libstdc++-v3 subtree from the GCC release tarball + matching the toolchain, with -fsanitize=thread. The result is a drop-in + runtime replacement for the compiler's own libstdc++ (same soname and + symbol versions) that lets tsan observe the synchronization inside the + C++ standard library (e.g. std::locale/std::regex lazy initialization). + + Select it via LD_LIBRARY_PATH in the environment of the instrumented + executables only. Like any shared library built with gcc + -fsanitize=thread it carries unresolved __tsan_* symbols satisfied by the + executable's libtsan, so loading it into an uninstrumented executable + fails. + + Recipe modeled on https://iree.dev/developers/debugging/sanitizers/. + """ + + homepage = "https://gcc.gnu.org/onlinedocs/libstdc++/" + url = "https://ftp.gnu.org/gnu/gcc/gcc-15.2.0/gcc-15.2.0.tar.xz" + + # Keep in lockstep with the gcc version used by the tsan CI job: a + # libstdc++ older than the compiler may lack GLIBCXX symbol versions + # that binaries built by that compiler reference. Checksums match the + # version() directives in spack's gcc package. + version("15.2.0", sha256="438fd996826b0c82485a29da03a72d71d6e3541a83ec702df4271f6fe025d24e") + version("14.3.0", sha256="e0dc77297625631ac8e50fa92fffefe899a4eb702592da5c32ef04e2293aca3a") + + depends_on("c", type="build") + depends_on("cxx", type="build") + depends_on("gmake", type="build") + + def install(self, spec, prefix): + # gthr-default.h only materializes in a configured libgcc build dir; + # on POSIX targets it is a verbatim copy of gthr-posix.h. Providing it + # up front spares building libgcc (and the in-tree compiler). + gthr_include = join_path(self.stage.source_path, "spack-gthr") + mkdirp(gthr_include) + copy( + join_path(self.stage.source_path, "libgcc", "gthr-posix.h"), + join_path(gthr_include, "gthr-default.h"), + ) + + flags = f"-I{gthr_include} -O2 -g -fno-omit-frame-pointer -fsanitize=thread" + build_dir = join_path(self.stage.source_path, "spack-build") + mkdirp(build_dir) + with working_dir(build_dir): + configure = Executable( + join_path(self.stage.source_path, "libstdc++-v3", "configure") + ) + configure( + f"--prefix={prefix}", + f"--libdir={prefix.lib}", + "--disable-multilib", + "--disable-libstdcxx-pch", + "--enable-libstdcxx-threads=yes", + "--with-default-libstdcxx-abi=new", + f"CFLAGS={flags}", + f"CXXFLAGS={flags}", + "LDFLAGS=-fsanitize=thread", + ) + make() + make("install") + + # The runtime libraries land in the multilib os dir (lib64 on + # x86_64), --libdir notwithstanding (--with-toolexeclibdir only + # applies to cross builds); normalize to lib/ for the consumer. + if os.path.isdir(prefix.lib64): + mkdirp(prefix.lib) + for entry in os.listdir(prefix.lib64): + shutil.move(join_path(prefix.lib64, entry), join_path(prefix.lib, entry)) + os.rmdir(prefix.lib64) + + # Only the runtime library is consumed -- compilation uses the + # compiler's own (identical) headers. Drop the rest to keep the + # buildcache entry small. + for directory in ("include", "share"): + path = join_path(prefix, directory) + if os.path.isdir(path): + shutil.rmtree(path) diff --git a/test/ci/spack_repo/fairmq_ci/repo.yaml b/test/ci/spack_repo/fairmq_ci/repo.yaml new file mode 100644 index 00000000..8d96cd9e --- /dev/null +++ b/test/ci/spack_repo/fairmq_ci/repo.yaml @@ -0,0 +1,3 @@ +repo: + namespace: fairmq_ci + api: v2.0