From 671ac828605f59fb326a3369f1166cffbb788344 Mon Sep 17 00:00:00 2001 From: Andrew Noyes Date: Wed, 22 Jul 2026 18:11:31 -0400 Subject: [PATCH] ci: add MemorySanitizer CI job Add build_msan_toolchain.sh to produce a tarball containing an MSan-instrumented libc++/libc++abi/libunwind toolchain, and add an msan CI job that downloads the tarball and runs the test suite under MSan. Also add USE_MSAN CMake option that disables conflicting sanitizers, switches to lld, and propagates -fsanitize=memory to the shared library and fuzz_driver targets. --- .gitea/workflows/ci.yml | 81 +++++++++++++++++++++++++++++++++++++++++ CMakeLists.txt | 38 ++++++++++++++++--- build_msan_toolchain.sh | 43 ++++++++++++++++++++++ 3 files changed, 157 insertions(+), 5 deletions(-) create mode 100755 build_msan_toolchain.sh diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index b255702..2e5bfc2 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -218,6 +218,87 @@ jobs: --link "https://minio.weaselab.dev/jenkins/conflict-set/${{ gitea.run_number }}/release-${{ matrix.arch }}/Test.xml.zst" \ | tee -a "$GITHUB_STEP_SUMMARY" + msan: + runs-on: ubuntu-latest-amd64 + env: + MSAN_VERSION: "21.1.8" + MSAN_URL: "https://minio.weaselab.dev/public/x86_64/msan-toolchain-21.1.8.tar.zst" + steps: + - uses: actions/checkout@v4 + + - uses: actions/cache@v4 + with: + path: /var/cache/apt/archives + key: apt-amd64-${{ hashFiles('.gitea/workflows/ci.yml') }} + + - name: Install dependencies + run: | + . /etc/os-release + wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc + echo "deb http://apt.llvm.org/${VERSION_CODENAME}/ llvm-toolchain-${VERSION_CODENAME}-21 main" | sudo tee /etc/apt/sources.list.d/llvm.list + sudo apt-get update -qq + sudo apt-get install -y \ + build-essential ccache clang-21 cmake libc6-dbg \ + llvm-21 lld-21 mold ninja-build python3 zstd + for tool in clang clang++ llvm-ar llvm-nm llvm-ranlib llvm-objcopy llvm-cov llvm-symbolizer lld ld.lld; do + sudo update-alternatives --install /usr/bin/${tool} ${tool} /usr/bin/${tool}-21 100 + done + + - name: Download MSan toolchain + run: | + curl -Ls "${MSAN_URL}" -o /tmp/msan-toolchain.tar.zst + sudo mkdir -p /opt/msan + sudo tar --zstd -xf /tmp/msan-toolchain.tar.zst -C /opt/msan + + - uses: actions/cache@v4 + with: + path: .ccache + key: ccache-msan-${{ gitea.sha }} + restore-keys: | + ccache-msan- + + - name: Build + run: | + export CCACHE_DIR="$GITHUB_WORKSPACE/.ccache" + rm -rf build + cmake -S . -B build -G Ninja \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DCMAKE_BUILD_TYPE=Debug \ + -DDISABLE_TSAN=ON \ + -DUSE_MSAN=ON \ + -DCMAKE_CXX_FLAGS="-fsanitize=memory -stdlib=libc++ -I/opt/msan/include/c++/v1 -L/opt/msan/lib -UNDEBUG" \ + -DCMAKE_EXE_LINKER_FLAGS="-stdlib=libc++ -Wl,-rpath,/opt/msan/lib" \ + -DCMAKE_SHARED_LINKER_FLAGS="-stdlib=libc++ -Wl,-rpath,/opt/msan/lib" + ninja -C build + ccache -s + + - name: Test + run: | + cd build + ctest --no-compress-output --test-output-size-passed 100000 --test-output-size-failed 100000 -T Test -j "$(nproc)" --timeout 300 > /dev/null + + - name: Upload test results to MinIO + if: always() + env: + MINIO_ACCESS_KEY: ${{ secrets.MINIO_ACCESS_KEY }} + MC_HOST_minio: https://${{ secrets.MINIO_ACCESS_KEY }}:${{ secrets.MINIO_SECRET_KEY }}@minio.weaselab.dev + run: | + if [ -z "$MINIO_ACCESS_KEY" ]; then + echo "MinIO credentials not configured; skipping upload" + exit 0 + fi + zstd build/Testing/*/Test.xml + mc cp build/Testing/*/Test.xml.zst "minio/jenkins/conflict-set/${{ gitea.run_number }}/msan/" + + - name: Test summary + if: always() + run: | + python3 ctest_summary.py build/Testing/*/Test.xml \ + --link "https://minio.weaselab.dev/jenkins/conflict-set/${{ gitea.run_number }}/msan/Test.xml.zst" \ + | tee -a "$GITHUB_STEP_SUMMARY" + coverage: runs-on: ubuntu-latest-amd64 steps: diff --git a/CMakeLists.txt b/CMakeLists.txt index eabecea..e4ff2df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,18 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") ${LLVM_OBJCOPY} CACHE FILEPATH "path to objcopy binary" FORCE) endif() + if(USE_MSAN) + find_program(LLD_LINKER lld) + if(LLD_LINKER) + set(CMAKE_LINKER_TYPE + "LLD" + CACHE STRING "Use LLD linker" FORCE) + set(CMAKE_LINKER + ${LLD_LINKER} + CACHE FILEPATH "path to linker binary" FORCE) + add_link_options("-fuse-ld=lld") + endif() + endif() endif() if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") @@ -103,6 +115,8 @@ option(USE_SIMD_FALLBACK option(DISABLE_TSAN "Disable TSAN" OFF) +option(USE_MSAN "Build with MemorySanitizer (disables ASan/UBSan)" OFF) + # This is encouraged according to # https://valgrind.org/docs/manual/manual-core-adv.html#manual-core-adv.clientreq include_directories(SYSTEM ${CMAKE_CURRENT_SOURCE_DIR}/third_party/valgrind) @@ -232,8 +246,10 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND BUILD_TESTING) target_compile_definitions(conflict_set_main PRIVATE ENABLE_MAIN) target_link_libraries(conflict_set_main PRIVATE nanobench) - if(NOT APPLE) - # libfuzzer target, to generate/manage corpus + if(NOT APPLE AND NOT USE_MSAN) + # libfuzzer target, to generate/manage corpus. MSan requires an instrumented + # libfuzzer runtime, which is not shipped with the compiler, so skip this + # target when building with MSan. set(FUZZ_FLAGS "-fsanitize=fuzzer-no-link,address,undefined") include(CheckCXXCompilerFlag) cmake_push_check_state() @@ -257,8 +273,13 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND BUILD_TESTING) add_executable(fuzz_driver ConflictSet.cpp FuzzTestDriver.cpp) target_compile_options(fuzz_driver PRIVATE ${TEST_FLAGS}) if(NOT CMAKE_CROSSCOMPILING) - target_compile_options(fuzz_driver PRIVATE -fsanitize=address,undefined) - target_link_options(fuzz_driver PRIVATE -fsanitize=address,undefined) + if(USE_MSAN) + target_compile_options(fuzz_driver PRIVATE -fsanitize=memory) + target_link_options(fuzz_driver PRIVATE -fsanitize=memory) + else() + target_compile_options(fuzz_driver PRIVATE -fsanitize=address,undefined) + target_link_options(fuzz_driver PRIVATE -fsanitize=address,undefined) + endif() endif() target_compile_definitions(fuzz_driver PRIVATE ENABLE_FUZZ) target_include_directories(fuzz_driver @@ -269,7 +290,9 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND BUILD_TESTING) endforeach() # tsan tests - if(NOT CMAKE_CROSSCOMPILING AND NOT DISABLE_TSAN) + if(NOT CMAKE_CROSSCOMPILING + AND NOT DISABLE_TSAN + AND NOT USE_MSAN) add_executable(tsan_driver ConflictSet.cpp FuzzTestDriver.cpp) target_compile_options(tsan_driver PRIVATE ${TEST_FLAGS} -fsanitize=thread) target_link_options(tsan_driver PRIVATE -fsanitize=thread) @@ -491,6 +514,11 @@ target_include_directories( PUBLIC $ $) +if(USE_MSAN) + target_compile_options(${PROJECT_NAME} PUBLIC -fsanitize=memory) + target_link_options(${PROJECT_NAME} PUBLIC -fsanitize=memory) +endif() + set_target_properties( ${PROJECT_NAME} PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR}) diff --git a/build_msan_toolchain.sh b/build_msan_toolchain.sh new file mode 100755 index 0000000..29d7dba --- /dev/null +++ b/build_msan_toolchain.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +set -euxo pipefail + +LLVM_VERSION="${LLVM_VERSION:-21}" +MSAN_PREFIX="${MSAN_PREFIX:-$PWD/msan}" +JOBS="${JOBS:-$(nproc)}" + +cd /tmp +rm -rf libcxx-msan +mkdir libcxx-msan +cd libcxx-msan + +git clone --depth=1 "https://github.com/llvm/llvm-project.git" -b "release/${LLVM_VERSION}.x" + +cmake -S llvm-project/runtimes -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER="clang-${LLVM_VERSION}" \ + -DCMAKE_CXX_COMPILER="clang++-${LLVM_VERSION}" \ + -DLLVM_USE_SANITIZER=MemoryWithOrigins \ + -DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi;libunwind" \ + -DLLVM_TARGETS_TO_BUILD=Native \ + -DCMAKE_INSTALL_PREFIX="${MSAN_PREFIX}" \ + -DLIBCXXABI_USE_LLVM_UNWINDER=ON \ + -DLIBCXXABI_ENABLE_STATIC_UNWINDER=ON \ + -DLIBCXX_USE_COMPILER_RT=ON \ + -DLIBCXXABI_USE_COMPILER_RT=ON \ + -DLIBUNWIND_USE_COMPILER_RT=ON \ + -DLIBCXX_INCLUDE_TESTS=OFF \ + -DLIBCXXABI_INCLUDE_TESTS=OFF \ + -DLIBUNWIND_INCLUDE_TESTS=OFF \ + -DLIBCXX_ENABLE_CLANG_TIDY=OFF + +cmake --build build -j"${JOBS}" +cmake --install build + +VERSION="$(clang-"${LLVM_VERSION}" --version | head -n1 | sed -E 's/.*clang version ([0-9.]+).*/\1/')" +TARBALL="msan-toolchain-${VERSION}.tar.zst" + +tar --zstd -cf "${TARBALL}" -C "${MSAN_PREFIX}" . + +TARBALL_PATH="$(pwd)/${TARBALL}" +echo "Created: ${TARBALL_PATH}"