GWP-ASan

‫GWP-ASan הוא תכונה של מקצה זיכרון נייטיב שעוזרת למצוא באגים מסוג use-after-free ו-heap-buffer-overflow. השם הלא רשמי שלו הוא ראשי תיבות רקורסיביים,"GWP-ASan Will Provide Allocation SANity". בניגוד ל-HWASan או לניפוי באגים בשיטת Malloc, ‏GWP-ASan לא דורש מקור או קומפילציה מחדש (כלומר, פועל עם קובצי prebuilt), ופועל גם בתהליכים של 32 ביט וגם בתהליכים של 64 ביט (אם כי קריסות של 32 ביט כוללות פחות מידע על תוצאות ניפוי הבאגים). בנושא הזה מתוארות הפעולות שצריך לבצע כדי להפעיל את התכונה הזו באפליקציה. GWP-ASan זמין באפליקציות שמטרגטות את Android 11 (רמת API 30) ומעלה.

סקירה כללית

‫GWP-ASan מופעל באפליקציות מערכת ובקובצי הפעלה של פלטפורמות שנבחרו באופן אקראי, בזמן הפעלת התהליך (או כשמתבצע פיצול של ה-zygote). כדאי להפעיל את GWP-ASan באפליקציה שלכם כדי למצוא באגים שקשורים לזיכרון, ולהכין את האפליקציה לתמיכה ב-ARM Memory Tagging Extension‏ (MTE). מנגנוני הדגימה של ההקצאה מספקים גם אמינות מפני שאילתות קטלניות.

אחרי שמפעילים את GWP-ASan, הוא מיירט קבוצת משנה של הקצאות זיכרון בערימה (heap allocation) שנבחרה באופן אקראי, וממקם אותן באזור מיוחד שמזהה באגים שקשה לזהות של פגיעה בזיכרון בערימה. אם יש מספיק משתמשים, אפילו תדירות הדגימה הנמוכה הזו תמצא באגים בבטיחות הזיכרון של הערימה שלא נמצאים בבדיקות רגילות. לדוגמה, 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) ומעלה – GWP-ASan שניתן לשחזור מופעל.

  • ‫Always enabled (android:gwpAsanMode="always"): ההגדרה הזו מפעילה את GWP-ASan באפליקציה, וכוללת את האפשרויות הבאות:

    1. מערכת ההפעלה שומרת כמות קבועה של זיכרון RAM לפעולות של GWP-ASan, בערך 70KiB לכל תהליך שמושפע. (אם האפליקציה לא רגישה באופן קריטי לעלייה בשימוש בזיכרון, כדאי להפעיל את GWP-ASan).

    2. ‫GWP-ASan מיירט קבוצת משנה שנבחרה באופן אקראי של הקצאות זיכרון בערימה (heap allocation) וממקם אותן באזור מיוחד שמזהה באופן מהימן הפרות של בטיחות הזיכרון.

    3. כשמתרחשת הפרה של בטיחות הזיכרון באזור המיוחד, GWP-ASan מסיים את התהליך.

    4. ‫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) ואילך יש תמיכה ב-Recoverable GWP-ASan, שעוזר למפתחים למצוא באגים מסוג heap-buffer-overflow ו-heap-use-after-free בסביבת הייצור בלי לפגוע בחוויית המשתמש. אם לא מציינים את android:gwpAsanMode ב-AndroidManifest.xml, האפליקציה משתמשת ב-GWP-ASan שניתן לשחזור.

ההבדלים בין GWP-ASan שניתן לשחזור לבין GWP-ASan הבסיסי:

  1. ‫GWP-ASan שניתן לשחזור מופעל רק בכ-1% מההפעלות של האפליקציה, ולא בכל הפעלה.
  2. כשמזוהה באג מסוג heap-use-after-free או heap-buffer-overflow, הבאג הזה מופיע בדוח הקריסה (tombstone). דוח הקריסה הזה זמין דרך ה-API של ActivityManager#getHistoricalProcessExitReasons, כמו GWP-ASan המקורי.
  3. במקום לצאת אחרי השלכת דוח הקריסה, GWP-ASan שניתן לשחזור מאפשר לפגיעה בזיכרון להתרחש, והאפליקציה ממשיכה לפעול. יכול להיות שהתהליך ימשיך כרגיל, אבל ההתנהגות של האפליקציה כבר לא מוגדרת. בגלל פגיעה בזיכרון, יכול להיות שהאפליקציה תקרוס בשלב כלשהו בעתיד, או שהיא תמשיך לפעול בלי שהמשתמשים יבחינו בבעיה.
  4. התכונה "GWP-ASan שניתן לשחזר" מושבתת אחרי שדוח הקריסה נמחק. לכן, אפליקציה יכולה לקבל רק דוח אחד של GWP-ASan שניתן לשחזור בכל הפעלה של האפליקציה.
  5. אם באפליקציה מותקן handler של אותות בהתאמה אישית, הוא אף פעם לא נקרא לאות SIGSEGV שמעיד על תקלה ב-Recoverable 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 לא מספיקה כדי לזהות את שורש הבעיה, כדאי לנסות להריץ בדיקת פאזינג על הקוד הרלוונטי. אתם יכולים למקד את מאמצי ה-fuzzing על סמך המידע בדוח GWP-ASan, שיכול לזהות ולחשוף באופן מהימן בעיות בסיסיות בתקינות הקוד.

דוגמה

בקוד ה-Native הזה לדוגמה יש באג של שימוש בזיכרון אחרי שחרור (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));
}

בהרצת בדיקה באמצעות הקוד לדוגמה שלמעלה, 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)
      ...

מידע נוסף

במסמכי התיעוד של LLVM מוסבר בהרחבה על פרטי ההטמעה של GWP-ASan. מידע נוסף על דוחות קריסה ברמת שפת המכונה ב-Android זמין במאמר אבחון קריסות ברמת שפת המכונה.