Address Sanitizer

The Android NDK supports Address Sanitizer (also known as ASan) beginning with API level 27 (Android O MR 1).

ASan is a fast compiler-based tool for detecting memory bugs in native code. ASan detects:

  • Stack and heap buffer overflow/underflow
  • Heap use after free
  • Stack use outside scope
  • Double free/wild free

ASan runs on both 32-bit and 64-bit ARM, plus x86 and x86-64. ASan's CPU overhead is roughly 2x, code size overhead is between 50% and 2x, and the memory overhead is large (dependent on your allocation patterns, but on the order of 2x).

For 64-bit ARM, HWAddress Sanitizer may be a better choice.

Build

To build your app's native (JNI) code with Address Sanitizer, do the following:

ndk-build

In your Application.mk:

APP_STL := c++_shared # Or system, or none.
APP_CFLAGS := -fsanitize=address -fno-omit-frame-pointer
APP_LDFLAGS := -fsanitize=address

For each module in your Android.mk:

LOCAL_ARM_MODE := arm

CMake

In your module's build.gradle:

android {
    defaultConfig {
        externalNativeBuild {
            cmake {
                # Can also use system or none as ANDROID_STL.
                arguments "-DANDROID_ARM_MODE=arm", "-DANDROID_STL=c++_shared"
            }
        }
    }
}

For each target in your CMakeLists.txt:

target_compile_options(${TARGET} PUBLIC -fsanitize=address -fno-omit-frame-pointer)
set_target_properties(${TARGET} PROPERTIES LINK_FLAGS -fsanitize=address)

Run

Beginning with Android O MR1 (API level 27) an application can provide a wrap shell script that can wrap or replace the application process. This allows a debuggable application to customize their application startup, which enables using ASan on production devices.

  1. Add android:debuggable and android:extractNativeLibs=true to the application manifest. Note that the latter is the default for some configurations. See the wrap shell script for more information.
  2. Add the ASan runtime library to your app module's jniLibs.
  3. Add wrap.sh files with the following contents to each of the same directories.

    #!/system/bin/sh
    HERE="$(cd "$(dirname "$0")" && pwd)"
    export ASAN_OPTIONS=log_to_syslog=false,allow_user_segv_handler=1
    ASAN_LIB=$(ls $HERE/libclang_rt.asan-*-android.so)
    if [ -f "$HERE/libc++_shared.so" ]; then
        # Workaround for https://github.com/android-ndk/ndk/issues/988.
        export LD_PRELOAD="$ASAN_LIB $HERE/libc++_shared.so"
    else
        export LD_PRELOAD="$ASAN_LIB"
    fi
    "$@"
    

Assuming your project's application module is named app, your final directory structure should include the following:

<project root>
└── app
    └── src
        └── main
            ├── jniLibs
            │   ├── arm64-v8a
            │   │   └── libclang_rt.asan-aarch64-android.so
            │   ├── armeabi-v7a
            │   │   └── libclang_rt.asan-arm-android.so
            │   ├── x86
            │   │   └── libclang_rt.asan-i686-android.so
            │   └── x86_64
            │       └── libclang_rt.asan-x86_64-android.so
            └── resources
                └── lib
                    ├── arm64-v8a
                    │   └── wrap.sh
                    ├── armeabi-v7a
                    │   └── wrap.sh
                    ├── x86
                    │   └── wrap.sh
                    └── x86_64
                        └── wrap.sh

Stack traces

Address Sanitizer needs to unwind the stack on every malloc/realloc/free call. There are two options here:

  1. A "fast" frame pointer-based unwinder. This is what is used by following the instructions in the building section.

  2. A "slow" CFI unwinder. In this mode ASan uses _Unwind_Backtrace. It requires only -funwind-tables, which is normally enabled by default.

The fast unwinder is the default for malloc/realloc/free. The slow unwinder is the default for fatal stack traces. The slow unwinder can be enabled for all stack traces by adding fast_unwind_on_malloc=0 to the ASAN_OPTIONS variable in your wrap.sh.