Skip to content

Commit 01798fa

Browse files
authored
Add ASAN CI build workflow (#2914)
# Description Add ASAN tests and fix ASAN errors --------- Co-authored-by: Alexis Placet <2400067+Alex-PLACET@users.noreply.github.com>
1 parent aa2b3ef commit 01798fa

11 files changed

Lines changed: 247 additions & 20 deletions

File tree

.github/workflows/sanitizers.yml

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
name: Sanitizers
2+
on:
3+
workflow_dispatch:
4+
pull_request:
5+
push:
6+
branches: [master]
7+
concurrency:
8+
group: ${{ github.workflow }}-${{ github.job }}-${{ github.ref }}
9+
cancel-in-progress: true
10+
defaults:
11+
run:
12+
shell: bash -e -l {0}
13+
jobs:
14+
build:
15+
runs-on: ${{ matrix.os }}
16+
name: sanitizer / ${{ matrix.sys.compiler }} ${{ matrix.sys.version }} / ${{ matrix.config.name }} / ${{ matrix.sys.name }}
17+
strategy:
18+
fail-fast: false
19+
matrix:
20+
os: [ubuntu-24.04]
21+
sys:
22+
- {compiler: clang, version: '21', name: asan, sanitizer: address}
23+
- {compiler: clang, version: '21', name: lsan, sanitizer: leak}
24+
- {compiler: clang, version: '21', name: ubsan, sanitizer: undefined}
25+
config:
26+
- {name: Debug}
27+
28+
steps:
29+
30+
- name: Install LLVM and Clang
31+
if: matrix.sys.compiler == 'clang'
32+
run: |
33+
wget https://apt.llvm.org/llvm.sh
34+
chmod +x llvm.sh
35+
sudo ./llvm.sh ${{matrix.sys.version}}
36+
sudo apt-get install -y clang-tools-${{matrix.sys.version}}
37+
sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-${{matrix.sys.version}} 200
38+
sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-${{matrix.sys.version}} 200
39+
sudo update-alternatives --install /usr/bin/clang-scan-deps clang-scan-deps /usr/bin/clang-scan-deps-${{matrix.sys.version}} 200
40+
sudo update-alternatives --set clang /usr/bin/clang-${{matrix.sys.version}}
41+
sudo update-alternatives --set clang++ /usr/bin/clang++-${{matrix.sys.version}}
42+
sudo update-alternatives --set clang-scan-deps /usr/bin/clang-scan-deps-${{matrix.sys.version}}
43+
44+
- name: Checkout code
45+
uses: actions/checkout@v6
46+
47+
- name: Set conda environment
48+
uses: mamba-org/setup-micromamba@main
49+
with:
50+
environment-name: myenv
51+
environment-file: environment-dev.yml
52+
init-shell: bash
53+
cache-downloads: true
54+
55+
- name: Configure using CMake
56+
run: |
57+
export CC=clang
58+
export CXX=clang++
59+
cmake -G Ninja \
60+
-Bbuild \
61+
-DCMAKE_BUILD_TYPE=${{matrix.config.name}} \
62+
-DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX \
63+
-DBUILD_TESTS=ON \
64+
-DUSE_SANITIZER=${{ matrix.sys.sanitizer }}
65+
66+
- name: Build tests
67+
working-directory: build
68+
run: cmake --build . --config ${{matrix.config.name}} --target test_xtensor_lib --parallel 8
69+
70+
- name: Run tests
71+
working-directory: build
72+
run: |
73+
SAN=${{ matrix.sys.sanitizer }}
74+
case "$SAN" in
75+
address)
76+
export ASAN_OPTIONS=log_path=asan_log_:alloc_dealloc_mismatch=0:halt_on_error=0:handle_abort=0
77+
export ASAN_SAVE_DUMPS=AsanDump.dmp
78+
;;
79+
leak)
80+
export LSAN_OPTIONS=log_path=lsan_log_:halt_on_error=0
81+
;;
82+
undefined)
83+
export UBSAN_OPTIONS=log_path=ubsan_log_:halt_on_error=0:print_stacktrace=1
84+
;;
85+
esac
86+
ctest -R ^xtest$ --output-on-failure
87+
88+
- name: Upload sanitizer log
89+
if: always()
90+
uses: actions/upload-artifact@v6
91+
with:
92+
name: sanitizer-log-${{ matrix.sys.sanitizer }}-${{ matrix.sys.compiler }}-${{ matrix.sys.version }}-${{ matrix.config.name }}-${{ runner.os }}
93+
path: '**/*san_log_*'
94+
if-no-files-found: ignore
95+
96+
- name: Upload sanitizer dump
97+
if: always()
98+
uses: actions/upload-artifact@v6
99+
with:
100+
name: sanitizer-dump-${{ matrix.sys.sanitizer }}-${{ matrix.sys.compiler }}-${{ matrix.sys.version }}-${{ matrix.config.name }}-${{ runner.os }}
101+
path: '**/AsanDump.dmp'
102+
if-no-files-found: ignore
103+
104+
- name: Return errors if sanitizer log content is not empty
105+
if: always()
106+
run: |
107+
if [ -n "$(find build/test -name '*san_log_*' -type f -size +0 2>/dev/null)" ]; then
108+
echo "Sanitizer detected errors. See the log for details."
109+
exit 1
110+
fi

cmake/sanitizers.cmake

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
set(AVALAIBLE_SANITIZERS "address;leak;memory;thread;undefined")
2+
OPTION(USE_SANITIZER "Enable sanitizer(s). Options are: ${AVALAIBLE_SANITIZERS}. Case insensitive; multiple options delimited by comma or space possible." "")
3+
string(TOLOWER "${USE_SANITIZER}" USE_SANITIZER)
4+
5+
if((CMAKE_BUILD_TYPE IN_LIST "Debug;RelWithDebInfo") AND USE_SANITIZER)
6+
message(FATAL_ERROR "❌ Sanitizer only supported in Debug and RelWithDebInfo build types.")
7+
endif()
8+
9+
if(USE_SANITIZER)
10+
if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
11+
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<IF:$<AND:$<C_COMPILER_ID:MSVC>,$<CXX_COMPILER_ID:MSVC>>,$<$<CONFIG:Debug,RelWithDebInfo>:EditAndContinue>,$<$<CONFIG:Debug,RelWithDebInfo>:ProgramDatabase>>")
12+
13+
if(USE_SANITIZER MATCHES "address")
14+
list(APPEND SANITIZER_COMPILE_OPTIONS /fsanitize=address /D_DISABLE_VECTOR_ANNOTATION /D_DISABLE_STRING_ANNOTATION)
15+
else()
16+
message(FATAL_ERROR "❌ Sanitizer not supported by MSVC: ${USE_SANITIZER}. It only supports 'address'.")
17+
endif()
18+
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
19+
if(USE_SANITIZER MATCHES "address")
20+
list(APPEND SANITIZER_COMPILE_OPTIONS /fsanitize=address /D_DISABLE_VECTOR_ANNOTATION /D_DISABLE_STRING_ANNOTATION)
21+
list(APPEND SANITIZER_LINK_LIBRARIES clang_rt.asan_dynamic-x86_64 clang_rt.asan_dynamic_runtime_thunk-x86_64)
22+
else()
23+
message(FATAL_ERROR "❌ Sanitizer not supported by Clang-MSVC: ${USE_SANITIZER}. It only supports 'address'.")
24+
endif()
25+
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
26+
foreach(sanitizer ${USE_SANITIZER})
27+
if(NOT ${sanitizer} IN_LIST AVALAIBLE_SANITIZERS)
28+
message(FATAL_ERROR "❌ Sanitizer not supported: ${sanitizer}. It should be one of: ${AVALAIBLE_SANITIZERS}.")
29+
endif()
30+
list(APPEND SANITIZER_COMPILE_OPTIONS -fsanitize=${sanitizer})
31+
list(APPEND SANITIZER_LINK_OPTIONS -fsanitize=${sanitizer})
32+
if (${sanitizer} MATCHES "undefined")
33+
list(APPEND SANITIZER_COMPILE_OPTIONS -fno-sanitize=signed-integer-overflow)
34+
endif()
35+
if (${sanitizer} MATCHES "memory")
36+
list(APPEND SANITIZER_LINK_LIBRARIES -fsanitize-memory-track-origins -fPIE -pie)
37+
list(APPEND SANITIZER_LINK_OPTIONS -fsanitize-memory-track-origins -fPIE -pie)
38+
endif()
39+
endforeach()
40+
list(APPEND SANITIZER_COMPILE_OPTIONS -fno-omit-frame-pointer)
41+
else()
42+
message(FATAL_ERROR "❌ Sanitizer: Unsupported compiler: ${CMAKE_CXX_COMPILER_ID}")
43+
endif()
44+
45+
list(REMOVE_DUPLICATES SANITIZER_COMPILE_OPTIONS)
46+
list(REMOVE_DUPLICATES SANITIZER_LINK_OPTIONS)
47+
list(REMOVE_DUPLICATES SANITIZER_LINK_LIBRARIES)
48+
49+
message(STATUS "🔍 Using sanitizer: ${USE_SANITIZER}")
50+
endif()

include/xtensor/core/xstrides.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@ namespace xt
171171
It strided_data_end(const C& c, It begin, layout_type l, size_type offset)
172172
{
173173
using difference_type = typename std::iterator_traits<It>::difference_type;
174+
if (c.size() == 0 || std::find(c.shape().cbegin(), c.shape().cend(), size_type(0)) != c.shape().cend())
175+
{
176+
return begin;
177+
}
174178
if (c.dimension() == 0)
175179
{
176180
++begin;

include/xtensor/misc/xfft.hpp

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ namespace xt
6161
auto odd = radix2(xt::view(ev, xt::range(1, _, 2)));
6262
#endif
6363

64-
auto range = xt::arange<double>(N / 2);
64+
auto range = xt::arange<double>(static_cast<double>(N) / 2);
6565
auto exp = xt::exp(static_cast<value_type>(-2i) * pi * range / N);
6666
auto t = exp * odd;
6767
auto first_half = even + t;
@@ -82,8 +82,8 @@ namespace xt
8282

8383
// Find a power-of-2 convolution length m such that m >= n * 2 + 1
8484
const std::size_t n = data.size();
85-
size_t m = std::ceil(std::log2(n * 2 + 1));
86-
m = std::pow(2, m);
85+
size_t m = static_cast<size_t>(std::ceil(std::log2(n * 2 + 1)));
86+
m = static_cast<size_t>(std::pow(2, m));
8787

8888
// Trignometric table
8989
auto exp_table = xt::xtensor<std::complex<precision>, 1>::from_shape({n});
@@ -128,6 +128,10 @@ namespace xt
128128
inline auto fft(E&& e, std::ptrdiff_t axis = -1)
129129
{
130130
using value_type = typename std::decay<E>::type::value_type;
131+
if (e.dimension() == 0)
132+
{
133+
XTENSOR_THROW(std::runtime_error, "Cannot take the FFT of a scalar expression");
134+
}
131135
if constexpr (xtl::is_complex<typename std::decay<E>::type::value_type>::value)
132136
{
133137
using precision = typename value_type::value_type;
@@ -159,10 +163,14 @@ namespace xt
159163
template <class E>
160164
inline auto ifft(E&& e, std::ptrdiff_t axis = -1)
161165
{
166+
if (e.dimension() == 0)
167+
{
168+
XTENSOR_THROW(std::runtime_error, "Cannot take the iFFT of a scalar expression");
169+
}
162170
if constexpr (xtl::is_complex<typename std::decay<E>::type::value_type>::value)
163171
{
164172
// check the length of the data on that axis
165-
const std::size_t n = e.shape(axis);
173+
const std::size_t n = e.shape(xt::normalize_axis(e.dimension(), axis));
166174
if (n == 0)
167175
{
168176
XTENSOR_THROW(std::runtime_error, "Cannot take the iFFT along an empty dimention");

test/CMakeLists.txt

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,9 @@ endforeach()
231231

232232
file(GLOB XTENSOR_PREPROCESS_FILES files/cppy_source/*.cppy)
233233

234+
# Sanitizer support
235+
include(${CMAKE_SOURCE_DIR}/cmake/sanitizers.cmake)
236+
234237
# This target should only be run when the test source files have been changed.
235238
add_custom_target(
236239
preprocess_cppy
@@ -258,6 +261,8 @@ foreach(filename IN LISTS COMMON_BASE XTENSOR_TESTS)
258261
endif()
259262
target_include_directories(${targetname} PRIVATE ${XTENSOR_INCLUDE_DIR})
260263
target_link_libraries(${targetname} PRIVATE xtensor doctest::doctest ${CMAKE_THREAD_LIBS_INIT})
264+
target_compile_options(${targetname} PRIVATE $<$<BOOL:USE_SANITIZER>:${SANITIZER_COMPILE_OPTIONS}>)
265+
target_link_options(${targetname} PRIVATE $<$<BOOL:USE_SANITIZER>:${SANITIZER_LINK_OPTIONS}>)
261266
add_custom_target(
262267
x${targetname}
263268
COMMAND ${targetname}
@@ -282,10 +287,30 @@ if(XTENSOR_USE_OPENMP)
282287
target_compile_definitions(test_xtensor_lib PRIVATE XTENSOR_USE_OPENMP)
283288
endif()
284289

290+
target_compile_options(test_xtensor_lib PRIVATE $<$<BOOL:USE_SANITIZER>:${SANITIZER_COMPILE_OPTIONS}>)
291+
target_link_options(test_xtensor_lib PRIVATE $<$<BOOL:USE_SANITIZER>:${SANITIZER_LINK_OPTIONS}>)
292+
293+
# doctest's String union + MSan libc interceptors (strlen/strcmp) cause
294+
# false-positive use-of-uninitialized-value reports during reporter
295+
# registration and exception formatting. These cannot be suppressed at the
296+
# attribute level because MSan's libc interceptors check memory regardless
297+
# of calling-function attributes. The combined convenience binary therefore
298+
# opts out of MSan instrumentation so the xtest target can complete.
299+
# Individual per-test executables (test_xarray, test_xview, etc.) remain
300+
# fully instrumented and are the correct targets for MSan CI validation.
301+
if(USE_SANITIZER MATCHES "memory")
302+
target_compile_options(test_xtensor_lib PRIVATE -fno-sanitize=memory)
303+
target_link_options(test_xtensor_lib PRIVATE -fno-sanitize=memory)
304+
endif()
305+
285306
target_include_directories(test_xtensor_lib PRIVATE ${XTENSOR_INCLUDE_DIR})
286307
target_link_libraries(test_xtensor_lib PRIVATE xtensor doctest::doctest ${CMAKE_THREAD_LIBS_INIT})
287308

288-
add_custom_target(xtest COMMAND test_xtensor_lib DEPENDS test_xtensor_lib)
309+
add_custom_target(
310+
xtest
311+
COMMAND $<TARGET_FILE:test_xtensor_lib>
312+
DEPENDS test_xtensor_lib
313+
)
289314
add_test(NAME xtest COMMAND test_xtensor_lib)
290315

291316
# Some files will be compiled twice, however compiling common files in a static

test/msan_suppressions.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# MSan false positive: doctest reporter registration during static init
2+
# doctest::String has internal padding that MSan flags as uninitialized
3+
# when std::map compares keys during insert.
4+
fun:*doctest::detail::registerReporterImpl*
5+
src:*doctest/doctest.h

test/test_xadapt.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ namespace xt
132132
a1(1, 0) = static_cast<int>(i);
133133
EXPECT_EQ(i, data[i * size + st]);
134134
}
135+
136+
delete[] data;
135137
}
136138

137139
TEST(xarray_adaptor, pointer_acquire_ownership)
@@ -300,6 +302,8 @@ namespace xt
300302
a1(1, 0) = static_cast<int>(i);
301303
EXPECT_EQ(i, data[i * size + st]);
302304
}
305+
306+
delete[] data;
303307
}
304308

305309
TEST(xtensor_adaptor, pointer_const_no_ownership)

test/test_xblockwise_reducer.cpp

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,14 +111,24 @@ namespace xt
111111
dynamic_shape<std::size_t> chunk_shape({5, 4, 2});
112112
xarray<int> input_exp(shape);
113113

114-
// just iota is a bit boring since it will
115-
// lead to an uniform variance
116-
std::iota(input_exp.begin(), input_exp.end(), -5);
117-
for (std::size_t i = 0; i < input_exp.size(); ++i)
114+
if (std::is_same<tester_type, prod_tester>::value)
118115
{
119-
if (i % 2)
116+
for (std::size_t i = 0; i < input_exp.size(); ++i)
120117
{
121-
input_exp.flat(i) += 10;
118+
input_exp.flat(i) = (i % 2 == 0) ? 1 : -1;
119+
}
120+
}
121+
else
122+
{
123+
// just iota is a bit boring since it will
124+
// lead to an uniform variance
125+
std::iota(input_exp.begin(), input_exp.end(), -5);
126+
for (std::size_t i = 0; i < input_exp.size(); ++i)
127+
{
128+
if (i % 2)
129+
{
130+
input_exp.flat(i) += 10;
131+
}
122132
}
123133
}
124134

test/test_xbuffer_adaptor.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,8 @@ namespace xt
201201
size_t size2 = 50;
202202
XT_EXPECT_THROW(adapt.resize(size2), std::runtime_error);
203203
EXPECT_EQ(adapt.size(), size1);
204+
205+
delete[] data1;
204206
}
205207

206208
TEST(xbuffer_adaptor, no_owner_iterating)

test/test_xfft.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ namespace xt
2727
REQUIRE(A == doctest::Approx(std::abs(res(k))).epsilon(.0001));
2828
}
2929

30+
TEST(xfft, scalar_input_throws)
31+
{
32+
auto scalar = xt::xarray<float>::from_shape({});
33+
scalar() = 1.0f;
34+
35+
XT_EXPECT_THROW(xt::fft::fft(scalar), std::runtime_error);
36+
XT_EXPECT_THROW(xt::fft::ifft(scalar), std::runtime_error);
37+
}
38+
3039
TEST(xfft, convolve_power_2)
3140
{
3241
xt::xarray<float> x = {1.0, 1.0, 1.0, 5.0};

0 commit comments

Comments
 (0)