AddressSanitizer

‫AddressSanitizer‏ (ASan) הוא כלי מהיר שמבוסס על קומפיילר ומיועד לזיהוי באגים בזיכרון בקוד מקורי.

‫ASan מזהה:

  • גלישה חוצה של מאגר (overflow) או גלישה חוצה של מאגר (underflow) בזיכרון המחסנית ובזיכרון הערימה
  • שימוש בערימה אחרי שחרור
  • שימוש ב-Stack מחוץ להיקף
  • שחרור כפול/שחרור פראי

‫ASan פועל ב-ARM בגרסאות 32 ביט ו-64 ביט, וגם ב-x86 וב-x86-64. תקורת ה-CPU של ASan גדולה פי 2 בערך, תקורת גודל הקוד היא בין 50% ל-2x, ותקורת הזיכרון גדולה (תלויה בדפוסי ההקצאה, אבל בסדר גודל של 2x).

‫Android 10 והענף האחרון של AOSP ב-AArch64 תומכים ב-Hardware-assisted AddressSanitizer (HWASan), כלי דומה עם תקורה נמוכה יותר של RAM וטווח גדול יותר של באגים שזוהו. ‫HWASan מזהה שימוש במחסנית אחרי החזרה, בנוסף לבאגים שמזוהים על ידי ASan.

ל-HWASan יש תקורה דומה של מעבד וגודל קוד, אבל תקורה קטנה בהרבה של RAM‏ (15%). ‫HWASan הוא לא דטרמיניסטי. יש רק 256 ערכים אפשריים לתג, ולכן יש סיכוי של 0.4% בלבד לפספס באג. ל-HWASan אין אזורים אדומים בגודל מוגבל כמו ל-ASan, כדי לזהות הצפות, ואין לו הסגר בקיבולת מוגבלת כדי לזהות שימוש אחרי שחרור, ולכן לא משנה ל-HWASan מה גודל ההצפה או לפני כמה זמן הזיכרון בוטל. היתרון הזה הופך את HWASan לטובה יותר מ-ASan. מידע נוסף על העיצוב של HWASan או על השימוש ב-HWASan ב-Android

‫ASan מזהה הצפות של סטאק/גלובליות בנוסף להצפות של ערימה, והוא מהיר עם תקורה מינימלית של זיכרון.

במסמך הזה מוסבר איך לבנות ולהפעיל חלקים מ-Android או את כל המערכת באמצעות ASan. אם אתם מפתחים אפליקציית SDK/NDK באמצעות ASan, כדאי לעיין במאמר בנושא Address Sanitizer.

ניקוי קובצי הפעלה ספציפיים באמצעות ASan

מוסיפים את LOCAL_SANITIZE:=address או את sanitize: { address: true } לכלל הבנייה של הקובץ הניתן להפעלה. אפשר לחפש בקוד דוגמאות קיימות או למצוא את אמצעי החיטוי האחרים שזמינים.

כשמזוהה באג, ASan מדפיס דוח מפורט גם לפלט הסטנדרטי וגם ל-logcat, ואז התהליך קורס.

ניקוי ספריות משותפות באמצעות ASan

בגלל האופן שבו ASan פועל, אפשר להשתמש בספרייה שנבנתה באמצעות ASan רק בקובץ הפעלה שנבנה באמצעות ASan.

כדי לבצע סניטציה בספרייה משותפת שמשמשת כמה קבצים הפעלה, שלא כולם נוצרו באמצעות ASan, צריך שני עותקים של הספרייה. הדרך המומלצת לעשות זאת היא להוסיף את הקוד הבא אל Android.mk עבור המודול הרלוונטי:

LOCAL_SANITIZE:=address
LOCAL_MODULE_RELATIVE_PATH := asan

הספרייה תופיע ב/system/lib/asan במקום ב/system/lib. לאחר מכן, מריצים את קובץ ההפעלה באמצעות הפקודה:

LD_LIBRARY_PATH=/system/lib/asan

בשביל שדימונים של המערכת יפעלו, צריך להוסיף את השורות הבאות לקטע המתאים בקובץ /init.rc או /init.$device$.rc.

setenv LD_LIBRARY_PATH /system/lib/asan

בודקים שהתהליך משתמש בספריות מ-/system/lib/asan אם הן קיימות, על ידי קריאת /proc/$PID/maps. אם לא, יכול להיות שתצטרכו להשבית את SELinux:

adb root
adb shell setenforce 0
# restart the process with adb shell kill $PID
# if it is a system service, or may be adb shell stop; adb shell start.

מעקבי ערימה משופרים

‫ASan משתמש ב-unwinder מהיר שמבוסס על מצביע מסגרת כדי לתעד stack trace לכל אירוע של הקצאת זיכרון וביטול הקצאת זיכרון בתוכנית. רוב מערכת Android בנויה בלי מצביעי מסגרת. כתוצאה מכך, לרוב מקבלים רק פריים אחד או שניים משמעותיים. כדי לפתור את הבעיה, אפשר לבנות מחדש את הספרייה באמצעות ASan (מומלץ!) או באמצעות:

LOCAL_CFLAGS:=-fno-omit-frame-pointer
LOCAL_ARM_MODE:=arm

אפשר גם להגדיר את ASAN_OPTIONS=fast_unwind_on_malloc=0 בסביבת התהליך. האחרון יכול להיות מאוד אינטנסיבי מבחינת צריכת משאבי המעבד (CPU), בהתאם לעומס.

סימבוליזציה

בתחילה, דוחות ASan מכילים הפניות להיסטים בקבצים בינאריים ובספריות משותפות. יש שתי דרכים לקבל מידע על קובץ המקור ועל השורה:

  • מוודאים שהקובץ הבינארי llvm-symbolizer נמצא ב-/system/bin. ‫llvm-symbolizer מבוסס על מקורות ב-third_party/llvm/tools/llvm-symbolizer.
  • מסננים את הדוח באמצעות external/compiler-rt/lib/asan/scripts/symbolize.py הסקריפט.

הגישה השנייה יכולה לספק יותר נתונים (כלומר, file:line מיקומים) בגלל הזמינות של ספריות עם סמלים במארח.

‫ASan באפליקציות

‫ASan לא יכול לראות את קוד Java, אבל הוא יכול לזהות באגים בספריות JNI. לשם כך, צריך ליצור את קובץ ההפעלה באמצעות ASan, שבמקרה הזה הוא /system/bin/app_process(32|64). הפעולה הזו מאפשרת להשתמש ב-ASan בכל האפליקציות במכשיר בו-זמנית, וזה עומס כבד, אבל מכשיר עם RAM בנפח 2GB אמור להיות מסוגל להתמודד עם זה.

מוסיפים את LOCAL_SANITIZE:=address לכלל הבנייה app_process ב-frameworks/base/cmds/app_process.

עורכים את הקטע service zygote בקובץ system/core/rootdir/init.zygote(32|64).rc המתאים כדי להוסיף את השורות הבאות לבלוק של השורות עם הכניסה שמכיל את class main, עם אותה כניסה:

    setenv LD_LIBRARY_PATH /system/lib/asan:/system/lib
    setenv ASAN_OPTIONS allow_user_segv_handler=true

‫Build,‏ adb sync,‏ fastboot flash boot ו-reboot.

שימוש במאפיין wrap

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

אפשר לעשות זאת על ידי הפעלת האפליקציה עם המאפיין wrap.. בדוגמה הבאה מריצים את אפליקציית Gmail באמצעות ASan:

adb root
adb shell setenforce 0  # disable SELinux
adb shell setprop wrap.com.google.android.gm "asanwrapper"

בהקשר הזה, asanwrapper משכתב את /system/bin/app_process ל-/system/bin/asan/app_process, שנבנה באמצעות ASan. בנוסף, הוא מוסיף /system/lib/asan בתחילת נתיב החיפוש של הספרייה הדינמית. כך, כשמריצים עם asanwrapper, ספריות עם ASan מ-/system/lib/asan מקבלות עדיפות על פני ספריות רגילות ב-/system/lib.

אם נמצא באג, האפליקציה קורסת והדוח מודפס ביומן.

SANITIZE_TARGET

‫Android 7.0 ואילך כולל תמיכה בבנייה של כל פלטפורמת Android באמצעות ASan בבת אחת. (אם אתם יוצרים גרסת הפצה שגבוהה מ-Android 9, כדאי יותר להשתמש ב-HWASan).

מריצים את הפקודות הבאות באותו עץ בנייה.

make -j42
SANITIZE_TARGET=address make -j42

במצב הזה, userdata.img מכיל ספריות נוספות וצריך להעביר אותו למכשיר גם כן. משתמשים בשורת הפקודה הבאה:

fastboot flash userdata && fastboot flashall

הפעולה הזו יוצרת שני סטים של ספריות משותפות: ספריות רגילות ב-/system/lib (הפעלת make הראשונה) וספריות עם ASan ב-/data/asan/lib (הפעלת make השנייה). קבצים הפעלה מהגרסה השנייה מחליפים את הקבצים מהגרסה הראשונה. קובצי הפעלה עם ASan מקבלים נתיב חיפוש שונה בספרייה שכולל את /data/asan/lib לפני /system/lib באמצעות /system/bin/linker_asan ב-PT_INTERP.

מערכת ה-build מוחקת ספריות של אובייקטים זמניים אם הערך של $SANITIZE_TARGET השתנה. הפעולה הזו מאלצת בנייה מחדש של כל יעדי הבנייה תוך שמירה על קובצי ההפעלה המותקנים ב-/system/lib.

אי אפשר ליצור חלק מהיעדים באמצעות ASan:

  • קבצים הפעלה שמקושרים באופן סטטי
  • LOCAL_CLANG:=false יעדים
  • LOCAL_SANITIZE:=false לא מוגדרות כ-ASan עבור SANITIZE_TARGET=address

קובצי הפעלה כאלה מדלגים על בניית SANITIZE_TARGET, והגרסה מהפעלת make הראשונה נשארת ב-/system/bin.

ספריות כאלה נוצרות ללא ASan. הם יכולים להכיל קוד ASan מסוים מהספריות הסטטיות שהם תלויים בהן.

מסמכים תומכים