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:
Yuanyuan Chen
2026-08-01 00:59:54 +08:00
committed by GitHub
parent a5ea26bd24
commit 9ef07d367e
6 changed files with 152 additions and 41 deletions
+30 -6
View File
@@ -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'
+6
View File
@@ -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
View File
@@ -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"
+30
View File
@@ -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()
+81 -34
View File
@@ -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()
+4
View File
@@ -0,0 +1,4 @@
race:v8::
race:object_stack
race:mi_segment_page_clear
race:libpython