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 נועד לא ליצור תקורה משמעותית של המעבד (CPU). כשמפעילים את GWP-ASan, הוא צורך נפח קטן וקבוע של זיכרון RAM. התקורה הזו נקבעת על ידי מערכת Android, וכרגע היא כ-70KiB (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-free בשלב ייצור בלי לפגוע בחוויית המשתמש. אם לא צוין android:gwpAsanMode
ב-AndroidManifest.xml
, האפליקציה משתמשת ב-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 כדי לתעד את הטרייסים של ההקצאה והביטול של ההקצאה מסיבות של ביצועים, והוא לא יכול לבצע ביטול של הטרייסים ב-stack אם הם לא נמצאים.
זיהויי הפריים מופעלים כברירת מחדל במכשירי Arm64 ומושבתים כברירת מחדל במכשירי זרוע 32. לאפליקציות אין שליטה על 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, ראו אבחון קריסות מקוריות.