From 900f22239459da3177a1f6cfcd58a7c869a483e7 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 31 Aug 2026 09:31:19 +0200 Subject: [PATCH 1/2] ci: add ASAN job for Android --- .github/workflows/ci.yml | 12 ++++++++ ndk/lib/CMakeLists.txt | 19 ++++++++++++ ndk/lib/build.gradle.kts | 23 +++++++++++++-- scripts/setup-android-sanitizer.sh | 47 ++++++++++++++++++++++++++++++ tests/test_dotnet_signals.py | 4 +-- 5 files changed, 101 insertions(+), 4 deletions(-) create mode 100755 scripts/setup-android-sanitizer.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d36d62eaa..5dae27a6e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -240,6 +240,12 @@ jobs: ANDROID_API: 35 ANDROID_NDK: 29.0.14206865 ANDROID_ARCH: x86_64 + - name: Android (API 36, NDK 30 + asan) + os: ubuntu-latest + ANDROID_API: 36 + ANDROID_NDK: 30.0.16138531 + ANDROID_ARCH: x86_64 + RUN_ANALYZER: asan name: ${{ matrix.name }} runs-on: ${{ matrix.os }} @@ -501,6 +507,12 @@ jobs: script: | # Sync emulator clock with host to avoid timestamp assertion failures adb shell cmd alarm set-time $(date +%s000) || adb shell su root date $(date -u +%m%d%H%M%Y.%S) + for sanitizer in asan tsan; do + if [[ ",$RUN_ANALYZER," == *",$sanitizer,"* ]]; then + # Set up the runtime for pytest and Gradle tests + scripts/setup-android-sanitizer.sh "$sanitizer" + fi + done pip install --upgrade --requirement tests/requirements.txt pytest --capture=no --verbose tests (cd ndk && ./gradlew :sentry-native-ndk:connectedDebugAndroidTest --stacktrace) diff --git a/ndk/lib/CMakeLists.txt b/ndk/lib/CMakeLists.txt index 1b4153eda8..211acf1771 100644 --- a/ndk/lib/CMakeLists.txt +++ b/ndk/lib/CMakeLists.txt @@ -16,6 +16,25 @@ set(SENTRY_BUILD_SHARED_LIBS ON) # Adding sentry-native project add_subdirectory(${SENTRY_NATIVE_SRC} sentry_build) +if(WITH_ASAN_OPTION) + set(SENTRY_ANDROID_SANITIZER address) +elseif(WITH_TSAN_OPTION) + set(SENTRY_ANDROID_SANITIZER thread) +endif() + +if(SENTRY_ANDROID_SANITIZER) + set(SENTRY_ANDROID_SANITIZER_TARGETS sentry-android) + if(ENABLE_TESTS) + list(APPEND SENTRY_ANDROID_SANITIZER_TARGETS sentry-android-test) + endif() + foreach(target IN LISTS SENTRY_ANDROID_SANITIZER_TARGETS) + target_compile_options(${target} PRIVATE + -g -fsanitize=${SENTRY_ANDROID_SANITIZER} -fno-omit-frame-pointer) + target_link_libraries(${target} PRIVATE + -fsanitize=${SENTRY_ANDROID_SANITIZER}) + endforeach() +endif() + # Android logging library find_library(LOG_LIB log) diff --git a/ndk/lib/build.gradle.kts b/ndk/lib/build.gradle.kts index 73708d9dc7..75a94be5eb 100644 --- a/ndk/lib/build.gradle.kts +++ b/ndk/lib/build.gradle.kts @@ -5,6 +5,8 @@ plugins { } var sentryNativeSrc: String = "${project.projectDir}/../.." +val sanitizer = System.getenv("RUN_ANALYZER").orEmpty().split(',') + .firstOrNull { it == "asan" || it == "tsan" } android { compileSdk = 35 @@ -17,18 +19,35 @@ android { externalNativeBuild { cmake { - arguments.add(0, "-DANDROID_STL=c++_static") + arguments.add(0, "-DANDROID_STL=${if (sanitizer != null) "c++_shared" else "c++_static"}") arguments.add(0, "-DSENTRY_NATIVE_SRC=$sentryNativeSrc") + if (sanitizer == "asan") { + arguments.add(0, "-DWITH_ASAN_OPTION=ON") + } + if (sanitizer == "tsan") { + arguments.add(0, "-DWITH_TSAN_OPTION=ON") + } } } ndk { - abiFilters.addAll(listOf("x86", "armeabi-v7a", "x86_64", "arm64-v8a")) + abiFilters.addAll( + if (sanitizer == "tsan") listOf("x86_64", "arm64-v8a") + else listOf("x86", "armeabi-v7a", "x86_64", "arm64-v8a") + ) } testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } + if (sanitizer != null) { + System.getenv("ANDROID_NDK")?.let { ndkVersion = it } + sourceSets.getByName("androidTest") { + jniLibs.srcDir("build/$sanitizer/jniLibs") + resources.srcDir("build/$sanitizer/resources") + } + } + // we use the default NDK and CMake versions based on the AGP's version // https://developer.android.com/studio/projects/install-ndk#apply-specific-version externalNativeBuild { diff --git a/scripts/setup-android-sanitizer.sh b/scripts/setup-android-sanitizer.sh new file mode 100755 index 0000000000..02e96edbdc --- /dev/null +++ b/scripts/setup-android-sanitizer.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euxo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +: "${ANDROID_HOME:?ANDROID_HOME must point to the Android SDK}" +: "${ANDROID_NDK:?ANDROID_NDK must name an installed NDK version}" +: "${ANDROID_ARCH:?ANDROID_ARCH must name the emulator ABI}" +sanitizer="${1:?sanitizer must be asan or tsan}" + +if [[ "$sanitizer" != asan && "$sanitizer" != tsan ]]; then + echo "Unsupported sanitizer: $sanitizer" >&2 + exit 1 +fi + +case "$ANDROID_ARCH" in + armeabi-v7a) runtime_arch=arm ;; + arm64-v8a) runtime_arch=aarch64 ;; + x86) runtime_arch=i686 ;; + x86_64) runtime_arch=x86_64 ;; + *) + echo "Unsupported Android ABI: $ANDROID_ARCH" >&2 + exit 1 + ;; +esac + +ndk_dir="$ANDROID_HOME/ndk/$ANDROID_NDK" +runtime=$(find "$ndk_dir/toolchains/llvm/prebuilt" \ + -name "libclang_rt.$sanitizer-$runtime_arch-android.so" -print -quit) +if [[ ! -f "$runtime" ]]; then + echo "No $sanitizer runtime for Android ABI: $ANDROID_ARCH" >&2 + exit 1 +fi + +sanitizer_dir="$script_dir/../ndk/lib/build/$sanitizer" +runtime_dir="$sanitizer_dir/jniLibs/$ANDROID_ARCH" +resource_dir="$sanitizer_dir/resources/lib/$ANDROID_ARCH" + +"$ANDROID_HOME/platform-tools/adb" push "$runtime" /data/local/tmp/ +mkdir -p "$runtime_dir" +cp "$runtime" "$runtime_dir/" + +wrapper="$ndk_dir/wrap.sh/$sanitizer.sh" +if [[ -f "$wrapper" ]]; then + mkdir -p "$resource_dir" + cp "$wrapper" "$resource_dir/wrap.sh" +fi diff --git a/tests/test_dotnet_signals.py b/tests/test_dotnet_signals.py index 8be0938ab3..b0cc4d19cc 100644 --- a/tests/test_dotnet_signals.py +++ b/tests/test_dotnet_signals.py @@ -373,8 +373,8 @@ def run_android_native_crash(strategy=None, reinit=False): @pytest.mark.skipif( - not is_android or int(is_android) < 26, - reason="needs Android API 26+ (tombstoned)", + not is_android or int(is_android) < 26 or is_asan or is_tsan, + reason="needs Android API 26+ (tombstoned) without sanitizers", ) # Mono on Android keeps using CHAIN_AT_START. Preload is the CoreCLR path, # where sentry-native can enter the signal chain before the runtime installs From 3d5a693f48ec48b5f6262ecbd7185cc2b9cbbdb9 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 31 Aug 2026 13:02:26 +0200 Subject: [PATCH 2/2] ++ --- .github/workflows/ci.yml | 7 +------ scripts/setup-android-sanitizer.sh | 4 +++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5dae27a6e0..f26ee9fe3a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -507,12 +507,7 @@ jobs: script: | # Sync emulator clock with host to avoid timestamp assertion failures adb shell cmd alarm set-time $(date +%s000) || adb shell su root date $(date -u +%m%d%H%M%Y.%S) - for sanitizer in asan tsan; do - if [[ ",$RUN_ANALYZER," == *",$sanitizer,"* ]]; then - # Set up the runtime for pytest and Gradle tests - scripts/setup-android-sanitizer.sh "$sanitizer" - fi - done + scripts/setup-android-sanitizer.sh "$RUN_ANALYZER" pip install --upgrade --requirement tests/requirements.txt pytest --capture=no --verbose tests (cd ndk && ./gradlew :sentry-native-ndk:connectedDebugAndroidTest --stacktrace) diff --git a/scripts/setup-android-sanitizer.sh b/scripts/setup-android-sanitizer.sh index 02e96edbdc..175c8e1dfe 100755 --- a/scripts/setup-android-sanitizer.sh +++ b/scripts/setup-android-sanitizer.sh @@ -3,10 +3,12 @@ set -euxo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +sanitizer="${1:-}" +[[ -n "$sanitizer" ]] || exit 0 + : "${ANDROID_HOME:?ANDROID_HOME must point to the Android SDK}" : "${ANDROID_NDK:?ANDROID_NDK must name an installed NDK version}" : "${ANDROID_ARCH:?ANDROID_ARCH must name the emulator ABI}" -sanitizer="${1:?sanitizer must be asan or tsan}" if [[ "$sanitizer" != asan && "$sanitizer" != tsan ]]; then echo "Unsupported sanitizer: $sanitizer" >&2