GWP-ASan

O GWP-ASan é um recurso nativo de alocação de memória que ajuda a localizar bugs de uso após a liberação e de overflow do buffer de heap (links em inglês). O nome informal é um acrônimo recursivo, "GWP-ASan Will Provide Allocation SANity". Ao contrário de HWASan ou Depuração Malloc, o GWP-ASan não requer origem ou recompilação (ou seja, funciona com pré-compiladores) e funciona em processos de 32 e 64 bits, embora falhas de 32 bits tenham menos informações de depuração. Este tópico descreve as ações que você precisa realizar para ativar esse recurso no seu app. O GWP-ASan está disponível em apps direcionados ao Android 11 (API de nível 30) ou versões mais recentes.

Informações gerais

O GWP-ASan é ativado em alguns aplicativos do sistema e executáveis de plataforma selecionados aleatoriamente na inicialização do processo (ou quando o zigoto se divide). Ative o GWP-ASan no seu próprio app para encontrar bugs relacionados à memória e preparar o app para a compatibilidade com a extensão ARM Memory Tagging Extension (MTE) (link em inglês). Os mecanismos de amostragem de alocação também oferecem confiabilidade em consultas de morte (link em inglês).

Uma vez ativado, o GWP-ASan intercepta um subconjunto de alocações de heap escolhido aleatoriamente e o coloca em uma região especial que captura bugs de corrupção de memória de heap difíceis de detectar. Com usuários suficientes, até mesmo essa baixa taxa de amostragem encontrará bugs de segurança de memória de heap que não são encontrados por meio de testes regulares. Por exemplo, o GWP-ASan encontrou um número significativo de bugs no navegador Chrome (muitos deles ainda estão sob visualização restrita).

O GWP-ASan coleta informações adicionais sobre todas as alocações que intercepta. Essas informações estão disponíveis quando o GWP-ASan detecta uma violação de segurança de memória e é colocado automaticamente no relatório de falhas nativas, o que pode ajudar significativamente na depuração (veja um exemplo).

O GWP-ASan foi projetado para não sobrecarregar significativamente a CPU. O GWP-ASan introduz uma sobrecarga pequena e fixa de RAM quando ativado. Essa sobrecarga é decidida pelo sistema Android e atualmente é de aproximadamente 70 kibibytes (KiB) para cada processo afetado.

Ativar no seu app

O GWP-ASan pode ser ativado por apps em um nível por processo usando a tag android:gwpAsanMode no manifesto do app. As opções a seguir são permitidas:

  • Sempre desativado (android:gwpAsanMode="never"): essa configuração desativa completamente o GWP-ASan no seu app e é o padrão para apps que não são do sistema.

  • Padrão (android:gwpAsanMode="default" ou não especificado): Android 13 (nível 33 da API) e versões anteriores. O GWP-ASan está desativado. Android 14 (nível 34 da API) e versões mais recentes: o GWP-ASan recuperável está ativado.

  • Sempre ativado (android:gwpAsanMode="always"): essa configuração ativa o GWP-ASan no seu app, o que inclui o seguinte:

    1. O sistema operacional reserva uma quantidade fixa de RAM para operações do GWP-ASan, aproximadamente 70 KiB para cada processo afetado. Ative o GWP-ASan se o app não for extremamente sensível a aumentos no uso da memória.

    2. O GWP-ASan intercepta um subconjunto de alocações de heap escolhido aleatoriamente e o coloca em uma região especial que detecta violações de segurança da memória de maneira confiável.

    3. Quando ocorre uma violação de segurança de memória na região especial, o GWP-ASan encerra o processo.

    4. O GWP-ASan fornece mais informações no relatório de falhas.

Para ativar o GWP-ASan globalmente no seu app, adicione o seguinte ao arquivo AndroidManifest.xml:

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

Além disso, o GWP-ASan pode ser ativado ou desativado explicitamente para subprocessos específicos do app. É possível segmentar atividades e serviços usando processos explicitamente ativados ou desativados do GWP-ASan. Veja o exemplo a seguir:

<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 recuperável

O Android 14 (nível 34 da API) e versões mais recentes oferecem suporte ao GWP-ASan recuperável, que ajuda os desenvolvedores a encontrar bugs de estouro de buffer de heap e de uso de heap após a liberação na produção sem prejudicar a experiência do usuário. Quando android:gwpAsanMode não não é especificado em um AndroidManifest.xml, o app usa o GWP-ASan recuperável.

O GWP-ASan recuperável difere do GWP-ASan base das seguintes maneiras:

  1. O GWP-ASan recuperável é ativado apenas em aproximadamente 1% das inicializações de apps, em vez de cada inicialização.
  2. Quando um bug de uso de heap após a liberação ou de estouro de buffer de heap é detectado, esse bug aparece no relatório de erros (tombstone). Esse relatório de erros está disponível pela API ActivityManager#getHistoricalProcessExitReasons, o mesmo que o GWP-ASan original.
  3. Em vez de sair após o despejo do relatório de falhas, o GWP-ASan recuperável permite que a corrupção da memória ocorra e o app continua em execução. Embora o processo possa continuar normalmente, o comportamento do app não será mais especificado. Devido à corrupção da memória, o app pode falhar em algum momento arbitrário no futuro ou continuar sem qualquer impacto visível ao usuário.
  4. O GWP-ASan recuperável é desativado após o despejo do relatório de erros. Portanto, um app pode receber apenas um único relatório recuperável do GWP-Asan por inicialização.
  5. Se um gerenciador de sinal personalizado estiver instalado no app, ele nunca será chamado para um sinal SIGSEGV que indica uma falha do GWP-ASan recuperável.

Como as falhas recuperáveis do GWP-ASan indicam instâncias reais de corrupção de memória em dispositivos de usuários finais, é altamente recomendável fazer a triagem e a correção de bugs identificados pelo GWP-ASan recuperável com alta prioridade.

Suporte para desenvolvedores

Estas seções descrevem problemas que podem ocorrer ao usar o GWP-ASan e como resolvê-los.

Os traces de alocação/desalocação estão ausentes

Se você estiver diagnosticando uma falha nativa que parece estar sem frames de alocação/desalocação, seu aplicativo provavelmente não terá ponteiros de frame (link em inglês). O GWP-ASan usa ponteiros de frame para registrar os traces de alocação e desalocação por motivos de desempenho e não consegue desvincular o stack trace caso ele não esteja presente.

Os ponteiros de frame são ativados por padrão para dispositivos arm64 e desativados por padrão para dispositivos arm32. Como os aplicativos não têm controle sobre a libc, em geral, não é possível que o GWP-ASan colete traces de alocação/desalocação para apps ou executáveis de 32 bits. Aplicativos de 64 bits precisam garantir que eles não sejam criados com -fomit-frame-pointer para que o GWP-ASan possa coletar stack traces de alocação e desalocação.

Reprodução de violações de segurança

O GWP-ASan foi projetado para capturar violações de segurança da memória de heap nos dispositivos dos usuários. O GWP-ASan fornece o máximo de contexto possível sobre a falha (trace de acesso da violação, string de causa e traces de alocação/desalocação), mas ainda pode ser difícil deduzir como a violação ocorreu. Infelizmente, como a detecção de bugs é probabilística, os relatórios do GWP-ASan costumam ser difíceis de reproduzir em um dispositivo local.

Nesses casos, se o bug afetar dispositivos de 64 bits, use HWAddressSanitizer (HWASan). O HWASan detecta violações de segurança da memória de maneira confiável na pilha, no heap e em globais. Executar seu aplicativo com o HWASan pode reproduzir de forma confiável o mesmo resultado que está sendo relatado pelo GWP-ASan.

Nos casos em que a execução do aplicativo no HWASan é insuficiente para determinar a causa raiz de um bug, tente confundir o código em questão. Você pode segmentar seus esforços de confusão com base nas informações do relatório do GWP-ASan, que podem detectar e revelar de maneira confiável problemas de integridade do código.

Exemplo

Este exemplo de código nativo tem um bug de heap de uso após a liberação:

#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 executar um teste usando o código de exemplo acima, o GWP-ASan detectou o uso ilegal e acionou o relatórios de erros abaixo. O GWP-ASan aprimorou o relatório automaticamente, fornecendo informações sobre o tipo de falha, os metadados de alocação e os stack traces de alocação e desalocação associados.

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

Mais informações

Para saber mais sobre os detalhes de implementação do GWP-ASan, consulte a documentação do LLVM (link em inglês). Para saber mais sobre os relatórios de falhas nativas do Android, consulte Diagnóstico de falhas nativas.