GWP-ASan

GWP-ASan to funkcja alokatora pamięci natywnej, która pomaga znajdować błędy odwołania do pamięci po jej zwolnieniuprzepełnienia bufora na stercie. Jego nieformalna nazwa to rekurencyjny akronim „GWP-ASan Will Provide Allocation SANity”. W przeciwieństwie do HWASanMalloc Debug GWP-ASan nie wymaga kodu źródłowego ani ponownej kompilacji (działa z wstępnie skompilowanymi plikami) i działa zarówno w przypadku procesów 32-bitowych, jak i 64-bitowych (chociaż awarie 32-bitowe mają mniej informacji do debugowania). W tym artykule opisujemy działania, które musisz wykonać, aby włączyć tę funkcję w aplikacji. GWP-ASan jest dostępny w aplikacjach kierowanych na Androida 11 (API na poziomie 30) lub nowszego.

Omówienie

GWP-ASan jest włączany w przypadku niektórych losowo wybranych aplikacji systemowych i plików wykonywalnych platformy podczas uruchamiania procesu (lub gdy proces zygote się rozwidla). Włącz GWP-ASan w swojej aplikacji, aby łatwiej znajdować błędy związane z pamięcią i przygotować aplikację do obsługi rozszerzenia ARM Memory Tagging Extension (MTE). Mechanizmy próbkowania przydziału zapewniają też niezawodność w przypadku zapytań o dużej liczbie wyników.

Po włączeniu GWP-ASan przechwytuje losowo wybrany podzbiór alokacji sterty i umieszcza je w specjalnym regionie, który wykrywa trudne do wykrycia błędy uszkodzenia pamięci sterty. Przy wystarczającej liczbie użytkowników nawet tak niski wskaźnik próbkowania znajdzie błędy bezpieczeństwa pamięci sterty, które nie są wykrywane podczas zwykłych testów. Na przykład GWP-ASan wykrył znaczną liczbę błędów w przeglądarce Chrome (wiele z nich jest nadal objętych ograniczonym dostępem).

GWP-ASan zbiera dodatkowe informacje o wszystkich przydziałach, które przechwytuje. Te informacje są dostępne, gdy GWP-ASan wykryje naruszenie bezpieczeństwa pamięci. Są one automatycznie umieszczane w raporcie o awarii natywnej, co może znacznie ułatwić debugowanie (patrz przykład).

GWP-ASan nie powoduje znaczącego obciążenia procesora. GWP-ASan po włączeniu wprowadza niewielki, stały narzut na pamięć RAM. Wielkość tego narzutu jest określana przez system Android i obecnie wynosi około 70 kibibajtów (KiB) na każdy proces, którego dotyczy.

Włączanie aplikacji

GWP-ASan można włączyć w aplikacjach na poziomie poszczególnych procesów, używając tagu android:gwpAsanMode w pliku manifestu aplikacji. Obsługiwane są te opcje:

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

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

  • Zawsze włączone (android:gwpAsanMode="always"): to ustawienie włącza w aplikacji GWP-ASan, który obejmuje:

    1. System operacyjny rezerwuje stałą ilość pamięci RAM na potrzeby operacji GWP-ASan, około 70 KiB na każdy proces, którego dotyczy problem. (Włącz GWP-ASan, jeśli Twoja aplikacja nie jest szczególnie wrażliwa na zwiększenie zużycia pamięci).

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

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

    4. GWP-ASan dostarcza dodatkowych informacji o błędzie w raporcie o awarii.

Aby włączyć GWP-ASan globalnie w aplikacji, dodaj do pliku AndroidManifest.xml ten kod:

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

Dodatkowo GWP-ASan można włączyć lub wyłączyć w przypadku konkretnych podprocesów aplikacji. Możesz kierować działania i usługi na procesy, które zostały wyraźnie włączone lub wyłączone w GWP-ASan. Przykład znajdziesz poniżej:

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

Błędy GWP-ASan, które można odzyskać

Android 14 (poziom interfejsu API 34) i nowsze wersje obsługują GWP-ASan z możliwością odzyskiwania, który pomaga deweloperom znajdować w wersjach produkcyjnych błędy przepełnienia bufora na stercie (heap-buffer-overflow) i odwołania do pamięci po jej zwolnieniu (heap-use-after-free) bez pogarszania wrażeń użytkowników. Jeśli w AndroidManifest.xml nie określono wartości android:gwpAsanMode, aplikacja używa odzyskiwalnego GWP-ASan.

Wersja GWP-ASan z możliwością odzyskiwania różni się od podstawowej wersji GWP-ASan w tych aspektach:

  1. Odzyskiwalny GWP-ASan jest włączony tylko w przypadku około 1% uruchomień aplikacji, a nie w przypadku każdego uruchomienia.
  2. Gdy zostanie wykryty błąd heap-use-after-free lub heap-buffer-overflow, pojawi się on w raporcie o awarii (tombstone). Ten raport o awariach jest dostępny za pomocą interfejsu ActivityManager#getHistoricalProcessExitReasons API, tak samo jak oryginalny GWP-ASan.
  3. W przeciwieństwie do zwykłego GWP-ASan, który po wygenerowaniu raportu o awarii kończy działanie, Recoverable GWP-ASan umożliwia uszkodzenie pamięci, a aplikacja nadal działa. Proces może przebiegać jak zwykle, ale zachowanie aplikacji nie jest już określone. Z powodu uszkodzenia pamięci aplikacja może w przyszłości ulec awarii w dowolnym momencie lub może działać dalej bez widocznych dla użytkownika skutków.
  4. Po wygenerowaniu raportu o awarii funkcja GWP-ASan z możliwością odzyskiwania jest wyłączana. Dlatego aplikacja może otrzymać tylko jeden raport GWP-ASan z możliwością odzyskania na jedno uruchomienie.
  5. Jeśli w aplikacji jest zainstalowany niestandardowy moduł obsługi sygnałów, nigdy nie jest on wywoływany w przypadku sygnału SIGSEGV, który wskazuje na błąd GWP-ASan, który można naprawić.

Błędy zgłaszane przez GWP-ASan z możliwością odzyskania wskazują rzeczywiste przypadki uszkodzenia pamięci na urządzeniach użytkowników, dlatego zalecamy, aby błędy zidentyfikowane przez GWP-ASan z możliwością odzyskania były traktowane priorytetowo.

Pomoc dla programistów

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

Brak śladów przydzielania i zwalniania pamięci

Jeśli diagnozujesz błąd natywny, w którym brakuje ramek alokacji/dealokacji, w aplikacji prawdopodobnie nie ma wskaźników ramek. GWP-ASan używa wskaźników ramki do rejestrowania śladów alokacji i dealokacji ze względu na wydajność. Jeśli wskaźniki nie są obecne, nie może cofnąć śladu stosu.

Wskaźniki ramek są domyślnie włączone na urządzeniach z architekturą arm64 i wyłączone na urządzeniach z architekturą arm32. Ponieważ aplikacje nie mają kontroli nad biblioteką libc, GWP-ASan nie może (zwykle) zbierać śladów alokacji i dealokacji w przypadku 32-bitowych plików wykonywalnych ani aplikacji. Aplikacje 64-bitowe powinny być nie kompilowane za pomocą -fomit-frame-pointer, aby GWP-ASan mógł zbierać ślady stosu alokacji i dealokacji.

Powielanie naruszeń zasad bezpieczeństwa

GWP-ASan został zaprojektowany tak, aby wykrywać naruszenia bezpieczeństwa pamięci sterty na urządzeniach użytkowników. GWP-ASan dostarcza jak najwięcej informacji kontekstowych o awarii (ślad dostępu naruszenia, ciąg przyczyn i ślady alokacji/dealokacji), ale nadal może być trudno ustalić, jak doszło do naruszenia. Niestety wykrywanie błędów ma charakter probabilistyczny, więc 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 HWAddressSanitizer (HWASan). HWASan niezawodnie wykrywa naruszenia bezpieczeństwa pamięci na stosie, stercie i w przypadku zmiennych globalnych. Uruchomienie aplikacji z HWASan może niezawodnie odtworzyć ten sam wynik, który jest zgłaszany przez GWP-ASan.

Jeśli uruchomienie aplikacji w HWASan nie wystarczy do znalezienia przyczyny błędu, spróbuj przetestować dany kod. Możesz kierować testy fuzzingowe na podstawie informacji w raporcie GWP-ASan, który może niezawodnie wykrywać i ujawniać problemy z kodem.

Przykład

Ten przykładowy kod natywny zawiera błąd użycia po zwolnieniu pamięci sterty:

#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 testowego uruchomienia z użyciem powyższego przykładowego kodu GWP-ASan wykrył nieprawidłowe użycie i wygenerował poniższy raport o awarii. GWP-ASan automatycznie wzbogacił raport, podając informacje o typie awarii, metadanych alokacji oraz powiązanych śladach stosu alokacji i dealokacji.

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
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 szczegółach implementacji GWP-ASan znajdziesz w dokumentacji LLVM. Więcej informacji o raportach o awariach systemowych na Androidzie znajdziesz w artykule Diagnosing Native Crashes (Diagnostyka awarii systemowych).