GWP-ASan

GWP-ASan to natywna funkcja przydzielania pamięci, która pomaga wykrywać błędy use-after-free i heap-buffer-overflow. Jego nieformalna nazwa jest akronimem rekurencyjnym, czyli „GWP-ASan Wzawiść Prowido Twojej lokalizacji. W przeciwieństwie do HWASan lub Malloc Debug GWP-ASan nie wymaga źródła ani ponownej kompilacji (czyli działa z gotowymi komponentami) i działa zarówno w przypadku procesów 32-, jak i 64-bitowych (chociaż awarie 32-bitowe zawierają mniej informacji o debugowaniu). W tym temacie opisujemy działania, które musisz wykonać, aby włączyć tę funkcję w swojej aplikacji. GWP-ASan jest dostępny w aplikacjach kierowanych na Androida 11 (poziom interfejsu API 30) lub nowszego.

Przegląd

Narzędzie GWP-ASan jest włączone w niektórych losowo wybranych aplikacjach systemowych i plikach wykonywalnych platformy podczas uruchamiania procesu (lub gdy zygote jest rozwidlona). Włącz GWP-ASan w swojej aplikacji, aby wykryć błędy związane z pamięcią i przygotować aplikację do obsługi rozszerzenia ARM Memory Tagging Extension (MTE). Mechanizmy próbkowania alokacji również zapewniają niezawodność wobec zapytań o śmierć.

Po włączeniu GWP-ASan przechwytuje losowo wybrany podzbiór alokacji sterty i umieszcza je w specjalnym regionie, który wychwytuje trudne do wykrycia błędy uszkodzenia pamięci stosu. Przy wystarczającej liczbie użytkowników nawet tak niska częstotliwość próbkowania znajdzie błędy związane z bezpieczeństwem pamięci sterty, których nie wykryto podczas regularnych testów. Na przykład zespół GWP-ASan znalazł znaczną liczbę błędów w przeglądarce Chrome (wiele z nich nadal działa w trybie ograniczonego dostępu).

GWP-ASan zbiera dodatkowe informacje o wszystkich alokacji, które przechwytuje. Te informacje są dostępne po wykryciu naruszenia bezpieczeństwa pamięci przez GWP-ASan i automatycznie umieszczane w raporcie o awarii natywnej, co może znacznie ułatwić debugowanie (patrz przykład).

Narzędzie GWP-ASan zostało zaprojektowane tak, aby nie powodować znaczącego obciążenia procesora. GWP-ASan wprowadza niewielki, stały narzut pamięci RAM, gdy ta funkcja jest włączona. To obciążenie jest określane przez system Android i wynosi obecnie około 70 kibibajtów (KiB) dla każdego odpowiedniego procesu.

Włącz aplikację

Aplikacje GWP-ASan mogą być włączane przez aplikacje na poziomie poszczególnych procesów za pomocą tagu android:gwpAsanMode w manifeście aplikacji. Obsługiwane są te opcje:

  • Zawsze wyłączona (android:gwpAsanMode="never"): to ustawienie całkowicie wyłącza GWP-ASan w aplikacji i jest domyślne w przypadku aplikacji niesystemowych.

  • Domyślny (android:gwpAsanMode="default" lub nieokreślony): Android 13 (poziom interfejsu API 33) i starsze wersje – GWP-ASan jest wyłączony. Android 14 (poziom interfejsu API 34) i nowsze – włączona jest opcja Recoverable GWP-ASan.

  • Zawsze włączone (android:gwpAsanMode="always"): to ustawienie powoduje włączenie GWP-ASan w aplikacji, co obejmuje:

    1. System operacyjny rezerwuje stałą ilość pamięci RAM na potrzeby operacji GWP-ASan, około 70 KB na każdy proces, którego dotyczy problem. (Włącz GWP-ASan, jeśli aplikacja nie jest krytycznie wrażliwa na wzrost wykorzystania pamięci).

    2. GWP-ASan przechwytuje losowo wybrany podzbiór alokacji sterty i umieszcza je w specjalnym regionie, który niezawodnie wykrywa naruszenia bezpieczeństwa pamięci.

    3. Gdy w specjalnym regionie wystąpi naruszenie bezpieczeństwa pamięci, GWP-ASan kończy proces.

    4. GWP-ASan podaje dodatkowe informacje na temat tego błędu w raporcie o awarii.

Aby włączyć globalnie GWP-ASan w swojej aplikacji, do pliku AndroidManifest.xml dodaj te elementy:

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

Możesz też włączyć lub wyłączyć GWP-ASan w konkretnych podprocesach aplikacji. Możesz kierować działania i usługi za pomocą procesów, które mają włączone lub wyłączone korzystanie z GWP-ASan. Oto przykład:

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

Możliwe do odzyskania GWP-ASan

Android 14 (poziom interfejsu API 34) i nowsze obsługują kod Recoverable GWP-ASan, który ułatwia programistom znalezienie w produkcji błędów związanych z przepełnieniem bufora na stercie i błędami po jej wykorzystaniu bez pogorszenia wygody użytkowników. Gdy AndroidManifest.xml nie ma określonego identyfikatora android:gwpAsanMode, aplikacja używa Recoverable GWP-ASan.

możliwy do odzyskania GWP-ASan różni się od podstawowego GWP-ASan pod tymi względami:

  1. Narzędzie GWP-ASan z możliwością odzyskiwania jest włączane tylko w przypadku około 1% uruchomień aplikacji, a nie przy każdym uruchomieniu aplikacji.
  2. Błąd ten pojawia się w raporcie o awariach (tombstone) w przypadku błędu „używania po sterowaniu” lub „przepełnienia bufora na stercie”. Ten raport o awariach jest dostępny przez interfejs API ActivityManager#getHistoricalProcessExitReasons, taki sam jak oryginalny GWP-ASan.
  3. Narzędzie Recoverable GWP-ASan umożliwia uszkodzenie pamięci, a aplikacja nieprzerwanie działa, a nie zamyka się po zrzucie raportu o awariach. Chociaż ten proces może przebiegać w zwykły sposób, działanie aplikacji nie jest już określone. Z powodu uszkodzenia pamięci aplikacja może ulec awarii w dowolnym momencie w przyszłości lub działać bez zakłóceń.
  4. Po skopiowaniu raportu o awariach usługa GWP-ASan do przywrócenia jest wyłączona. W związku z tym aplikacja może otrzymać tylko 1 możliwy do przywrócenia raport GWP-ASan na uruchomienie aplikacji.
  5. Jeśli w aplikacji jest zainstalowany niestandardowy moduł obsługi sygnału, nigdy nie jest on wywoływany dla sygnału SIGSEGV, który wskazuje na usterkę odzyskaną w GWP-ASan.

Ponieważ awarie GWP-ASan z możliwością odzyskiwania wskazują na prawdziwe przypadki uszkodzenia pamięci na urządzeniach użytkowników, zdecydowanie zalecamy sklasyfikowanie i naprawienie błędów wykrytych przez narzędzie Recoverable GWP-ASan o wysokim priorytecie.

Pomoc dla programistów

W tych sekcjach omawiamy problemy, które mogą wystąpić podczas korzystania z GWP-ASan, i sposoby ich rozwiązywania.

Brakuje logów alokacji/alokacji

Jeśli diagnozujesz awaria systemową, w której brakuje ramek alokacji i rozdzielania, prawdopodobnie w aplikacji brakuje wskaźników klatek. GWP-ASan używa wskaźników ramek do rejestrowania zrzutów stosu alokacji i transakcji ze względu na wydajność. Nie jest też w stanie rozwinąć zrzutu stosu, jeśli go nie ma.

Wskaźniki klatek są domyślnie włączone na urządzeniach z systemem arm64 i domyślnie wyłączone na urządzeniach z arm32. Aplikacje nie mają kontroli nad biblioteką libc, więc zasadniczo GWP-ASan nie może zbierać logów czasu alokacji/dealokacji dla 32-bitowych plików wykonywalnych lub aplikacji. Aplikacje 64-bitowe powinny się upewnić, że nie są one kompilowane za pomocą -fomit-frame-pointer, aby narzędzie GWP-ASan mogło zbierać zrzuty stosu alokacji i alokacji.

Odtwarzanie przypadków naruszenia bezpieczeństwa

Narzędzie GWP-ASan ma wykrywać naruszenia bezpieczeństwa pamięci sterty na urządzeniach użytkowników. GWP-ASan podaje jak najwięcej informacji o awarii (zrzut dostępu do naruszenia, ciąg znaków przyczyny i ślady alokacji/deallokacji), ale wciąż może być trudno wywnioskować, jak doszło do naruszenia. Wykrywanie błędów jest bardzo prawdopodobne, dlatego raporty GWP-ASan są często trudne do odtworzenia na urządzeniu lokalnym.

W takich przypadkach, jeśli błąd dotyczy urządzeń 64-bitowych, użyj narzędzia HWAddressSanitizer (HWASan). HWASan niezawodnie wykrywa naruszenia bezpieczeństwa pamięci w przypadku stosów, sterty i globalnych. Uruchomienie aplikacji przy użyciu HWASan może niezawodnie przynieść ten sam wynik raportowany przez GWP-ASan.

Jeśli uruchomienie aplikacji w środowisku HWASan nie wystarcza do wywołania błędu, spróbuj usuwać kwestionowany kod. Możesz kierować działania związane z zamazywaniem kodu na podstawie informacji z raportu GWP-ASan, który pozwala skutecznie wykrywać i ujawniać problemy ze stanem kodu.

Przykład

W tym przykładowym kodzie natywnym występuje błąd typu „używanie po bezpłonności” na stercie:

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

W przypadku uruchomienia testowego z użyciem powyższego przykładowego kodu GWP-ASan udało się wykryć nielegalne użycie i wygenerować poniższy raport o awariach. GWP-ASan automatycznie ulepszył raport, podając informacje o typie awarii, metadanych alokacji oraz powiązanych śladach stosu alokacji i atrybucji.

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

Więcej informacji

Więcej informacji o implementacji GWP-ASan znajdziesz w dokumentacji LLVM. Aby dowiedzieć się więcej o raportach o awariach natywnych aplikacji na Androida, przeczytaj artykuł o diagnozowaniu awarii natywnych.