GWP-ASan

GWP-ASan est une fonctionnalité d'allocation de mémoire native qui permet de détecter les bugs use-after-free et heap-buffer-overflow. Son nom informel est un acronyme récursif : GWP-ASan Will Provide Allocation SANity (littéralement : GWP-ASan assure l'intégrité de l'allocation). Contrairement à HWASan ou au débogage malloc, GWP-ASan ne nécessite pas de source ni de recompilation (autrement dit, il fonctionne avec des éléments prédéfinis). Il est également compatible avec les processus 32 bits et 64 bits (bien que les plantages 32 bits contiennent moins d'informations de débogage). Cet article décrit les mesures que vous devez prendre pour activer cette fonctionnalité dans votre application. GWP-ASan est disponible dans les applications qui ciblent Android 11 (niveau d'API 30) ou version ultérieure.

Présentation

GWP-ASan est activé sur certaines applications système et certains exécutables de plate-forme spécifiques de manière aléatoire lors du démarrage du processus. Activez GWP-ASan dans votre propre application pour détecter les bugs liés à la mémoire et préparer votre application pour qu'elle soit compatible avec l'extension d'ajout de balises de mémoire ARM (MTE). Les mécanismes d'échantillonnage de l'allocation assurent également la fiabilité par rapport aux requêtes de type "death".

Une fois activé, GWP-ASan intercepte un sous-ensemble d'allocations de segments de mémoire sélectionné de manière aléatoire et le place dans une région spéciale qui détecte les bugs de corruption de tas de mémoire difficiles à détecter. Avec suffisamment d'utilisateurs, même ce faible taux d'échantillonnage identifie des bugs de sécurité qui ne sont pas détectés par les tests réguliers dans les segments de mémoire. Par exemple, GWP-ASan a détecté un nombre significatif de bugs dans le navigateur Chrome (dont beaucoup ne sont encore visibles que par un nombre limité de personnes).

GWP-ASan collecte des informations supplémentaires sur toutes les allocations qu'il intercepte. Ces informations sont disponibles lorsque GWP-ASan détecte une violation de la sécurité de la mémoire, et sont automatiquement placées dans le rapport de plantage natif, ce qui peut faciliter considérablement le débogage (voir l'exemple).

GWP-ASan est conçu pour ne pas entraîner de surcharge de processeur importante. GWP-ASan introduit une faible surcharge de mémoire RAM fixe, le cas échéant. Cette surcharge est déterminée par le système Android et est actuellement d'environ 70 kibioctets (Kio) pour chaque processus concerné.

Activer votre application

GWP-ASan peut être activé par les applications au niveau de chaque processus à l'aide de la balise android:gwpAsanMode dans le fichier manifeste de l'application. Les options suivantes sont compatibles :

  • Toujours désactivé (android:gwpAsanMode="never") : ce paramètre désactive complètement GWP-ASan dans votre application. Il s'agit de la valeur par défaut pour les applications autres que les applications système.

  • Par défaut (android:gwpAsanMode="default" ou non spécifié) : Android 13 (niveau d'API 33) ou version antérieure : GWP-ASan est désactivé. Android 14 (niveau d'API 34) ou version ultérieure : Recoverable GWP-ASan est activé.

  • Toujours activé (android:gwpAsanMode="always") : ce paramètre active GWP-ASan dans votre application, y compris les suivants :

    1. Le système d'exploitation réserve une quantité de RAM fixe pour les opérations GWP-ASan, environ 70 Kio par processus concerné. Activez GWP-ASan si votre application n'est pas sensible à l'augmentation de l'utilisation de la mémoire.

    2. GWP-ASan intercepte un sous-ensemble d'allocations de segments de mémoire choisi de manière aléatoire et les place dans une région spéciale qui détecte de manière fiable les violations de sécurité de la mémoire.

    3. En cas de violation de la sécurité de la mémoire dans la région spéciale, GWP-ASan met fin au processus.

    4. GWP-ASan fournit des informations supplémentaires sur l'erreur dans le rapport d'erreur.

Pour activer GWP-ASan dans le monde entier pour votre application, ajoutez les éléments suivants au fichier AndroidManifest.xml :

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

De plus, GWP-ASan peut être explicitement activé ou désactivé pour des sous-processus spécifiques de votre application. Vous pouvez cibler des activités et des services à l'aide de processus explicitement activés ou désactivés pour GWP-ASan. Voici un exemple :

<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>

Recoverable GWP-ASan

Android 14 (niveau d'API 34) ou version ultérieure est compatible avec Recoverable GWP-ASan, ce qui aide les développeurs à détecter les bugs liés à des erreurs d'utilisation après libération (use-after-free) et de dépassement de mémoire tampon dans un tas (heap-buffer-overflow) sans nuire à l'expérience utilisateur Lorsque android:gwpAsanMode n'est pas spécifié dans un élément AndroidManifest.xml, l'application utilise l'outil Recoverable GWP-ASan.

Recoverable GWP-ASan présente les différences suivantes par rapport au GWP-ASan de base :

  1. Recoverable GWP-ASan n'est activée que pour environ 1 % des lancements d'applications, plutôt que pour chaque lancement d'application.
  2. Lorsqu'un bug d'utilisation après libération ou de dépassement de mémoire tampon du tas est détecté, il apparaît dans le rapport d'erreur (tombstone). Ce rapport d'erreur est disponible via l'API ActivityManager#getHistoricalProcessExitReasons, identique à la fonctionnalité GWP-ASan d'origine.
  3. Au lieu de se fermer après la suppression du rapport d'erreur, Recoverable GWP-ASan permet une corruption de la mémoire et l'application continue de s'exécuter. Le processus peut se poursuivre normalement, mais le comportement de l'application n'est plus spécifié. En raison de la corruption de la mémoire, l'application peut planter à un moment arbitraire à l'avenir ou continuer sans aucun impact visible par l'utilisateur.
  4. Recoverable GWP-ASan est désactivé une fois le rapport d'erreur supprimé. Par conséquent, une application ne peut recevoir qu'un seul rapport Recoverable GWP-ASan par lancement d'application.
  5. Si un gestionnaire de signaux personnalisé est installé dans l'application, il n'est jamais appelé pour un signal SIGSEGV indiquant une erreur Recoverable GWP-ASan.

Étant donné que les plantages de Recoverable GWP-ASan indiquent des instances réelles de corruption de mémoire sur les appareils des utilisateurs finaux, nous vous recommandons vivement de trier et de corriger les bugs identifiés par Recoverable GWP-ASan avec une priorité élevée.

Assistance pour les développeurs

Ces sections décrivent les problèmes qui peuvent survenir lorsque vous utilisez GWP-ASan et comment les résoudre.

Les traces d'allocation ou de désallocation sont manquantes

Si vous diagnostiquez un plantage natif qui semble ne pas comporter de frames d'allocation ou de désallocation, votre application ne comporte probablement pas de pointeurs de frame. GWP-ASan utilise des pointeurs de frame afin d'enregistrer des traces d'allocation et de désallocation pour des raisons de performances. Sans eux, il ne peut pas dérouler la trace de la pile.

Les pointeurs de frame sont activés par défaut pour les appareils arm64 et désactivés par défaut pour les appareils arm32. Étant donné que les applications n'ont pas de contrôle sur libc, GWP-ASan ne peut généralement pas collecter de traces d'allocation ni de désallocation pour les exécutables ou les applications 32 bits. Les applications 64 bits doivent s'assurer qu'elles ne sont pas créées avec -fomit-frame-pointer afin que GWP-ASan puisse collecter les traces de la pile d'allocation et de désallocation.

Reproduire les violations de sécurité

GWP-ASan est conçu pour détecter les cas de violation de la sécurité des segments de mémoire sur les appareils des utilisateurs. Il fournit autant de contexte que possible sur le plantage (trace d'accès au cas de non-respect, chaîne de cause et traces d'allocation ou de désallocation). Toutefois, il reste parfois difficile de déduire comment le cas de non-respect s'est produit. Malheureusement, comme la détection des bugs est probabiliste, il est souvent difficile de reproduire les problèmes signalés par GWP-ASan sur un appareil local.

Dans ce cas, si le bug affecte les appareils 64 bits, vous devez utiliser HWAddressSanitizer (HWASan). HWASan détecte les violations de sécurité de la mémoire de manière fiable sur la pile, le tas de mémoire et les variables globales. L'exécution de votre application avec HWASan peut reproduire de manière fiable le même résultat que celui signalé par GWP-ASan.

Si l'exécution de votre application sous HWASan ne suffit pas pour déterminer la cause première d'un bug, essayez de tester le code en question avec des données aléatoires. Dans ce cas, ciblez vos tests en fonction des informations du rapport GWP-ASan, qui peut détecter et révéler de manière fiable les problèmes sous-jacents liés à l'intégrité du code.

Exemple

Cet exemple de code natif présente un bug "use-after-free" affectant les segments de mémoire :

#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));
}

Pour un test exécuté à l'aide de l'exemple de code ci-dessus, GWP-ASan a détecté l'utilisation interdite et a déclenché le rapport d'erreur ci-dessous. GWP-ASan a automatiquement optimisé le rapport en fournissant des informations sur le type de plantage, les métadonnées d'allocation et les traces associées de la pile d'allocation et de désallocation.

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
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)
      ...

Plus d'informations

Pour en savoir plus sur l'implémentation de GWP-ASan, consultez la documentation de LLVM. Pour en savoir plus sur les rapports d'erreur natifs Android, consultez la section concernant le diagnostic des plantages natifs.