diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml new file mode 100644 index 000000000..ee5965748 --- /dev/null +++ b/.github/workflows/sanitizers.yml @@ -0,0 +1,114 @@ +name: Sanitizers +on: + workflow_dispatch: + pull_request: + push: + branches: [master] +concurrency: + group: ${{ github.workflow }}-${{ github.job }}-${{ github.ref }} + cancel-in-progress: true +defaults: + run: + shell: bash -e -l {0} +jobs: + build: + runs-on: ${{ matrix.os }} + name: sanitizer / ${{ matrix.sys.compiler }} ${{ matrix.sys.version }} / ${{ matrix.config.name }} / ${{ matrix.sys.name }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04] + sys: + - {compiler: clang, version: '21', name: asan, sanitizer: address} + - {compiler: clang, version: '21', name: msan, sanitizer: memory} + - {compiler: clang, version: '21', name: lsan, sanitizer: leak} + - {compiler: clang, version: '21', name: ubsan, sanitizer: undefined} + config: + - {name: Debug} + + steps: + + - name: Install LLVM and Clang + if: matrix.sys.compiler == 'clang' + run: | + wget https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + sudo ./llvm.sh ${{matrix.sys.version}} + sudo apt-get install -y clang-tools-${{matrix.sys.version}} + sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-${{matrix.sys.version}} 200 + sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-${{matrix.sys.version}} 200 + sudo update-alternatives --install /usr/bin/clang-scan-deps clang-scan-deps /usr/bin/clang-scan-deps-${{matrix.sys.version}} 200 + sudo update-alternatives --set clang /usr/bin/clang-${{matrix.sys.version}} + sudo update-alternatives --set clang++ /usr/bin/clang++-${{matrix.sys.version}} + sudo update-alternatives --set clang-scan-deps /usr/bin/clang-scan-deps-${{matrix.sys.version}} + + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set conda environment + uses: mamba-org/setup-micromamba@main + with: + environment-name: myenv + environment-file: environment-dev.yml + init-shell: bash + cache-downloads: true + + - name: Configure using CMake + run: | + export CC=clang + export CXX=clang++ + cmake -G Ninja \ + -Bbuild \ + -DCMAKE_BUILD_TYPE=${{matrix.config.name}} \ + -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX \ + -DBUILD_TESTS=ON \ + -DUSE_SANITIZER=${{ matrix.sys.sanitizer }} + + - name: Build tests + working-directory: build + run: cmake --build . --config ${{matrix.config.name}} --target test_xtensor_lib --parallel 8 + + - name: Run tests + working-directory: build + run: | + SAN=${{ matrix.sys.sanitizer }} + case "$SAN" in + address) + export ASAN_OPTIONS=log_path=asan_log_:alloc_dealloc_mismatch=0:halt_on_error=0:handle_abort=0 + export ASAN_SAVE_DUMPS=AsanDump.dmp + ;; + memory) + export MSAN_OPTIONS=log_path=msan_log_:halt_on_error=0 + ;; + leak) + export LSAN_OPTIONS=log_path=lsan_log_:halt_on_error=0 + ;; + undefined) + export UBSAN_OPTIONS=log_path=ubsan_log_:halt_on_error=0:print_stacktrace=1 + ;; + esac + ctest -R ^xtest$ --output-on-failure + + - name: Upload sanitizer log + if: always() + uses: actions/upload-artifact@v6 + with: + name: sanitizer-log-${{ matrix.sys.sanitizer }}-${{ matrix.sys.compiler }}-${{ matrix.sys.version }}-${{ matrix.config.name }}-${{ runner.os }} + path: '**/*san_log_*' + if-no-files-found: ignore + + - name: Upload sanitizer dump + if: always() + uses: actions/upload-artifact@v6 + with: + name: sanitizer-dump-${{ matrix.sys.sanitizer }}-${{ matrix.sys.compiler }}-${{ matrix.sys.version }}-${{ matrix.config.name }}-${{ runner.os }} + path: '**/AsanDump.dmp' + if-no-files-found: ignore + + - name: Return errors if sanitizer log content is not empty + if: always() + run: | + if [ -n "$(find build/test -name '*san_log_*' -type f -size +0 2>/dev/null)" ]; then + echo "Sanitizer detected errors. See the log for details." + exit 1 + fi diff --git a/cmake/sanitizers.cmake b/cmake/sanitizers.cmake new file mode 100644 index 000000000..716021f96 --- /dev/null +++ b/cmake/sanitizers.cmake @@ -0,0 +1,47 @@ +set(AVALAIBLE_SANITIZERS "address;leak;memory;thread;undefined") +OPTION(USE_SANITIZER "Enable sanitizer(s). Options are: ${AVALAIBLE_SANITIZERS}. Case insensitive; multiple options delimited by comma or space possible." "") +string(TOLOWER "${USE_SANITIZER}" USE_SANITIZER) + +if((CMAKE_BUILD_TYPE IN_LIST "Debug;RelWithDebInfo") AND USE_SANITIZER) + message(FATAL_ERROR "❌ Sanitizer only supported in Debug and RelWithDebInfo build types.") +endif() + +if(USE_SANITIZER) + if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$,$>,$<$:EditAndContinue>,$<$:ProgramDatabase>>") + + if(USE_SANITIZER MATCHES "address") + list(APPEND SANITIZER_COMPILE_OPTIONS /fsanitize=address /D_DISABLE_VECTOR_ANNOTATION /D_DISABLE_STRING_ANNOTATION) + else() + message(FATAL_ERROR "❌ Sanitizer not supported by MSVC: ${USE_SANITIZER}. It only supports 'address'.") + endif() + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + if(USE_SANITIZER MATCHES "address") + list(APPEND SANITIZER_COMPILE_OPTIONS /fsanitize=address /D_DISABLE_VECTOR_ANNOTATION /D_DISABLE_STRING_ANNOTATION) + list(APPEND SANITIZER_LINK_LIBRARIES clang_rt.asan_dynamic-x86_64 clang_rt.asan_dynamic_runtime_thunk-x86_64) + else() + message(FATAL_ERROR "❌ Sanitizer not supported by Clang-MSVC: ${USE_SANITIZER}. It only supports 'address'.") + endif() + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + foreach(sanitizer ${USE_SANITIZER}) + if(NOT ${sanitizer} IN_LIST AVALAIBLE_SANITIZERS) + message(FATAL_ERROR "❌ Sanitizer not supported: ${sanitizer}. It should be one of: ${AVALAIBLE_SANITIZERS}.") + endif() + list(APPEND SANITIZER_COMPILE_OPTIONS -fsanitize=${sanitizer}) + list(APPEND SANITIZER_LINK_OPTIONS -fsanitize=${sanitizer}) + if (${sanitizer} MATCHES "memory") + list(APPEND SANITIZER_LINK_LIBRARIES -fsanitize-memory-track-origins -fPIE -pie) + list(APPEND SANITIZER_LINK_OPTIONS -fsanitize-memory-track-origins -fPIE -pie) + endif() + endforeach() + list(APPEND SANITIZER_COMPILE_OPTIONS -fno-omit-frame-pointer) + else() + message(FATAL_ERROR "❌ Sanitizer: Unsupported compiler: ${CMAKE_CXX_COMPILER_ID}") + endif() + + list(REMOVE_DUPLICATES SANITIZER_COMPILE_OPTIONS) + list(REMOVE_DUPLICATES SANITIZER_LINK_OPTIONS) + list(REMOVE_DUPLICATES SANITIZER_LINK_LIBRARIES) + + message(STATUS "🔍 Using sanitizer: ${USE_SANITIZER}") +endif() diff --git a/include/xtensor/views/index_mapper.hpp b/include/xtensor/views/index_mapper.hpp index 574bcb476..330a71baa 100644 --- a/include/xtensor/views/index_mapper.hpp +++ b/include/xtensor/views/index_mapper.hpp @@ -193,7 +193,7 @@ namespace xt * @throws Assertion failure if `i != 0` for integral slices. * @throws Assertion failure if `i >= slice.size()` for non-integral slices. */ - template + template size_t map_ith_index(const view_type& view, const Index i) const; /** @@ -490,16 +490,16 @@ namespace xt { if constexpr (ACCESS == access_t::SAFE) { - return container.at(map_ith_index(view, indices[Is])...); + return container.at(map_ith_index(view, indices[Is])...); } else { - return container(map_ith_index(view, indices[Is])...); + return container(map_ith_index(view, indices[Is])...); } } template - template + template auto index_mapper>::map_ith_index(const view_type& view, const Index i) const -> size_t @@ -515,14 +515,51 @@ namespace xt if constexpr (std::is_integral_v) { - assert(i == 0); + if constexpr (ACCESS == access_t::SAFE) + { + if (i != 0) + { + XTENSOR_THROW(std::out_of_range, "Index out of range in index_mapper access"); + } + } + else + { + assert(i == 0); + } return size_t(slice); } + else if constexpr (xt::detail::is_xall_slice>::value) + { + return size_t(i); + } else { using slice_size_type = typename current_slice::size_type; - assert(i < slice.size()); - return size_t(slice(static_cast(i))); + const auto slice_index = static_cast(i); + + if constexpr (ACCESS == access_t::SAFE) + { + if constexpr (std::is_signed_v) + { + if (slice_index < 0 || slice_index >= slice.size()) + { + XTENSOR_THROW(std::out_of_range, "Index out of range in index_mapper access"); + } + } + else if (slice_index >= slice.size()) + { + XTENSOR_THROW(std::out_of_range, "Index out of range in index_mapper access"); + } + } + else + { + if constexpr (std::is_signed_v) + { + assert(slice_index >= 0); + } + assert(slice_index < slice.size()); + } + return size_t(slice(slice_index)); } } else diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 8341230ee..743396aea 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -231,6 +231,9 @@ endforeach() file(GLOB XTENSOR_PREPROCESS_FILES files/cppy_source/*.cppy) +# Sanitizer support +include(${CMAKE_SOURCE_DIR}/cmake/sanitizers.cmake) + # This target should only be run when the test source files have been changed. add_custom_target( preprocess_cppy @@ -258,6 +261,8 @@ foreach(filename IN LISTS COMMON_BASE XTENSOR_TESTS) endif() target_include_directories(${targetname} PRIVATE ${XTENSOR_INCLUDE_DIR}) target_link_libraries(${targetname} PRIVATE xtensor doctest::doctest ${CMAKE_THREAD_LIBS_INIT}) + target_compile_options(${targetname} PRIVATE $<$:${SANITIZER_COMPILE_OPTIONS}>) + target_link_options(${targetname} PRIVATE $<$:${SANITIZER_LINK_OPTIONS}>) add_custom_target( x${targetname} COMMAND ${targetname} @@ -282,6 +287,9 @@ if(XTENSOR_USE_OPENMP) target_compile_definitions(test_xtensor_lib PRIVATE XTENSOR_USE_OPENMP) endif() +target_compile_options(test_xtensor_lib PRIVATE $<$:${SANITIZER_COMPILE_OPTIONS}>) +target_link_options(test_xtensor_lib PRIVATE $<$:${SANITIZER_LINK_OPTIONS}>) + target_include_directories(test_xtensor_lib PRIVATE ${XTENSOR_INCLUDE_DIR}) target_link_libraries(test_xtensor_lib PRIVATE xtensor doctest::doctest ${CMAKE_THREAD_LIBS_INIT}) diff --git a/test/test_xadapt.cpp b/test/test_xadapt.cpp index 5876ff926..ef5451b91 100644 --- a/test/test_xadapt.cpp +++ b/test/test_xadapt.cpp @@ -132,6 +132,8 @@ namespace xt a1(1, 0) = static_cast(i); EXPECT_EQ(i, data[i * size + st]); } + + delete[] data; } TEST(xarray_adaptor, pointer_acquire_ownership) @@ -300,6 +302,8 @@ namespace xt a1(1, 0) = static_cast(i); EXPECT_EQ(i, data[i * size + st]); } + + delete[] data; } TEST(xtensor_adaptor, pointer_const_no_ownership) diff --git a/test/test_xbuffer_adaptor.cpp b/test/test_xbuffer_adaptor.cpp index d6ad32b21..5b0a5fcb6 100644 --- a/test/test_xbuffer_adaptor.cpp +++ b/test/test_xbuffer_adaptor.cpp @@ -201,6 +201,8 @@ namespace xt size_t size2 = 50; XT_EXPECT_THROW(adapt.resize(size2), std::runtime_error); EXPECT_EQ(adapt.size(), size1); + + delete[] data1; } TEST(xbuffer_adaptor, no_owner_iterating)