diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d36d62ea..f26ee9fe3 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,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) + 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/ndk/lib/CMakeLists.txt b/ndk/lib/CMakeLists.txt index 1b4153eda..211acf177 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 73708d9dc..75a94be5e 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 000000000..175c8e1df --- /dev/null +++ b/scripts/setup-android-sanitizer.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +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}" + +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 8be0938ab..b0cc4d19c 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