GWP-ASan היא תכונה של מנהל זיכרון נייטיב שעוזרת למצוא באגים מסוג use-after-free ו-heap-buffer-overflow. השם הלא רשמי שלו הוא ראשי תיבות רפלקסיביים: GWP-ASan Will Provide Allocation SANity". בניגוד ל-HWASan או ל-Malloc Debug, GWP-ASan לא דורש קובץ מקור או הידור מחדש (כלומר, הוא פועל עם קובצי build מוכנים מראש), והוא פועל גם בתהליכים של 32 ביט וגם בתהליכים של 64 ביט (אבל בקריסות של 32 ביט כוללות פחות מידע על ניפוי באגים). בנושא הזה מוסבר מה צריך לעשות כדי להפעיל את התכונה הזו באפליקציה. GWP-ASan זמין באפליקציות שמטרגטות את Android 11 (רמת API 30) ואילך.
סקירה כללית
GWP-ASan מופעל באפליקציות מערכת ובקובצי הפעלה של פלטפורמות שנבחרו באופן אקראי בזמן הפעלת התהליך (או כשה-zygote מתפצל). הפעלת GWP-ASan באפליקציה שלכם תעזור לכם למצוא באגים שקשורים לזיכרון, ותאפשר לכם להכין את האפליקציה לתמיכה ב-ARM Memory Tagging Extension (MTE). מנגנוני הדגימה של ההקצאה מספקים גם מהימנות מפני שאילתות של מוות.
אחרי ההפעלה, GWP-ASan מיירט תת-קבוצה שנבחרה באופן אקראי של הקצאות אשפה ומציב אותן באזור מיוחד שמאתר באגים קשים לזיהוי של פגיעה בזיכרון האשפה. אם יש מספיק משתמשים, גם שיעור הדגימה הנמוך הזה יגלה באגים באבטחת זיכרון האשפה שלא מתגלים בבדיקות רגילות. לדוגמה, GWP-ASan זיהה מספר משמעותי של באגים בדפדפן Chrome (רבים מהם עדיין גלויים רק למפתחים).
GWP-ASan אוסף מידע נוסף על כל ההקצאות שהוא מיירט. המידע הזה זמין כש-GWP-ASan מזהה הפרה של בטיחות הזיכרון, והוא מתווסף באופן אוטומטי לדוח הקריסה המקורי. המידע הזה יכול לעזור מאוד בניפוי הבאגים (ראו דוגמה).
GWP-ASan תוכנן כך שלא יגרום לעומס משמעותי על המעבד. כשמפעילים את 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, בערך 70KB לכל תהליך מושפע. (מפעילים את 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
, האפליקציה משתמשת ב-Recoverable GWP-ASan.
ההבדלים בין GWP-ASan שניתן לשחזור לבין GWP-ASan הבסיסי הם:
- התכונה GWP-ASan לשחזור מופעל רק ב-1% מהפעלות האפליקציה, ולא בכל הפעלה.
- כשמזוהה באג מסוג heap-use-after-free או heap-buffer-overflow, הבאג הזה מופיע בדוח הקריסה (tombstone). דוח הקריסה הזה זמין דרך ה-API של
ActivityManager#getHistoricalProcessExitReasons
, כמו GWP-ASan המקורי. - במקום לצאת אחרי שמורידים את דוח הקריסה, GWP-ASan הניתן לשחזור מאפשר לזיכרון להתקלקל והאפליקציה ממשיכה לפעול. התהליך עשוי להמשיך כרגיל, אבל ההתנהגות של האפליקציה כבר לא מוגדרת. כתוצאה מהפגיעה בזיכרון, האפליקציה עשויה לקרוס בשלב כלשהו בעתיד, או להמשיך לפעול ללא השפעה גלויה למשתמשים.
- GWP-ASan לשחזור מושבת אחרי שהדוח על הקריסה מוטמע. לכן, אפשר לקבל רק דוח GWP-ASan אחד לצורך שחזור לכל השקה של אפליקציה.
- אם מותקן באפליקציה בורר אותות מותאם אישית, הוא אף פעם לא נקרא לטיפול באות SIGSEGV שמציין שגיאה שניתנת לתיקון ב-GWP-ASan.
מכיוון שקריסות של Recoverable GWP-ASan הן אינדיקציה למקרים אמיתיים של פגיעה בזיכרון במכשירי משתמשי הקצה, מומלץ מאוד לתעדף ולתקן באגים שזוהו על ידי Recoverable GWP-ASan בעדיפות גבוהה.
תמיכת מפתחים
בקטעים האלה מתוארות בעיות שעשויות להתרחש במהלך השימוש ב-GWP-ASan, וגם דרכים לטיפול בהן.
חסרים נתוני מעקב של הקצאה/ביטול הקצאה
אם אתם מאבחנים קריסה מקורית שנראה שאין בה מסגרות הקצאה/ביטול הקצאה, סביר להניח שאין באפליקציה מצביע מסגרות. GWP-ASan משתמש ב-frame pointers כדי לתעד את הטרייסים של ההקצאה והביטול של ההקצאה מסיבות של ביצועים, והוא לא יכול לבטל את הטרייסים של סטאק אם הם לא נמצאים.
כברירת מחדל, מצב frame pointers מופעל במכשירי arm64 ומושבת במכשירי arm32. לאפליקציות אין שליטה על libc, ולכן (באופן כללי) לא ניתן ל-GWP-ASan לאסוף עקבות הקצאה/ביטול הקצאה של קובצי הפעלה או אפליקציות של 32 ביט. באפליקציות של 64 ביט צריך לוודא שהן לא נוצרו באמצעות -fomit-frame-pointer
, כדי ש-GWP-ASan יוכל לאסוף מעקבים אחרי סטאקים של הקצאה וביטול הקצאה.
הדמיה של הפרות בטיחות
GWP-ASan נועד לזהות הפרות של בטיחות זיכרון אשכול במכשירי המשתמשים. GWP-ASan מספק כמה שיותר הקשר לגבי ההתרסקות (מעקב גישה של ההפרה, מחרוזת הסיבה ומעקב הקצאה/ביטול הקצאה), אבל עדיין יכול להיות שיהיה קשה להסיק איך ההפרה התרחשה. לצערנו, מכיוון שהזיהוי של הבאגים הוא מבוסס-סבירות, לרוב קשה לשחזר דוחות של GWP-ASan במכשיר מקומי.
במקרים כאלה, אם הבאג משפיע על מכשירים עם 64 ביט, צריך להשתמש ב-HWAddressSanitizer (HWASan). HWASan מזהה באופן מהימן הפרות של בטיחות הזיכרון ב-stack, ב-heap וב-globals. הפעלת האפליקציה באמצעות GWP-ASan עשויה לשחזר בצורה מהימנה את אותה תוצאה שמדווחת על ידי GWP-ASan.
במקרים שבהם הפעלת האפליקציה באמצעות HWASan לא מספיקה כדי למצוא את שורש הבעיה, כדאי לנסות לערער את הקוד הנדון. אתם יכולים לטרגט את המאמצים שלכם לזיהוי באגים על סמך המידע בדוח GWP-ASan, שיכול לזהות ולחשוף באופן מהימן בעיות בקוד.
דוגמה
בקוד הנייטיב לדוגמה הזה יש באג של שימוש ב-heap אחרי שחרור:
#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, ראו אבחון קריסות ברמת שפת המכונה.