GWP-ASan

GWP-ASan היא תכונה של הקצאת זיכרון נייטיב שעוזרת למצוא שימוש אחרי שימוש בחינם וגם heap-buffer-overflow באגים. השם הלא רשמי הוא ראשי תיבות רקורסיביים,"GWP-ASan Wil פקודת Aמיקום SANity". ביטול הלייק HWASan או Malloc Debug, ל-GWP-ASan אין צורך במקור או באיסוף מחדש (כלומר, פועלים עם הוא מובנה מראש), ופועל גם על תהליכי 32-64 ביט (למרות שקריסות של 32 ביט) כוללים פחות מידע על תוצאות ניפוי הבאגים). כאן נפרט את הפעולות שצריך לבצע כדי להפעיל את התכונה הזו אפליקציה. GWP-ASan זמין באפליקציות שמטרגטות את Android 11 (רמת API 30) ואילך.

סקירה כללית

GWP-ASan מופעל בחלק מאפליקציות המערכת והפלטפורמות שנבחרו באופן אקראי קובצי הפעלה במהלך אתחול התהליך (או במהלך מזלגות הזיגוטה). הפעלת GWP-ASan ב- כדי לעזור לכם לאתר באגים הקשורים לזיכרון ולהכין את האפליקציה תמיכה בתוסף תיוג הזיכרון (MTE). מנגנוני הדגימה של ההקצאה מספקים גם הם אמינות ביחס שאילתות המוות.

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

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

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

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

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

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

  1. GWP-ASan ניתן לשחזור מופעל רק בכ-1% מהפעלות האפליקציה, ולא כל השקה של אפליקציה.
  2. כשהמערכת מזהה באג שעושה שימוש בערימה אחרי שימוש חופשי או גלישת נתונים במאגר הנתונים הזמני, הבאג הזה מופיעה בדוח הקריסה (מצבה). דוח הקריסה הזה זמין באמצעות ActivityManager#getHistoricalProcessExitReasons API זהה ל-GWP-ASan המקורי.
  3. במקום לצאת לאחר הסרת דוח הקריסה, ניתן לשחזר את GWP-ASan מאפשרת פגיעה בזיכרון, והאפליקציה ממשיכה לפעול. התהליך עשוי להימשך כרגיל, אבל ההתנהגות של האפליקציה לא חלה יותר שצוין. עקב פגיעה בזיכרון, האפליקציה עלולה לקרוס באופן שרירותי באופן מסוים בעתיד, או שהוא ימשיך ללא השפעה נראית לעין.
  4. GWP-ASan הניתן לשחזור מושבת לאחר יצירת דוח הקריסה. לכן, אפליקציה יכולה לקבל רק דוח GWP-ASan אחד שניתן לשחזור לכל השקה של אפליקציה.
  5. אם ה-handler של האותות בהתאמה אישית מותקן באפליקציה, הוא אף פעם לא מופעל אות SIGSEGV שמצביע על תקלה של GWP-ASan שניתן לשחזר.

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

תמיכת מפתחים

בקטעים האלה מתוארות הבעיות שעלולות להתרחש במהלך השימוש ב-GWP-ASan ואיך להתייחס אליהן.

חסרים נתוני מעקב אחר הקצאה או הקצאה

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

זיהויי פריימים מופעלים כברירת מחדל במכשירי Arm64 ומושבתים כברירת מחדל בזרוע 32 מכשירים. בגלל שלאפליקציות אין שליטה על libc, זה (באופן כללי) ל-GWP-ASan אין אפשרות לאסוף נתוני מעקב אחר הקצאה/הקצאה עבור 32 סיביות בקובצי הפעלה או באפליקציות. אפליקציות של 64 ביט צריכות להבטיח שהן לא נוצר באמצעות -fomit-frame-pointer כדי ש-GWP-ASan יוכל לאסוף דוחות קריסות של מיקומים בעסק.

שחזור של הפרות בטיחות

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

במקרים כאלה, אם הבאג משפיע על מכשירים של 64 ביט, צריך להשתמש HWAddressSanitizer (HWASan). HWASan מזהה את בטיחות הזיכרון ב-Stack, בערימה וב-globals. מריץ את האפליקציה עם HWASan עשוי לשחזר באופן מהימן את אותה התוצאה המדווחת GWP-ASan.

במקרים שבהם הפעלת האפליקציה באמצעות HWASan לא מספיקה כדי גורם לבאג, כדאי לנסות fuzz את הקוד שבהן מדובר. תוכלו להתמקד במאמצים המעורפלים שלכם על סמך מידע דוח GWP-ASan, שיכול לזהות ולחשוף באופן מהימן את תקינות הקוד הבסיסית או בעיות.

דוגמה

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

מידע נוסף

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