Add TSAN and TypeSAN sanitizer support; fix ASAN CI env-var propagation (#7471)
### Description Adds ThreadSanitizer (TSAN) as a new CI job, adds TypeSanitizer (TypeSAN, Clang's `-fsanitize=type`) as a build option, and fixes a couple of pre-existing issues in the ASAN setup along the way. **CI** - New matrix entry: Ubuntu debug build on Python 3.14t (free-threaded) with `-DONNX_USE_TSAN=ON`, to catch data races — particularly relevant for free-threaded Python where the GIL no longer serializes access. - Added `TSAN_OPTIONS: suppressions=./tsan.supp` and a starter `tsan.supp` suppression list for known noisy/third-party findings (CPython internals, etc.). - **Fixed ASAN's `LD_PRELOAD` not actually taking effect**: the old step used `export LD_PRELOAD=...`, but `export` inside a GitHub Actions `run:` step only affects that step's own process — it never propagated to the later "Run Python/C++ tests" steps. Switched to `echo "LD_PRELOAD=..." >> $GITHUB_ENV`, which is the correct way to persist an env var across steps. Split into separate ASAN/TSAN steps, and also preload `libstdc++.so` alongside the sanitizer runtime (avoids known ODR-violation false positives between the sanitizer and libstdc++). - Tidied `ASAN_OPTIONS` (dropped defaults/no-ops like `symbolize=1`, `check_initialization_order=true` (superseded by `strict_init_order`)) and added `alloc_dealloc_mismatch=1:abort_on_error=1`. **Build (CMake)** - New `ONNX_USE_TSAN` and `ONNX_USE_TYPESAN` options in `CMakeLists.txt`, wired up in `cmake/Utils.cmake` alongside the existing ASAN/UBSAN linking. TypeSAN is added to the build system here but not yet turned on in CI. - `cmake/external/FindSanitizer.cmake`: added `Sanitizer::thread` and `Sanitizer::type` targets, migrated from the legacy per-language `CheckCSourceCompiles`/`CheckCXXSourceCompiles`/etc. modules to the unified `CheckSourceCompiles`/`CheckSourceRuns` (available since the project's CMake 3.26 minimum), and extended the self-verification step (compile+run a snippet that should trigger each sanitizer, to confirm it's actually catching bugs and not just linking) to memory and thread sanitizers as well, not just address/undefined. - Moved `find_package(Sanitizer REQUIRED)` earlier in `CMakeLists.txt` so the `Sanitizer::*` targets exist before `add_onnx_compile_options()` is called for `onnx_proto`/`onnx`/`onnx_cpp2py_export`. **REUSE** - Added `tsan.supp` to the `REUSE.toml` aggregate-copyright path list. --- ### Merge conflict resolution and fixes (andife, via Claude Code) Since "allow edits from maintainers" is enabled, I merged latest `main` and made two small fixes found in review: - **Merge conflict in `.github/workflows/main.yml`**: `main` independently flipped `detect_container_overflow` from `0` to `1` in `ASAN_OPTIONS` while this branch was in flight. Resolved by keeping `main`'s current tuning and layering this PR's own additions (`alloc_dealloc_mismatch`, `abort_on_error`) on top, rather than silently reverting `main`'s unrelated decision. - **`cmake/external/FindSanitizer.cmake`**: the self-verification loop now runs for `sanitizer_name = type` too, but no `_bug_type_code` variable was ever defined (only `_bug_address_code`/`_bug_undefined_code`/`_bug_thread_code`/`_bug_memory_code` exist). `check_source_runs()` against an undefined/empty variable fails to compile, which the surrounding logic treats as "no bug detected, sanitizer works" — so TypeSAN's self-test was silently a no-op. Added a minimal `_bug_type_code` snippet (write through an incompatible pointer type) matching the style of the existing ones. - **`.github/workflows/main.yml`**: restored `export LD_LIBRARY_PATH="./.setuptools-cmake-build/:$LD_LIBRARY_PATH"` before running `onnx_gtests`, which this PR had dropped without explanation. It's been present since this step was introduced and is documented in `CLAUDE.md` as required for Linux/macOS; removing it risks breaking C++ test execution for matrix entries that need to resolve a shared library from that build directory (e.g. External/dynamic-protobuf builds). Verified locally: `pixi run install` succeeds, 104/104 C++ gtests pass, lint clean. (Sanitizer-specific behavior — ASAN/TSAN/TypeSAN builds themselves — could not be exercised locally on this Windows/MSVC machine; that part still needs the actual CI run to confirm.) **AI tool disclosure**: the merge-conflict resolution and the two fixes above were made by @andife using [Claude Code](https://claude.com/claude-code) (Anthropic, Claude Sonnet 5) as a pair-programming assistant; every change was reviewed and explicitly approved before being pushed. This covers only that portion — @cyyever, feel free to add a note on what (if anything) was used for the original PR content. ### Motivation and Context This PR adds TSAN to the CI matrix and adds TypeSAN support to the build system, to catch data races and type-confusion bugs that ASAN/UBSAN don't cover — particularly useful for the free-threaded Python (3.14t) build where the GIL no longer serializes access to shared state. --------- Signed-off-by: Yuanyuan Chen <cyyever@outlook.com> Signed-off-by: cyy <cyyever@outlook.com> Signed-off-by: Andreas Fehlner <fehlner@arcor.de> Co-authored-by: Andreas Fehlner <fehlner@arcor.de> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -5,8 +5,9 @@
|
||||
name: CI
|
||||
|
||||
env:
|
||||
ASAN_OPTIONS: detect_leaks=0:symbolize=1:detect_stack_use_after_return=true:strict_init_order=true:detect_odr_violation=1:detect_container_overflow=1:check_initialization_order=true:debug=true:fast_unwind_on_malloc=1:verify_asan_link_order=0
|
||||
ASAN_OPTIONS: detect_leaks=0:symbolize=1:detect_stack_use_after_return=true:strict_init_order=true:detect_odr_violation=1:detect_container_overflow=1:check_initialization_order=true:debug=true:fast_unwind_on_malloc=1:verify_asan_link_order=0:alloc_dealloc_mismatch=1:abort_on_error=1
|
||||
UBSAN_OPTIONS: print_stacktrace=1
|
||||
TSAN_OPTIONS: suppressions=./tsan.supp
|
||||
|
||||
on:
|
||||
schedule:
|
||||
@@ -50,6 +51,15 @@ jobs:
|
||||
protobuf_type: 'Internal'
|
||||
unity_build: 0
|
||||
|
||||
# Ubuntu debug build with TSAN (free-threaded Python)
|
||||
- python_version: "3.14t"
|
||||
autogenerate_files: 0
|
||||
debug_build: 1
|
||||
onnx_ml: 1
|
||||
os: "ubuntu-24.04"
|
||||
protobuf_type: 'Internal'
|
||||
unity_build: 0
|
||||
|
||||
# Test compilation with dynamically linked protobuf.
|
||||
# This is actually redundant with the pixi-tests which
|
||||
# also use dynamic linking.
|
||||
@@ -157,6 +167,13 @@ jobs:
|
||||
if [ "${{ matrix.protobuf_type }}" == "External" ]; then
|
||||
export CMAKE_ARGS="$CMAKE_ARGS -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DONNX_USE_PROTOBUF_SHARED_LIBS=ON"
|
||||
fi
|
||||
if [ "${{ matrix.debug_build }}" == "1" ]; then
|
||||
if [ "${{ matrix.python_version }}" == "3.14t" ]; then
|
||||
export CMAKE_ARGS="$CMAKE_ARGS -DONNX_USE_ASAN=OFF -DONNX_USE_TSAN=ON"
|
||||
else
|
||||
export CMAKE_ARGS="$CMAKE_ARGS -DONNX_USE_ASAN=ON"
|
||||
fi
|
||||
fi
|
||||
pip install -e ".[reference]" -v
|
||||
env:
|
||||
DEBUG: ${{ matrix.debug_build }}
|
||||
@@ -188,19 +205,26 @@ jobs:
|
||||
run: |
|
||||
pip freeze
|
||||
|
||||
- name: Setup GCC ASAN LD_PRELOAD
|
||||
if: startsWith(matrix.os,'ubuntu') && (matrix.python_version != '3.13t')
|
||||
run: |
|
||||
export LD_PRELOAD="$(/usr/bin/c++ -print-file-name=libasan.so):$LD_PRELOAD"
|
||||
|
||||
- name: Install test dependencies
|
||||
run: |
|
||||
python -m pip install -r requirements-release_test.txt
|
||||
|
||||
- name: Setup ASAN LD_PRELOAD
|
||||
if: startsWith(matrix.os,'ubuntu') && (matrix.debug_build == 1) && (matrix.python_version != '3.14t') && (matrix.python_version != '3.10')
|
||||
run: |
|
||||
echo "LD_PRELOAD=$(/usr/bin/c++ -print-file-name=libasan.so):$(/usr/bin/c++ -print-file-name=libstdc++.so)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup TSAN LD_PRELOAD
|
||||
if: startsWith(matrix.os,'ubuntu') && (matrix.debug_build == 1) && (matrix.python_version == '3.14t')
|
||||
run: |
|
||||
echo "LD_PRELOAD=$(/usr/bin/c++ -print-file-name=libtsan.so):$(/usr/bin/c++ -print-file-name=libstdc++.so)" >> $GITHUB_ENV
|
||||
|
||||
- name: Run Python tests
|
||||
# Prohibitively slow for Windows debug builds
|
||||
if: ${{ !(startsWith(matrix.os, 'windows') && matrix.debug_build == 1) }}
|
||||
run: |
|
||||
# LD_PRELOAD'd ASan trips a protobuf RepeatedField false positive (protobuf #13115); keep the check on for gtests, disable it only here.
|
||||
if [ -n "$LD_PRELOAD" ]; then export ASAN_OPTIONS="${ASAN_OPTIONS//detect_container_overflow=1/detect_container_overflow=0}"; fi
|
||||
pytest -sv --cov=onnx --cov-report=xml --cov-append --cov-branch --junitxml junit.xml -n auto --dist loadscope
|
||||
- name: Run C++ tests
|
||||
if: startsWith(matrix.os,'ubuntu') || matrix.os == 'macos-latest'
|
||||
|
||||
@@ -40,6 +40,8 @@ option(ONNX_WERROR "Build with Werror" OFF)
|
||||
option(ONNX_COVERAGE "Build with coverage instrumentation" OFF)
|
||||
option(ONNX_BUILD_TESTS "Build ONNX C++ APIs Tests" OFF)
|
||||
option(ONNX_USE_ASAN "Build ONNX with ASAN" OFF)
|
||||
option(ONNX_USE_TSAN "Build ONNX with TSAN" OFF)
|
||||
option(ONNX_USE_TYPESAN "Build ONNX with TypeSAN" OFF)
|
||||
option(ONNX_HARDENING "Build with compiler hardening flags (OpenSSF guidelines)" OFF)
|
||||
option(ONNX_USE_LITE_PROTO "Use lite protobuf instead of full." OFF)
|
||||
option(ONNX_DISABLE_EXCEPTIONS "Disable exception handling." OFF)
|
||||
@@ -180,6 +182,10 @@ if(NOT DEFINED CMAKE_POSITION_INDEPENDENT_CODE)
|
||||
endif()
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH ${ONNX_ROOT}/cmake/external)
|
||||
if(ONNX_USE_ASAN OR ONNX_USE_TSAN OR ONNX_USE_TYPESAN)
|
||||
find_package(Sanitizer REQUIRED)
|
||||
endif()
|
||||
|
||||
if(NOT ONNX_BUILD_CUSTOM_PROTOBUF)
|
||||
if((ONNX_USE_LITE_PROTO AND TARGET protobuf::libprotobuf-lite) OR ((NOT ONNX_USE_LITE_PROTO) AND TARGET protobuf::libprotobuf))
|
||||
# Sometimes we need to use protoc compiled for host architecture while linking
|
||||
|
||||
+1
-1
@@ -224,7 +224,7 @@ SPDX-FileCopyrightText = "Copyright (c) ONNX Project Contributors"
|
||||
SPDX-License-Identifier = "Apache-2.0"
|
||||
|
||||
[[annotations]]
|
||||
path = [".agents/**", ".claude/**", "uv.lock"]
|
||||
path = [".agents/**", ".claude/**", "uv.lock", "tsan.supp"]
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "Copyright (c) ONNX Project Contributors"
|
||||
SPDX-License-Identifier = "Apache-2.0"
|
||||
|
||||
@@ -203,6 +203,36 @@ function(add_onnx_compile_options target)
|
||||
target_link_libraries(${target} PRIVATE "-lstdc++fs")
|
||||
endif()
|
||||
|
||||
if(ONNX_USE_ASAN)
|
||||
if(TARGET Sanitizer::address)
|
||||
target_link_libraries(${target} PUBLIC Sanitizer::address)
|
||||
message(STATUS "Use ASAN for ${target}")
|
||||
else()
|
||||
message(STATUS "No ASAN detected for ${target}")
|
||||
endif()
|
||||
if(TARGET Sanitizer::undefined)
|
||||
target_link_libraries(${target} PUBLIC Sanitizer::undefined)
|
||||
message(STATUS "Use UBSAN for ${target}")
|
||||
else()
|
||||
message(STATUS "No UBSAN detected for ${target}")
|
||||
endif()
|
||||
endif()
|
||||
if(ONNX_USE_TSAN)
|
||||
if(TARGET Sanitizer::thread)
|
||||
target_link_libraries(${target} PUBLIC Sanitizer::thread)
|
||||
message(STATUS "Use TSAN for ${target}")
|
||||
else()
|
||||
message(STATUS "No TSAN detected for ${target}")
|
||||
endif()
|
||||
endif()
|
||||
if(ONNX_USE_TYPESAN)
|
||||
if(TARGET Sanitizer::type)
|
||||
target_link_libraries(${target} PUBLIC Sanitizer::type)
|
||||
message(STATUS "Use TypeSAN for ${target}")
|
||||
else()
|
||||
message(STATUS "No TypeSAN detected for ${target}")
|
||||
endif()
|
||||
endif()
|
||||
# Apply hardening flags if enabled
|
||||
add_onnx_hardening_flags(${target})
|
||||
endfunction()
|
||||
|
||||
Vendored
+81
-34
@@ -7,10 +7,13 @@
|
||||
# Sanitizer::thread
|
||||
# Sanitizer::undefined
|
||||
# Sanitizer::memory
|
||||
# Sanitizer::type
|
||||
include_guard(GLOBAL)
|
||||
|
||||
option(UBSAN_FLAGS "additional UBSAN flags" OFF)
|
||||
option(MSAN_FLAGS "additional MSAN flags" OFF)
|
||||
option(ASAN_FLAGS "additional ASAN flags" "")
|
||||
option(UBSAN_FLAGS "additional UBSAN flags" "")
|
||||
option(TSAN_FLAGS "additional TSAN flags" "")
|
||||
option(MSAN_FLAGS "additional MSAN flags" "-fsanitize-memory-track-origins=2")
|
||||
|
||||
get_property(languages GLOBAL PROPERTY ENABLED_LANGUAGES)
|
||||
|
||||
@@ -44,21 +47,68 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
]==])
|
||||
|
||||
set(_bug_thread_code
|
||||
[==[
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <string>
|
||||
#include <map>
|
||||
|
||||
typedef std::map<std::string, std::string> map_t;
|
||||
|
||||
void *threadfunc(void *p) {
|
||||
map_t& m = *(map_t*)p;
|
||||
m["foo"] = "bar";
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main() {
|
||||
map_t m;
|
||||
pthread_t t;
|
||||
pthread_create(&t, 0, threadfunc, &m);
|
||||
printf("foo=%s\n", m["foo"].c_str());
|
||||
pthread_join(t, 0);
|
||||
}
|
||||
]==])
|
||||
|
||||
set(_bug_memory_code
|
||||
[==[
|
||||
int main(int argc, char** argv) {
|
||||
int* a = new int[10];
|
||||
a[5] = 0;
|
||||
volatile int b = a[argc];
|
||||
if (b)
|
||||
printf("xx\n");
|
||||
return 0;
|
||||
}
|
||||
]==])
|
||||
|
||||
set(_bug_type_code
|
||||
[==[
|
||||
int main(int argc, char **argv) {
|
||||
int i = argc;
|
||||
float *p = (float *)&i;
|
||||
*p = 1.0f; // BOOM: write through a type incompatible with the allocation
|
||||
return (int)*p;
|
||||
}
|
||||
]==])
|
||||
|
||||
include(CMakePushCheckState)
|
||||
foreach(lang IN LISTS languages)
|
||||
if(lang STREQUAL C)
|
||||
include(CheckCSourceCompiles)
|
||||
include(CheckCSourceRuns)
|
||||
elseif(lang STREQUAL CXX)
|
||||
include(CheckCXXSourceCompiles)
|
||||
include(CheckCXXSourceRuns)
|
||||
if(lang STREQUAL C OR lang STREQUAL CXX)
|
||||
include(CheckSourceCompiles)
|
||||
include(CheckSourceRuns)
|
||||
else()
|
||||
continue()
|
||||
endif()
|
||||
foreach(sanitizer_name IN ITEMS address thread undefined memory)
|
||||
foreach(sanitizer_name IN ITEMS address thread undefined memory type)
|
||||
if(TARGET Sanitizer::${sanitizer_name}_${lang})
|
||||
continue()
|
||||
endif()
|
||||
# TypeSAN (-fsanitize=type / tysan) is Clang-only; skip on GCC and others to avoid a spurious "Can't find type" warning.
|
||||
if(sanitizer_name STREQUAL "type" AND NOT CMAKE_${lang}_COMPILER_ID MATCHES "Clang")
|
||||
continue()
|
||||
endif()
|
||||
if(CMAKE_${lang}_COMPILER_ID STREQUAL "MSVC")
|
||||
if(sanitizer_name STREQUAL "address")
|
||||
set(SANITIZER_FLAGS "/fsanitize=${sanitizer_name}")
|
||||
@@ -69,14 +119,17 @@ foreach(lang IN LISTS languages)
|
||||
set(SANITIZER_FLAGS
|
||||
"-fsanitize=${sanitizer_name};-fno-omit-frame-pointer")
|
||||
endif()
|
||||
if(sanitizer_name STREQUAL "address" AND ASAN_FLAGS)
|
||||
list(APPEND SANITIZER_FLAGS "${ASAN_FLAGS}")
|
||||
endif()
|
||||
if(sanitizer_name STREQUAL "thread" AND TSAN_FLAGS)
|
||||
list(APPEND SANITIZER_FLAGS "${TSAN_FLAGS}")
|
||||
endif()
|
||||
if(sanitizer_name STREQUAL "undefined" AND UBSAN_FLAGS)
|
||||
list(APPEND SANITIZER_FLAGS "${UBSAN_FLAGS}")
|
||||
endif()
|
||||
if(sanitizer_name STREQUAL "memory")
|
||||
list(APPEND SANITIZER_FLAGS "-fsanitize-memory-track-origins=2")
|
||||
if(MSAN_FLAGS)
|
||||
list(APPEND SANITIZER_FLAGS "${MSAN_FLAGS}")
|
||||
endif()
|
||||
if(sanitizer_name STREQUAL "memory" AND MSAN_FLAGS)
|
||||
list(APPEND SANITIZER_FLAGS "${MSAN_FLAGS}")
|
||||
endif()
|
||||
cmake_push_check_state(RESET)
|
||||
set(CMAKE_REQUIRED_QUIET ON)
|
||||
@@ -91,35 +144,29 @@ foreach(lang IN LISTS languages)
|
||||
set(CMAKE_REQUIRED_LINK_OPTIONS "${SANITIZER_LINK_FLAGS}")
|
||||
|
||||
unset(__res CACHE)
|
||||
if(lang STREQUAL C)
|
||||
if(CMAKE_${lang}_COMPILER_ID STREQUAL "MSVC")
|
||||
check_c_source_compiles("${_source_code}" __res)
|
||||
else()
|
||||
check_c_source_runs("${_source_code}" __res)
|
||||
endif()
|
||||
if(CMAKE_${lang}_COMPILER_ID STREQUAL "MSVC")
|
||||
check_source_compiles(${lang} "${_source_code}" __res)
|
||||
else()
|
||||
if(CMAKE_${lang}_COMPILER_ID STREQUAL "MSVC")
|
||||
check_cxx_source_compiles("${_source_code}" __res)
|
||||
else()
|
||||
check_cxx_source_runs("${_source_code}" __res)
|
||||
endif()
|
||||
check_source_runs(${lang} "${_source_code}" __res)
|
||||
endif()
|
||||
if(NOT __res)
|
||||
message(WARNING "Can't find ${sanitizer_name} in ${lang}")
|
||||
# no memory sanitizer is common
|
||||
if(NOT sanitizer_name STREQUAL "memory")
|
||||
message(WARNING "Can't find ${sanitizer_name} in ${lang}")
|
||||
endif()
|
||||
cmake_pop_check_state()
|
||||
continue()
|
||||
endif()
|
||||
|
||||
unset(__res CACHE)
|
||||
if(NOT CMAKE_${lang}_COMPILER_ID STREQUAL "MSVC" AND (sanitizer_name STREQUAL "address") OR (sanitizer_name STREQUAL "undefined"))
|
||||
set(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -fno-sanitize-recover=all")
|
||||
if(lang STREQUAL C)
|
||||
check_c_source_runs("${_bug_${sanitizer_name}_code}" __res)
|
||||
else()
|
||||
check_cxx_source_runs("${_bug_${sanitizer_name}_code}" __res)
|
||||
endif()
|
||||
if(NOT CMAKE_${lang}_COMPILER_ID STREQUAL "MSVC")
|
||||
set(CMAKE_REQUIRED_FLAGS
|
||||
"${CMAKE_REQUIRED_FLAGS} -fno-sanitize-recover=all")
|
||||
check_source_runs(${lang} "${_bug_${sanitizer_name}_code}" __res)
|
||||
if(__res)
|
||||
message(WARNING "Buffer overflow bug is not detected in ${lang} ${sanitizer_name}")
|
||||
message(
|
||||
WARNING
|
||||
"Buffer overflow bug is not detected in ${lang} ${sanitizer_name}")
|
||||
cmake_pop_check_state()
|
||||
continue()
|
||||
endif()
|
||||
|
||||
Reference in New Issue
Block a user