From 8063112285a6793e2c36053658dfd0abd0c36d38 Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Tue, 31 Mar 2026 23:26:30 +0100 Subject: [PATCH] Enable sanitizers --- .github/workflows/sanitizers.yml | 70 ++++++++++++++++++++++++++++++++ CMakeLists.txt | 9 ++++ GEMINI.md | 14 ++++++- cmake/sanitizers.cmake | 44 ++++++++++++++++++-- cmake/xyz_add_test.cmake | 6 ++- scripts/cmake.py | 10 +++++ 6 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/sanitizers.yml diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml new file mode 100644 index 0000000..f5946af --- /dev/null +++ b/.github/workflows/sanitizers.yml @@ -0,0 +1,70 @@ +name: Sanitizers + +on: + schedule: + - cron: "0 1 * * *" + push: + branches: [main] + paths: + - ".github/workflows/sanitizers.yml" + - "**/*.cc" + - "**/*.h" + - "**/CMakeLists.txt" + - "CMakePresets.json" + - "**/*.cmake" + - "scripts/cmake.py" + pull_request: + branches: [main] + paths: + - ".github/workflows/sanitizers.yml" + - "**/*.cc" + - "**/*.h" + - "**/CMakeLists.txt" + - "CMakePresets.json" + - "**/*.cmake" + - "scripts/cmake.py" + +env: + UV_FROZEN: 1 + +jobs: + sanitized-tests: + name: ${{ matrix.sanitizer }} (Clang-${{ matrix.clang_version }}) + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + sanitizer: ["asan", "tsan", "msan"] + clang_version: ["19"] + steps: + - uses: actions/checkout@v4 + - uses: seanmiddleditch/gha-setup-ninja@master + - name: Install Clang + uses: egor-tensin/setup-clang@v1 + with: + version: ${{ matrix.clang_version }} + platform: x64 + - name: Install uv + uses: astral-sh/setup-uv@v5 + - name: Set up Python + run: uv sync + - name: Run CMake (Default VTable) + run: | + UBSAN_FLAG="" + if [ "${{ matrix.sanitizer }}" = "asan" ]; then + UBSAN_FLAG="--ubsan" + fi + ./scripts/cmake.sh -B build_${{ matrix.sanitizer }}_default --debug --${{ matrix.sanitizer }} ${UBSAN_FLAG} + env: + CC: clang-${{ matrix.clang_version }} + CXX: clang++-${{ matrix.clang_version }} + - name: Run CMake (Manual VTable) + run: | + UBSAN_FLAG="" + if [ "${{ matrix.sanitizer }}" = "asan" ]; then + UBSAN_FLAG="--ubsan" + fi + ./scripts/cmake.sh -B build_${{ matrix.sanitizer }}_manual --debug --manual-vtable --${{ matrix.sanitizer }} ${UBSAN_FLAG} + env: + CC: clang-${{ matrix.clang_version }} + CXX: clang++-${{ matrix.clang_version }} diff --git a/CMakeLists.txt b/CMakeLists.txt index e66d252..ec80124 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,15 @@ option(ENABLE_SANITIZERS "Enable Address Sanitizer and Undefined Behaviour Sanitizer if available" OFF) +option(ENABLE_ASAN "Enable Address Sanitizer" OFF) +option(ENABLE_UBSAN "Enable Undefined Behaviour Sanitizer" OFF) +option(ENABLE_TSAN "Enable Thread Sanitizer" OFF) +option(ENABLE_MSAN "Enable Memory Sanitizer" OFF) + +if(ENABLE_ASAN OR ENABLE_UBSAN OR ENABLE_TSAN OR ENABLE_MSAN) + set(ENABLE_SANITIZERS ON) +endif() + # Set up Python discovery set(Python3_FIND_VIRTUALENV FIRST) if(DEFINED ENV{UV_PROJECT_ENVIRONMENT}) diff --git a/GEMINI.md b/GEMINI.md index 00c641b..b0de6fb 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -22,7 +22,19 @@ general defaults for this repository. - **Tooling:** Always use `uv` for Python dependency management (`uv run ...`). - **Build & Test:** Use `scripts/cmake.sh` for all build and test operations. -- **Verification:** All changes must be verified against both the default (virtual dispatch) and manual vtable configurations. The `scripts/cmake.sh` script must be run twice: once without any flags, and a second time with the `--manual-vtable` flag to build and test the alternative implementation. + The `scripts/cmake.sh` entrypoint supports `--debug`, `--release`, + `--manual-vtable`, `--asan`, `--ubsan`, `--tsan`, and `--msan`. +- **Compiler Preferences:** Prefer Clang 19+ for sanitizer-based verification + and CI, as it provides superior support for MSAN and TSAN compared to + older GCC versions. +- **Verification:** All changes must be verified against both the default + (virtual dispatch) and manual vtable configurations. The `scripts/cmake.sh` + script must be run twice: once without any flags, and a second time with + the `--manual-vtable` flag to build and test the alternative implementation. +- **Sanitizer Verification:** When modifying memory-sensitive or concurrent + code, verify changes locally using at least one sanitizer (e.g., + `./scripts/cmake.sh --asan` or `--tsan`). Note that ASAN, TSAN, and MSAN + are mutually exclusive. - **Post-Change Checks:** Tests and pre-commit checks MUST be run after any modifications to the codebase. diff --git a/cmake/sanitizers.cmake b/cmake/sanitizers.cmake index 893498e..9dd572b 100644 --- a/cmake/sanitizers.cmake +++ b/cmake/sanitizers.cmake @@ -1,12 +1,36 @@ include_guard(GLOBAL) if(ENABLE_SANITIZERS) - set(SANITIZER_FLAGS_ASAN "-fsanitize=address -fno-omit-frame-pointer") + set(SANITIZER_FLAGS_ASAN "-fsanitize=address" "-fno-omit-frame-pointer") set(SANITIZER_FLAGS_UBSAN "-fsanitize=undefined") + set(SANITIZER_FLAGS_TSAN "-fsanitize=thread") + set(SANITIZER_FLAGS_MSAN "-fsanitize=memory" "-fsanitize-memory-track-origins") include(CheckCXXCompilerFlag) - check_cxx_compiler_flag("${SANITIZER_FLAGS_ASAN}" COMPILER_SUPPORTS_ASAN) - check_cxx_compiler_flag("${SANITIZER_FLAGS_UBSAN}" COMPILER_SUPPORTS_UBSAN) + + # Check ASAN + set(CMAKE_REQUIRED_FLAGS "-fsanitize=address -fno-omit-frame-pointer") + set(CMAKE_REQUIRED_LINK_OPTIONS "-fsanitize=address") + check_cxx_compiler_flag("-fsanitize=address" COMPILER_SUPPORTS_ASAN) + + # Check UBSAN + set(CMAKE_REQUIRED_FLAGS "-fsanitize=undefined") + set(CMAKE_REQUIRED_LINK_OPTIONS "-fsanitize=undefined") + check_cxx_compiler_flag("-fsanitize=undefined" COMPILER_SUPPORTS_UBSAN) + + # Check TSAN + set(CMAKE_REQUIRED_FLAGS "-fsanitize=thread") + set(CMAKE_REQUIRED_LINK_OPTIONS "-fsanitize=thread") + check_cxx_compiler_flag("-fsanitize=thread" COMPILER_SUPPORTS_TSAN) + + # Check MSAN + set(CMAKE_REQUIRED_FLAGS "-fsanitize=memory -fsanitize-memory-track-origins") + set(CMAKE_REQUIRED_LINK_OPTIONS "-fsanitize=memory") + check_cxx_compiler_flag("-fsanitize=memory" COMPILER_SUPPORTS_MSAN) + + # Reset required flags + unset(CMAKE_REQUIRED_FLAGS) + unset(CMAKE_REQUIRED_LINK_OPTIONS) if(COMPILER_SUPPORTS_ASAN) add_library(asan INTERFACE IMPORTED) @@ -21,4 +45,18 @@ if(ENABLE_SANITIZERS) ubsan PROPERTIES INTERFACE_COMPILE_OPTIONS "${SANITIZER_FLAGS_UBSAN}" INTERFACE_LINK_OPTIONS "${SANITIZER_FLAGS_UBSAN}") endif(COMPILER_SUPPORTS_UBSAN) + + if(COMPILER_SUPPORTS_TSAN) + add_library(tsan INTERFACE IMPORTED) + set_target_properties( + tsan PROPERTIES INTERFACE_COMPILE_OPTIONS "${SANITIZER_FLAGS_TSAN}" + INTERFACE_LINK_OPTIONS "${SANITIZER_FLAGS_TSAN}") + endif(COMPILER_SUPPORTS_TSAN) + + if(COMPILER_SUPPORTS_MSAN) + add_library(msan INTERFACE IMPORTED) + set_target_properties( + msan PROPERTIES INTERFACE_COMPILE_OPTIONS "${SANITIZER_FLAGS_MSAN}" + INTERFACE_LINK_OPTIONS "${SANITIZER_FLAGS_MSAN}") + endif(COMPILER_SUPPORTS_MSAN) endif(ENABLE_SANITIZERS) diff --git a/cmake/xyz_add_test.cmake b/cmake/xyz_add_test.cmake index 34fa56e..00cf265 100644 --- a/cmake/xyz_add_test.cmake +++ b/cmake/xyz_add_test.cmake @@ -77,8 +77,10 @@ function(xyz_add_test) target_link_libraries( ${XYZ_NAME} PRIVATE ${XYZ_LINK_LIBRARIES} GTest::gtest_main common_compiler_settings - $<$:asan> - $<$:ubsan>) + $<$,$>:asan> + $<$,$>:ubsan> + $<$,$>:tsan> + $<$,$>:msan>) set_target_properties( ${XYZ_NAME} diff --git a/scripts/cmake.py b/scripts/cmake.py index 3e61ace..f5c7688 100644 --- a/scripts/cmake.py +++ b/scripts/cmake.py @@ -32,6 +32,12 @@ def main(): action="store_true", help="Set XYZ_PROTOCOL_GENERATE_MANUAL_VTABLE=ON", ) + parser.add_argument("--asan", action="store_true", help="Enable Address Sanitizer") + parser.add_argument( + "--ubsan", action="store_true", help="Enable Undefined Behaviour Sanitizer" + ) + parser.add_argument("--tsan", action="store_true", help="Enable Thread Sanitizer") + parser.add_argument("--msan", action="store_true", help="Enable Memory Sanitizer") parser.add_argument("-B", "--build-dir", help="Build directory") parser.add_argument( "--clean", action="store_true", help="Fresh configuration and clean-first build" @@ -66,6 +72,10 @@ def log(msg): "--preset", preset, f"-DXYZ_PROTOCOL_GENERATE_MANUAL_VTABLE={'ON' if args.manual_vtable else 'OFF'}", + f"-DENABLE_ASAN={'ON' if args.asan else 'OFF'}", + f"-DENABLE_UBSAN={'ON' if args.ubsan else 'OFF'}", + f"-DENABLE_TSAN={'ON' if args.tsan else 'OFF'}", + f"-DENABLE_MSAN={'ON' if args.msan else 'OFF'}", ] if args.build_dir: configure_args.extend(["-B", args.build_dir])