מכשיר לחיטוי כתובות

‫Android NDK תומך ב-Address Sanitizer (שנקרא גם ASan) החל מרמת API‏ 27 (Android O MR 1).

‫ASan הוא כלי מהיר שמבוסס על קומפילציה, שמאפשר לזהות באגים בזיכרון בקוד Native. ‫ASan מזהה:

  • גלישה או חריגה ממאגרים (buffer) בזיכרון המחסנית (stack) ובזיכרון הערימה (heap)
  • שימוש בערימה אחרי שחרור
  • שימוש במערך מחוץ להיקף
  • שחרור כפול של זיכרון/שחרור זיכרון לא מנוהל

תקורת ה-CPU של ASan היא בערך פי 2, תקורת גודל הקוד היא בין 50% לבין פי 2, ותקורת הזיכרון גדולה (תלויה בדפוסי ההקצאה, אבל בסדר גודל של פי 2).

אפליקציה לדוגמה

אפליקציה לדוגמה מראה איך להגדיר וריאנט build עבור asan.

פיתוח פתרונות

כדי ליצור את הקוד המקורי (JNI) של האפליקציה באמצעות Address Sanitizer, צריך לבצע את הפעולות הבאות:

ndk-build

ב-Application.mk:

APP_STL := c++_shared # Or system, or none.
APP_CFLAGS := -fsanitize=address -fno-omit-frame-pointer
APP_LDFLAGS := -fsanitize=address

לכל מודול בקובץ Android.mk:

LOCAL_ARM_MODE := arm

CMake

בקובץ build.gradle של המודול:

android {
    defaultConfig {
        externalNativeBuild {
            cmake {
                // Can also use system or none as ANDROID_STL.
                arguments "-DANDROID_ARM_MODE=arm", "-DANDROID_STL=c++_shared"
            }
        }
    }
}

לכל יעד בקובץ CMakeLists.txt:

target_compile_options(${TARGET} PUBLIC -fsanitize=address -fno-omit-frame-pointer)
set_target_properties(${TARGET} PROPERTIES LINK_FLAGS -fsanitize=address)

ריצה

החל מ-Android O MR1 (רמת API‏ 27), אפליקציה יכולה לספק סקריפט מעטפת שיכול לעטוף או להחליף את תהליך האפליקציה. ההרשאה הזו מאפשרת לאפליקציה שניתן לבצע בה ניפוי באגים להתאים אישית את הפעלת האפליקציה, וכך להשתמש ב-ASan במכשירי ייצור.

  1. מוסיפים android:debuggable למניפסט של האפליקציה.
  2. מגדירים את useLegacyPackaging לערך true בקובץ build.gradle של האפליקציה. מידע נוסף זמין במדריך בנושא סקריפט מעטפת.
  3. מוסיפים את ספריית זמן הריצה של ASan ל-jniLibs של מודול האפליקציה.
  4. מוסיפים קובצי wrap.sh עם התוכן הבא לכל ספרייה בספרייה src/main/resources/lib.

    #!/system/bin/sh
    HERE="$(cd "$(dirname "$0")" && pwd)"
    export ASAN_OPTIONS=log_to_syslog=false,allow_user_segv_handler=1
    ASAN_LIB=$(ls $HERE/libclang_rt.asan-*-android.so)
    if [ -f "$HERE/libc++_shared.so" ]; then
        # Workaround for https://github.com/android-ndk/ndk/issues/988.
        export LD_PRELOAD="$ASAN_LIB $HERE/libc++_shared.so"
    else
        export LD_PRELOAD="$ASAN_LIB"
    fi
    "$@"
    

אם מודול האפליקציה של הפרויקט נקרא app, מבנה התיקיות הסופי צריך לכלול את הרכיבים הבאים:

<project root>
└── app
    └── src
        └── main
            ├── jniLibs
            │   ├── arm64-v8a
            │   │   └── libclang_rt.asan-aarch64-android.so
            │   ├── armeabi-v7a
            │   │   └── libclang_rt.asan-arm-android.so
            │   ├── x86
            │   │   └── libclang_rt.asan-i686-android.so
            │   └── x86_64
            │       └── libclang_rt.asan-x86_64-android.so
            └── resources
                └── lib
                    ├── arm64-v8a
                    │   └── wrap.sh
                    ├── armeabi-v7a
                    │   └── wrap.sh
                    ├── x86
                    │   └── wrap.sh
                    └── x86_64
                        └── wrap.sh

מעקבי ערימה

Address Sanitizer צריך לבטל את הפעולה של המחסנית בכל קריאה של malloc/realloc/free ‎. יש שתי אפשרויות:

  1. מבטל את ההרשמה של פונקציית ה-unwinder המבוססת על מצביע פריים 'מהיר'. ההוראות האלה מפורטות בקטע על בנייה.

  2. מבטל את ההרשמה של CFI 'איטי'. במצב הזה, ASan משתמש ב-_Unwind_Backtrace. היא דורשת רק את -funwind-tables, שבדרך כלל מופעלת כברירת מחדל.

ה-unwinder המהיר הוא ברירת המחדל עבור malloc/realloc/free. האפשרות הזו מוגדרת כברירת מחדל עבור מעקב אחר מחסנית (stack trace) שגורם לשגיאה קריטית. אפשר להפעיל את האפשרות 'הילוך איטי של פתיחת הקובץ' לכל עקבות המחסנית על ידי הוספת fast_unwind_on_malloc=0 למשתנה ASAN_OPTIONS בקובץ wrap.sh.