GWP-ASan to funkcja natywnego alokatora pamięci, która pomaga znajdować błędy odwołania do pamięci po jej zwolnieniu (use-after-free) i przepełnienia bufora na stercie (heap-buffer-overflow) bugs. Jej nieformalna nazwa to rekurencyjny akronim „GWP-ASan Will Provide Allocation SANity”. W przeciwieństwie do HWASan i Malloc Debug, GWP-ASan nie wymaga kodu źródłowego ani ponownej kompilacji (czyli działa z gotowymi bibliotekami) i działa zarówno w procesach 32-, jak i 64-bitowych (chociaż w przypadku awarii w procesach 32-bitowych informacje na potrzeby debugowania są mniej szczegółowe). W tym artykule 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
GWP-ASan jest włączany w przypadku niektórych losowo wybranych aplikacji systemowych i plików wykonywalnych platformy po uruchomieniu procesu (lub gdy zygota tworzy proces potomny). Włącz GWP-ASan w swojej aplikacji, aby łatwiej znajdować błędy związane z pamięcią i przygotować aplikację na obsługę rozszerzenia ARM Memory Tagging Extension (MTE). Mechanizmy próbkowania alokacji zapewniają też niezawodność w przypadku zapytań o zakończenie procesu.
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. Nawet przy tak niskim współczynniku próbkowania, jeśli aplikacja ma wystarczającą liczbę użytkowników, GWP-ASan znajdzie błędy bezpieczeństwa pamięci sterty, które nie są wykrywane podczas zwykłych testów. Na przykład GWP-ASan znalazł znaczną liczbę błędów w przeglądarce Chrome (wiele z nich jest nadal widocznych tylko dla wybranych osób).
GWP-ASan zbiera dodatkowe informacje o wszystkich przechwyconych alokacjach. Te informacje są dostępne, gdy GWP-ASan wykryje naruszenie bezpieczeństwa pamięci . Są one automatycznie umieszczane w natywnym raporcie o awariach, co może znacznie ułatwić debugowanie (patrz Przykład).
GWP-ASan został zaprojektowany tak, aby nie powodować znaczącego obciążenia procesora. Po włączeniu GWP-ASan powoduje niewielkie, stałe obciążenie pamięci RAM. To obciążenie jest określane 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 procesu za pomocą 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 Twojej aplikacji i jest domyślne w przypadku aplikacji innych niż systemowe.Domyślne (
android:gwpAsanMode="default"lub nieokreślone): Android 13 (poziom interfejsu API 33) i starsze – GWP-ASan jest wyłączony. Android 14 (poziom interfejsu API 34) i nowsze – włączona jest funkcja Recoverable GWP-ASan.Zawsze włączone (
android:gwpAsanMode="always"): to ustawienie włącza GWP-ASan w Twojej aplikacji, co obejmuje te działania:System operacyjny rezerwuje stałą ilość pamięci RAM na potrzeby operacji GWP-ASan, czyli około 70 KiB na każdy proces, którego dotyczy. (Włącz GWP-ASan, jeśli Twoja aplikacja nie jest krytycznie wrażliwa na wzrost zużycia pamięci).
GWP-ASan przechwytuje losowo wybrany podzbiór alokacji sterty i umieszcza je w specjalnym regionie, który niezawodnie wykrywa naruszenia bezpieczeństwa pamięci.
Gdy w specjalnym regionie wystąpi naruszenie bezpieczeństwa pamięci, GWP-ASan zakończy proces.
GWP-ASan podaje dodatkowe informacje o błędzie w raporcie o awariach.
Aby włączyć GWP-ASan globalnie w swojej aplikacji, dodaj ten kod do pliku AndroidManifest.xml:
<application android:gwpAsanMode="always"> ... </application>
Dodatkowo GWP-ASan można włączyć lub wyłączyć w przypadku konkretnych podprocesów aplikacji. Możesz kierować reklamy na działania i usługi za pomocą procesów, które są wyraźnie włączone lub wyłączone w 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>
Recoverable GWP-ASan
Android 14 (poziom interfejsu API 34) i nowsze obsługują funkcję Recoverable GWP-ASan, która pomaga deweloperom znajdować błędy przepełnienia bufora na stercie (heap-buffer-overflow) i odwołania do pamięci po jej zwolnieniu (heap-use-after-free) w środowisku produkcyjnym bez pogarszania wrażeń użytkowników. Gdy w pliku AndroidManifest.xml nie określono wartości android:gwpAsanMode, aplikacja używa funkcji Recoverable GWP-ASan.
Funkcja Recoverable GWP-ASan różni się od podstawowej funkcji GWP-ASan w tych aspektach:
- Funkcja Recoverable GWP-ASan jest włączana tylko w przypadku około 1% uruchomień aplikacji, a nie przy każdym uruchomieniu.
- Gdy zostanie wykryty błąd odwołania do pamięci po jej zwolnieniu (heap-use-after-free) lub przepełnienia bufora na stercie (heap-buffer-overflow), pojawi się on w raporcie o awariach (tombstone). Ten raport o awariach jest dostępny
za pomocą
ActivityManager#getHistoricalProcessExitReasonsAPI, tak samo jak w przypadku oryginalnej funkcji GWP-ASan. - Zamiast kończyć działanie po wygenerowaniu raportu o awariach, funkcja Recoverable GWP-ASan pozwala na wystąpienie uszkodzenia pamięci, a aplikacja kontynuuje działanie. Chociaż proces może przebiegać normalnie, zachowanie aplikacji nie jest już określone. Z powodu uszkodzenia pamięci aplikacja może się w przyszłości zawiesić w dowolnym momencie lub może działać dalej bez widocznego dla użytkownika wpływu.
- Funkcja Recoverable GWP-ASan jest wyłączana po wygenerowaniu raportu o awariach. Dlatego aplikacja może otrzymać tylko 1 raport Recoverable GWP-ASan na uruchomienie.
- Jeśli w aplikacji jest zainstalowany niestandardowy program obsługi sygnałów, nigdy nie jest on wywoływany w przypadku sygnału SIGSEGV, który wskazuje na błąd Recoverable GWP-ASan.
Ponieważ awarie Recoverable GWP-ASan wskazują na rzeczywiste przypadki uszkodzenia pamięci na urządzeniach użytkowników, zdecydowanie zalecamy, aby priorytetowo analizować i naprawiać błędy wykryte przez tę funkcję.
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 alokacji/dealokacji
Jeśli diagnozujesz natywną awarię, w której brakuje ramek alokacji/dealokacji, prawdopodobnie w Twojej aplikacji nie ma wskaźników ramek. GWP-ASan używa wskaźników ramek do rejestrowania śladów alokacji i dealokacji ze względu na wydajność. Jeśli nie są one obecne, nie może rozwinąć zrzutu stosu.
Wskaźniki ramek są domyślnie włączone na urządzeniach z architekturą arm64, a wyłączone na urządzeniach z architekturą arm32. Ponieważ aplikacje nie mają kontroli nad biblioteką libc, GWP-ASan (na ogół) nie może zbierać śladów alokacji/dealokacji w przypadku plików wykonywalnych lub aplikacji 32-bitowych. Aplikacje 64-bitowe powinny być kompilowane bez flagi -fomit-frame-pointer, aby GWP-ASan mógł zbierać zrzuty stosu alokacji i dealokacji.
Odtwarzanie naruszeń bezpieczeństwa
GWP-ASan został zaprojektowany tak, aby wykrywać naruszenia bezpieczeństwa pamięci sterty na urządzeniach użytkowników. GWP-ASan podaje jak najwięcej informacji o awarii (ślad dostępu do naruszenia, ciąg przyczyny oraz ślady alokacji i dealokacji), ale nadal może być trudno ustalić, jak doszło do naruszenia. Niestety, ponieważ wykrywanie błędów jest probabilistyczne, 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 na stosie, stercie i w zmiennych globalnych. Uruchomienie aplikacji za pomocą HWASan może niezawodnie odtworzyć ten sam wynik, który jest zgłaszany przez GWP-ASan.
Jeśli uruchomienie aplikacji za pomocą HWASan nie wystarczy do ustalenia przyczyny błędu, spróbuj przeprowadzić fuzzing kodu którego dotyczy problem. Możesz kierować fuzzing na podstawie informacji w raporcie GWP-ASan, który może niezawodnie wykrywać i ujawniać podstawowe problemy z kondycją kodu.
Przykład
Ten przykładowy kod natywny zawiera błąd odwołania do pamięci po jej zwolnieniu (heap-use-after-free):
#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));
}
Podczas testu z użyciem powyższego przykładowego kodu GWP-ASan wykrył nieprawidłowe użycie i wywołał raport o awariach poniżej. GWP-ASan automatycznie wzbogacił raport, podając informacje o typie awarii, metadanych alokacji oraz powiązanych zrzutach 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 natywnych raportach o awariach w Androidzie znajdziesz w artykule Diagnozowanie natywnych awarii.