GWP-ASan היא תכונה של הקצאת זיכרון נייטיב שעוזרת למצוא באגים מסוג use-after-free ו-heap-buffer-overflow. השם הלא רשמי שלו הוא ראשי תיבות רקורסיביים, GWP-ASan Will Provide Allocation SANity. בניגוד ל-HWASan או ל-Malloc Debug, GWP-ASan לא דורש מקור או קומפילציה מחדש (כלומר, הוא פועל עם קבצים מוכנים מראש), והוא פועל בתהליכים של 32 ביט ו-64 ביט (אבל לקריסות של 32 ביט יש פחות מידע לניפוי באגים). במאמר הזה מוסבר מה צריך לעשות כדי להפעיל את התכונה הזו באפליקציה. GWP-ASan זמין באפליקציות שמטרגטות Android 11 (רמת API 30) ומעלה.
סקירה כללית
GWP-ASan מופעל בחלק מאפליקציות המערכת ובקובצי ההפעלה של הפלטפורמה שנבחרו באופן אקראי, כשהתהליך מתחיל (או כשהזיגוט מתפצל). הפעלת GWP-ASan באפליקציה שלכם יכולה לעזור לכם למצוא באגים שקשורים לזיכרון, ולהכין את האפליקציה לתמיכה ב-ARM Memory Tagging Extension (MTE). מנגנוני הדגימה של הקצאת המשאבים מספקים גם מהימנות מפני שאילתות של קריסת מערכת.
אחרי שמפעילים את GWP-ASan, הוא מיירט תת-קבוצה שנבחרה באופן אקראי של הקצאות ערימה, ומציב אותן באזור מיוחד שמזהה באגים של השחתת זיכרון בערימה שקשה לזהות. אם יש מספיק משתמשים, גם שיעור הדגימה הנמוך הזה ימצא באגים של בטיחות זיכרון בערימה שלא נמצאים באמצעות בדיקות רגילות. לדוגמה, GWP-ASan מצא מספר משמעותי של באגים בדפדפן Chrome (רבים מהם עדיין בתצוגה מוגבלת).
GWP-ASan אוסף מידע נוסף על כל ההקצאות שהוא מיירט. המידע הזה זמין כש-GWP-ASan מזהה הפרה של בטיחות הזיכרון, והוא מופיע באופן אוטומטי בדוח הקריסה של הנייטיב, מה שיכול לעזור מאוד בניפוי באגים (ראו דוגמה).
הכלי GWP-ASan מתוכנן כך שלא ייווצר עומס משמעותי על ה-CPU. GWP-ASan מוסיף תקורה קטנה וקבועה של זיכרון RAM כשהוא מופעל. התקורה הזו נקבעת על ידי מערכת Android, והיא עומדת כרגע על כ-70 קיביבייט (KiB) לכל תהליך מושפע.
הפעלת האפשרות באפליקציה
אפשר להפעיל את GWP-ASan באפליקציות ברמת התהליך באמצעות התג android:gwpAsanMode
במניפסט האפליקציה. אלו האפשרויות הנתמכות:
מושבת תמיד (
android:gwpAsanMode="never"
): ההגדרה הזו משביתה לחלוטין את GWP-ASan באפליקציה, והיא ברירת המחדל לאפליקציות שאינן אפליקציות מערכת.ברירת מחדל (
android:gwpAsanMode="default"
או לא צוין): Android 13 (רמת API 33) ומטה – GWP-ASan מושבת. Android 14 (רמת API 34) ומעלה – Recoverable GWP-ASan מופעל.מופעל תמיד (
android:gwpAsanMode="always"
): ההגדרה הזו מפעילה את GWP-ASan באפליקציה, כולל:מערכת ההפעלה שומרת כמות קבועה של זיכרון RAM לפעולות של GWP-ASan, בערך ~70KiB לכל תהליך מושפע. (מפעילים את GWP-ASan אם האפליקציה לא רגישה במיוחד לעלייה בשימוש בזיכרון).
GWP-ASan מיירט תת-קבוצה שנבחרה באופן אקראי של הקצאות ערימה וממקם אותן באזור מיוחד שמזהה באופן מהימן הפרות של בטיחות הזיכרון.
כשמתרחשת הפרה של בטיחות הזיכרון באזור המיוחד, GWP-ASan מפסיק את התהליך.
GWP-ASan מספק מידע נוסף על התקלה בדוח הקריסה.
כדי להפעיל את GWP-ASan באופן גלובלי באפליקציה, מוסיפים את השורה הבאה לקובץ AndroidManifest.xml
:
<application android:gwpAsanMode="always"> ... </application>
בנוסף, אפשר להפעיל או להשבית את GWP-ASan באופן מפורש עבור תהליכי משנה ספציפיים של האפליקציה. אפשר לטרגט פעילויות ושירותים באמצעות תהליכים שהופעלו או הושבתו באופן מפורש ב-GWP-ASan. לדוגמה:
<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 שניתן לשחזור
ב-Android 14 (רמת API 34) ואילך יש תמיכה ב-GWP-ASan שניתן לשחזור, שעוזר למפתחים למצוא באגים מסוג heap-buffer-overflow ו-heap-use-after-free בסביבת ייצור בלי לפגוע בחוויית המשתמש. אם android:gwpAsanMode
לא מצוין ב-AndroidManifest.xml
, האפליקציה משתמשת ב-GWP-ASan שניתן לשחזור.
ההבדלים בין GWP-ASan שניתן לשחזור לבין GWP-ASan בסיסי:
- האפשרות GWP-ASan לשחזור מופעלת רק בכ-1% מהפעלות האפליקציות, ולא בכל הפעלה של אפליקציה.
- כשמזוהה באג מסוג heap-use-after-free או heap-buffer-overflow, הבאג הזה מופיע בדוח הקריסה (tombstone). דוח הקריסות הזה זמין דרך
ActivityManager#getHistoricalProcessExitReasons
API, כמו GWP-ASan המקורי. - במקום לצאת אחרי יצירת dump של דוח הקריסה, Recoverable GWP-ASan מאפשר השחתת זיכרון והאפליקציה ממשיכה לפעול. יכול להיות שהתהליך ימשיך כרגיל, אבל ההתנהגות של האפליקציה כבר לא מוגדרת. בגלל השחיתות בזיכרון, יכול להיות שהאפליקציה תקרוס בשלב כלשהו בעתיד, או שהיא תמשיך לפעול ללא השפעה גלויה למשתמש.
- אחרי שנוצר קובץ dump של דוח הקריסה, האפשרות 'שחזור GWP-ASan' מושבתת. לכן, אפליקציה יכולה לקבל רק דוח אחד של GWP-ASan שאפשר לשחזר לכל הפעלה של האפליקציה.
- אם מותקן באפליקציה handler מותאם אישית של אותות, הוא אף פעם לא נקרא עבור אות SIGSEGV שמצביע על תקלה ב-GWP-ASan שאפשר לתקן.
מכיוון שקריסות של GWP-ASan שניתנות לשחזור מצביעות על מקרים אמיתיים של פגיעה בזיכרון במכשירים של משתמשי קצה, מומלץ מאוד לתעדף את התיקון של באגים שזוהו על ידי GWP-ASan שניתן לשחזור.
תמיכת מפתחים
בקטעים האלה מפורטות בעיות שיכולות לקרות כשמשתמשים ב-GWP-ASan, ומוסבר איך לפתור אותן.
חסרים עקבות של הקצאה או ביטול הקצאה
אם אתם מאבחנים קריסה מקורית שנראה שחסרים בה פריימים של הקצאה או ביטול הקצאה, סביר להניח שחסרים באפליקציה שלכם מצביעי פריימים. GWP-ASan משתמש במצביעי מסגרת כדי לתעד הקצאות וביטולי הקצאות מסיבות שקשורות לביצועים, ולא יכול לבטל את ההרשמה של מעקב אחר מחסנית אם הם לא קיימים.
כברירת מחדל, מצביעי מסגרת מופעלים במכשירי arm64 ומושבתים במכשירי arm32. מכיוון שלאפליקציות אין שליטה ב-libc, בדרך כלל אי אפשר להשתמש ב-GWP-ASan כדי לאסוף נתוני מעקב של הקצאה או ביטול הקצאה של קובצי הפעלה או אפליקציות של 32 ביט. באפליקציות 64 ביט צריך לוודא שהן לא מבוססות על -fomit-frame-pointer
כדי ש-GWP-ASan יוכל לאסוף עקבות מחסנית של הקצאה וביטול הקצאה.
שחזור של הפרות של כללי הבטיחות
GWP-ASan נועד לזהות הפרות של בטיחות זיכרון הערימה במכשירי משתמשים. GWP-ASan מספק כמה שיותר הקשר לגבי הקריסה (גישה למעקב אחר ההפרה, מחרוזת הסיבה ומעקב אחר הקצאה/ביטול הקצאה), אבל יכול להיות שעדיין יהיה קשה להסיק איך ההפרה התרחשה. לצערנו, מכיוון שזיהוי הבאגים הוא הסתברותי, לעיתים קרובות קשה לשחזר את הדוחות של GWP-ASan במכשיר מקומי.
במקרים כאלה, אם הבאג משפיע על מכשירי 64 ביט, צריך להשתמש ב-HWAddressSanitizer (HWASan). HWASan מזהה באופן מהימן הפרות של בטיחות הזיכרון במחסנית, בערימה ובמשתנים גלובליים. הפעלת האפליקציה עם HWASan עשויה לשחזר באופן מהימן את אותה תוצאה שמדווחת על ידי GWP-ASan.
במקרים שבהם הפעלת האפליקציה באמצעות HWASan לא מספיקה כדי להגיע לשורש הבעיה של באג, כדאי לנסות לבדוק את הקוד המדובר. אפשר למקד את מאמצי הפאזינג על סמך מידע בדוח GWP-ASan, שמאפשר לזהות ולחשוף באופן מהימן בעיות בסיסיות בבריאות הקוד.
דוגמה
בקוד Native הזה יש באג של שימוש בזיכרון לאחר שחרורו:
#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));
}
בהרצת בדיקה באמצעות קוד הדוגמה שלמעלה, GWP-ASan זיהה בהצלחה את השימוש הלא חוקי והפעיל את דוח הקריסה שמופיע בהמשך. GWP-ASan שיפר את הדוח באופן אוטומטי על ידי מתן מידע על סוג הקריסה, מטא-נתונים של הקצאה ונתוני מעקב אחר הקצאה וביטול הקצאה שמשויכים לקריסה.
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
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)
...
מידע נוסף
מידע נוסף על פרטי ההטמעה של GWP-ASan זמין בתיעוד של LLVM. מידע נוסף על דוחות קריסה מקוריים של Android זמין במאמר אבחון קריסות מקוריות.