Address Sanitizer

El NDK de Android admite Address Sanitizer (también conocido como ASan) a partir del nivel de API 27 (Android O MR 1).

ASan es una herramienta rápida basada en compiladores para detectar errores de memoria en código nativo. ASan detecta:

  • Desabastecimiento de búfer de pila
  • Uso de pila después de liberación
  • Uso de pila fuera del alcance
  • Cierre doble o cierre wild

La sobrecarga de la CPU de ASan es de aproximadamente el doble, el tamaño del código aumenta entre el 50% y el doble, y la sobrecarga de la memoria es considerable (depende de tus patrones de asignación, pero es de aproximadamente el doble).

App de ejemplo

En una app de ejemplo, se muestra cómo configurar una variante de compilación para asan.

Compilación

Para compilar el código nativo (JNI) de tu app con Address Sanitizer, haz lo siguiente:

ndk-build

En tu Application.mk:

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

Para cada módulo de tu Android.mk:

LOCAL_ARM_MODE := arm

CMake

En build.gradle de tu módulo:

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

Para cada destino de tu CMakeLists.txt:

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

Ejecución

A partir de Android O MR1 (API nivel 27), una app puede proporcionar una unión de la secuencia de comandos de shell que puede unir o reemplazar el proceso de la app. Esto permite que una aplicación depurable personalice el inicio de tu aplicación, lo que permite usar ASan en los dispositivos de producción.

  1. Agrega android:debuggable al manifiesto de la aplicación.
  2. Configura useLegacyPackaging como true en el archivo build.gradle de tu app. Para obtener más información, consulta la guía de unión de la secuencia de comandos de shell.
  3. Agrega la biblioteca de tiempo de ejecución ASan a jniLibs del módulo de tu app.
  4. Agrega archivos wrap.sh con el siguiente contenido a cada directorio en el directorio src/main/resources/lib.

    #!/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
    "$@"
    

Suponiendo que el módulo de la aplicación de tu proyecto se llame app, tu estructura de directorio final debe incluir lo siguiente:

<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

Seguimiento de pila

Address Sanitizer tiene que desenrollar la pila en cada llamada malloc/realloc/free. En este caso, hay dos opciones:

  1. Un desenredador "rápido" basado en el puntero de marco. Esto es lo que se usa siguiendo las instrucciones de la sección de compilación.

  2. Un desenrollador "lento" de CFI. En este modo, ASan usa _Unwind_Backtrace. Solo requiere -funwind-tables, que normalmente está habilitado de forma predeterminada.

El desenrollador rápido es el predeterminado para malloc/realloc/free. El desenrollador lento es el predeterminado para los seguimientos de pila fatales. A fin de habilitar el desenrollador lento para todos los seguimientos de la pila, agrega fast_unwind_on_malloc=0 a la variable ASAN_OPTIONS en tu wrap.sh.