GWP-ASan

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 באפליקציה שלכם כדי לאתר באגים הקשורים לזיכרון, וכדי להכין את האפליקציה לתמיכה בתוסף תיוג הזיכרון (MTE) ל-ARM. מנגנוני הדגימה של ההקצאה גם מספקים מהימנות מפני שאילתות של מוות.

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

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

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

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