AddressSanitizer (ASan) הוא כלי מהיר שמבוסס על קומפילר, שנועד לזהות באגים בזיכרון בקוד Native.
ASan מזהה:
- גלישה או חוסר גלישה של מאגרים בערימה ובמחסנית
- שימוש בערימה אחרי שחרור
- שימוש ב-Stack מחוץ להיקף
- Double free/wild free
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 rootadb 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 rootadb shell setenforce 0 # disable SELinuxadb shell setprop wrap.com.google.android.gm "asanwrapper"
בהקשר הזה, asanwrapper נכתב מחדש /system/bin/app_process ל-/system/bin/asan/app_process, שנבנה באמצעות ASan. בנוסף, הוא מוסיף /system/lib/asan בתחילת נתיב החיפוש של הספרייה הדינמית. כך, ספריות עם ASan מ-/system/lib/asan מקבלות עדיפות על פני ספריות רגילות ב-/system/lib כשמריצים עם asanwrapper.
אם נמצא באג, האפליקציה קורסת והדוח מודפס ביומן.
SANITIZE_TARGET
Android מגרסה 7.0 ואילך כולל תמיכה בבנייה של כל פלטפורמת Android באמצעות ASan בבת אחת. (אם אתם יוצרים גרסת הפצה שגבוהה מ-Android 9, כדאי לבחור ב-HWASan).
מריצים את הפקודות הבאות באותו עץ build.
make -j42SANITIZE_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 מסוים מהספריות הסטטיות שהם תלויים בהן.