GWP-ASan

GWP-ASan es una función de asignación de memoria nativa que ayuda a encontrar errores de uso después de liberación y desbordamiento del búfer del montón. Su nombre informal es un acrónimo recurrente en inglés, que indica "GWP-ASan Will Provide Allocation SANity" (GWP-ASan informará el estado de asignación). A diferencia de HWASan o la depuración de Malloc, GWP-ASan no requiere código fuente ni recompilación (es decir, funciona con compilaciones previas), y funciona en procesos de 32 y 64 bits (aunque las fallas de 32 bits tienen menos información de depuración). En este tema, se describen las acciones que debes realizar para habilitar esta función en tu app. GWP-ASan está disponible en apps orientadas a Android 11 (nivel de API 30) o versiones posteriores.

Descripción general

GWP-ASan está habilitado en algunas aplicaciones del sistema y en archivos ejecutables de la plataforma seleccionados al azar durante el inicio del proceso (o cuando Zygote se bifurca). Habilita GWP-ASan en tu propia app para ayudarte a encontrar errores relacionados con la memoria y preparar la app para la compatibilidad con extensiones de etiquetado de memoria (MTE) ARM. Los mecanismos de muestreo de asignación también proporcionan confiabilidad contra las consultas de fin de procesos.

Una vez habilitada, GWP-ASan intercepta un subconjunto de asignaciones de montón elegido al azar y las coloca en una región especial que capta errores de memoria del montón difíciles de detectar. Si hay suficientes usuarios, incluso esta baja tasa de muestreo encontrará errores de seguridad de la memoria de montón que no se encuentran mediante pruebas periódicas. Por ejemplo, GWP-ASan encontró una cantidad significativa de errores en el navegador Chrome (muchos de ellos aún están en la vista restringida).

GWP-ASan recopila información adicional sobre todas las asignaciones que intercepta. Esta información está disponible cuando GWP-ASan detecta una infracción de la seguridad de la memoria, y se posiciona automáticamente en el informe de fallas por error en código nativo, lo que puede ayudar mucho en la depuración (con sulta el ejemplo).

GWP-ASan está diseñada para no sobrecargar a la CPU de forma significativa. GWP-ASan introduce una pequeña sobrecarga de RAM fija cuando está habilitada. El sistema Android decide esta sobrecarga, que actualmente es de aproximadamente 70 kibibytes (KiB) para cada proceso afectado.

Cómo habilitarla en la app

Las apps pueden habilitar GWP-ASan a nivel de proceso mediante la etiqueta android:gwpAsanMode en el manifiesto de la app. Se admiten las siguientes opciones:

  • Siempre inhabilitado (android:gwpAsanMode="never"): esta configuración inhabilita GWP-ASan por completo en tu app y es la opción predeterminada para las apps que no son del sistema.

  • Predeterminado (android:gwpAsanMode="default" o sin especificar): Android 13 (nivel de API 33) y versiones anteriores (GWP-ASan está inhabilitado). Android 14 (nivel de API 34) y versiones posteriores: está habilitado el GWP-ASan recuperable.

  • Siempre habilitado (android:gwpAsanMode="always"): Esta configuración habilita GWP-ASan en tu app, que incluye lo siguiente:

    1. El sistema operativo reserva una cantidad fija de RAM para las operaciones de GWP-ASan, aproximadamente ~70 KiB para cada proceso afectado. (Habilita GWP-ASan si tu app no es extremadamente sensible a los aumentos en el uso de la memoria).

    2. GWP-ASan intercepta un subconjunto de asignaciones de montón aleatoriamente y las coloca en una región especial que detecta de manera confiable las infracciones a la seguridad de la memoria.

    3. Cuando se produce una infracción de la seguridad de la memoria en la región especial, GWP-ASan finaliza el proceso.

    4. GWP-ASan proporciona información adicional sobre el error en el informe de fallas.

Para habilitar GWP-ASan globalmente para tu app, agrega lo siguiente a tu archivo AndroidManifest.xml:

<application android:gwpAsanMode="always">
  ...
</application>

Además, GWP-ASan se puede habilitar o inhabilitar explícitamente para subprocesos específicos de tu app. Puedes orientarla hacia actividades y servicios mediante procesos explícitamente habilitados o inhabilitados de GWP-ASan. Consulta lo siguiente para ver un ejemplo:

<application>
  <processes>
    <!-- Create the (empty) application process -->
    <process />

    <!-- Create subprocesses with GWP-ASan both explicitly enabled and disabled. -->
    <process android:process=":gwp_asan_enabled"
               android:gwpAsanMode="always" />
    <process android:process=":gwp_asan_disabled"
               android:gwpAsanMode="never" />
  </processes>

  <!-- Target services and activities to be run on either the GWP-ASan enabled or disabled processes. -->
  <activity android:name="android.gwpasan.GwpAsanEnabledActivity"
            android:process=":gwp_asan_enabled" />
  <activity android:name="android.gwpasan.GwpAsanDisabledActivity"
            android:process=":gwp_asan_disabled" />
  <service android:name="android.gwpasan.GwpAsanEnabledService"
           android:process=":gwp_asan_enabled" />
  <service android:name="android.gwpasan.GwpAsanDisabledService"
           android:process=":gwp_asan_disabled" />
</application>

GWP-ASan recuperable

Android 14 (nivel de API 34) y las versiones posteriores admiten GWP-ASan recuperable, lo que ayuda a los desarrolladores a encontrar errores de desbordamiento del búfer de montón y de uso después de la liberación de montón en producción sin degradar la experiencia del usuario. Cuando no se especifica android:gwpAsanMode en un AndroidManifest.xml, la app usa GWP-ASan recuperable.

La GWP-ASan recuperable difiere de la GWP-ASan base de las siguientes maneras:

  1. La recuperación de GWP-ASan solo se habilita en aproximadamente el 1% de los inicios de la app, en lugar de en todos los inicios de aplicaciones.
  2. Cuando se detecta un error de desbordamiento del búfer de montón o de uso después de la liberación, el error aparece en el informe de fallas (tombstone). Este informe de fallas está disponible a través de la API de ActivityManager#getHistoricalProcessExitReasons, al igual que el GWP-ASan original.
  3. En lugar de salir después de volcar el informe de fallas, el GWP-ASan recuperable permite que se dañe la memoria y la app sigue ejecutándose. Si bien el proceso puede continuar como de costumbre, ya no se especifica el comportamiento de la app. Debido a los daños en la memoria, es posible que la app falle en algún momento arbitrario en el futuro o que continúe sin que el usuario vea un impacto visible.
  4. GWP-ASan recuperable se inhabilita después de que se vuelca el informe de fallas. Por lo tanto, una app puede obtener solo un informe de GWP-ASan recuperable por cada inicio de la app.
  5. Si hay un controlador de indicadores personalizado instalado en la app, nunca se llama para obtener una señal SIGSEGV que indique una falla de GWP-ASan recuperable.

Debido a que las fallas de Retrieveable GWP-ASan indican instancias reales de daños en la memoria en dispositivos del usuario final, te recomendamos priorizar y corregir errores identificados por Retrieveable GWP-ASan con una prioridad alta.

Asistencia para desarrolladores

Estas secciones describen los errores que pueden ocurrir cuando se usa GWP-ASan, y cómo abordarlos.

Faltan seguimientos de asignación o desasignación

Si estás realizando el diagnóstico de una falla por error en código nativo para la cual parece que faltan marcos de asignación/desasignación, es probable que tu aplicación no tenga punteros de marco. GWP-ASan usa punteros de marco para registrar los seguimientos de asignación y desasignación por motivos de rendimiento, y no puede deshacer el seguimiento de pila si no están presentes.

Los punteros de marco están activados de forma predeterminada para los dispositivos arm64 y desactivados de forma predeterminada para los dispositivos arm32. Dado que las aplicaciones no tienen control sobre libc, (por lo general) no es posible que GWP-ASan recopile seguimientos de asignación o desasignación para sistemas o apps de 32 bits. Debes asegurarte de que las aplicaciones de 64 bits no se hayan compilado con -fomit-frame-pointer para que GWP-ASan pueda recopilar seguimientos de pila de asignación y desasignación.

Cómo reproducir infracciones de seguridad

GWP-ASan está diseñada para detectar infracciones a la seguridad de la memoria de montón en los dispositivos de los usuarios. GWP-ASan proporciona la mayor cantidad de contexto posible sobre la falla (seguimiento de acceso de la infracción, string de causa y seguimientos de asignación/desasignación), pero aún podría ser difícil deducir cómo se produjo la infracción. Lamentablemente, como la detección de errores es probabilística, los informes de GWP-ASan suelen ser difíciles de reproducir en un dispositivo local.

En estas instancias, si el error afecta dispositivos de 64 bits, debes usar HWAddressSanitizer (HWASan). HWASan detecta las infracciones de seguridad de la memoria de manera confiable en pilas, el montón y las secciones globales. Si ejecutas tu aplicación con HWASan, podrías reproducir de manera confiable el mismo resultado que informa GWP-ASan.

En los casos donde ejecutar la aplicación en HWASan no sea suficiente para encontrar la causa raíz de un error, debes intentar realizar una prueba de Fuzz sobre el código en cuestión. Puedes orientar tus esfuerzos de Fuzzing según la información del informe de GWP-ASan, que puede detectar y revelar problemas de estado subyacentes del código de manera confiable.

Ejemplo

Este código nativo de ejemplo tiene un error de uso después de la liberación del montón:

#include <jni.h>
#include <string>
#include <string_view>

jstring native_get_string(JNIEnv* env) {
   std::string s = "Hellooooooooooooooo ";
   std::string_view sv = s + "World\n";

   // BUG: Use-after-free. `sv` holds a dangling reference to the ephemeral
   // string created by `s + "World\n"`. Accessing the data here is a
   // use-after-free.
   return env->NewStringUTF(sv.data());
}

extern "C" JNIEXPORT jstring JNICALL
Java_android11_test_gwpasan_MainActivity_nativeGetString(
    JNIEnv* env, jobject /* this */) {
  // Repeat the buggy code a few thousand times. GWP-ASan has a small chance
  // of detecting the use-after-free every time it happens. A single user who
  // triggers the use-after-free thousands of times will catch the bug once.
  // Alternatively, if a few thousand users each trigger the bug a single time,
  // you'll also get one report (this is the assumed model).
  jstring return_string;
  for (unsigned i = 0; i < 0x10000; ++i) {
    return_string = native_get_string(env);
  }

  return reinterpret_cast<jstring>(env->NewGlobalRef(return_string));
}

Para una ejecución de prueba con el código de ejemplo anterior, GWP-ASan detectó correctamente el uso ilegal y activó el informe de fallas que figura debajo. GWP-ASan mejoró automáticamente el informe ya que proporcionó información sobre el tipo de falla, los metadatos de asignación y los seguimientos de pila de asignación y desasignación asociados.

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'google/sargo/sargo:10/RPP3.200320.009/6360804:userdebug/dev-keys'
Revision: 'PVT1.0'
ABI: 'arm64'
Timestamp: 2020-04-06 18:27:08-0700
pid: 16227, tid: 16227, name: 11.test.gwpasan  >>> android11.test.gwpasan <<<
uid: 10238
signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x736ad4afe0
Cause: [GWP-ASan]: Use After Free on a 32-byte allocation at 0x736ad4afe0

backtrace:
      #00 pc 000000000037a090  /apex/com.android.art/lib64/libart.so (art::(anonymous namespace)::ScopedCheck::CheckNonHeapValue(char, art::(anonymous namespace)::JniValueType)+448)
      #01 pc 0000000000378440  /apex/com.android.art/lib64/libart.so (art::(anonymous namespace)::ScopedCheck::CheckPossibleHeapValue(art::ScopedObjectAccess&, char, art::(anonymous namespace)::JniValueType)+204)
      #02 pc 0000000000377bec  /apex/com.android.art/lib64/libart.so (art::(anonymous namespace)::ScopedCheck::Check(art::ScopedObjectAccess&, bool, char const*, art::(anonymous namespace)::JniValueType*)+612)
      #03 pc 000000000036dcf4  /apex/com.android.art/lib64/libart.so (art::(anonymous namespace)::CheckJNI::NewStringUTF(_JNIEnv*, char const*)+708)
      #04 pc 000000000000eda4  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (_JNIEnv::NewStringUTF(char const*)+40)
      #05 pc 000000000000eab8  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (native_get_string(_JNIEnv*)+144)
      #06 pc 000000000000edf8  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (Java_android11_test_gwpasan_MainActivity_nativeGetString+44)
      ...

deallocated by thread 16227:
      #00 pc 0000000000048970  /apex/com.android.runtime/lib64/bionic/libc.so (gwp_asan::AllocationMetadata::CallSiteInfo::RecordBacktrace(unsigned long (*)(unsigned long*, unsigned long))+80)
      #01 pc 0000000000048f30  /apex/com.android.runtime/lib64/bionic/libc.so (gwp_asan::GuardedPoolAllocator::deallocate(void*)+184)
      #02 pc 000000000000f130  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (std::__ndk1::_DeallocateCaller::__do_call(void*)+20)
      ...
      #08 pc 000000000000ed6c  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (std::__ndk1::basic_string<char, std::__ndk1::char_traits<char>, std::__ndk1::allocator<char> >::~basic_string()+100)
      #09 pc 000000000000ea90  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (native_get_string(_JNIEnv*)+104)
      #10 pc 000000000000edf8  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (Java_android11_test_gwpasan_MainActivity_nativeGetString+44)
      ...

allocated by thread 16227:
      #00 pc 0000000000048970  /apex/com.android.runtime/lib64/bionic/libc.so (gwp_asan::AllocationMetadata::CallSiteInfo::RecordBacktrace(unsigned long (*)(unsigned long*, unsigned long))+80)
      #01 pc 0000000000048e4c  /apex/com.android.runtime/lib64/bionic/libc.so (gwp_asan::GuardedPoolAllocator::allocate(unsigned long)+368)
      #02 pc 000000000003b258  /apex/com.android.runtime/lib64/bionic/libc.so (gwp_asan_malloc(unsigned long)+132)
      #03 pc 000000000003bbec  /apex/com.android.runtime/lib64/bionic/libc.so (malloc+76)
      #04 pc 0000000000010414  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (operator new(unsigned long)+24)
      ...
      #10 pc 000000000000ea6c  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (native_get_string(_JNIEnv*)+68)
      #11 pc 000000000000edf8  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (Java_android11_test_gwpasan_MainActivity_nativeGetString+44)
      ...

Más información

Para obtener más información sobre los detalles de implementación de GWP-ASan, consulta la documentación de LLVM. Para obtener más información sobre los informes de fallas por errores en código nativo de Android, consulta Cómo diagnosticar fallas por errores en código nativo.