GWP-ASan

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 מופעל באפליקציות מערכת ובקובצי הפעלה של פלטפורמות שנבחרו באופן אקראי בזמן הפעלת התהליך (או כשה-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 באפליקציה, כולל:

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

    2. GWP-ASan מיירט תת-קבוצה שנבחרה באופן אקראי של הקצאות אשכול ומציב אותן באזור מיוחד שמזהה באופן מהימן הפרות של בטיחות הזיכרון.

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

מכיוון שקריסות של Recoverable GWP-ASan מצביעות על מקרים אמיתיים של פגיעה בזיכרון במכשירי משתמשי הקצה, מומלץ מאוד לתעדף ולתקן באגים שזוהו על ידי Recoverable GWP-ASan בעדיפות גבוהה.

תמיכת מפתחים

בקטעים האלה מתוארות בעיות שעשויות להתרחש במהלך השימוש ב-GWP-ASan, וגם דרכים לטיפול בהן.

חסרים עקבות של הקצאה/ביטול הקצאה

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