תוסף תיוג זיכרון של Arm

ב-Arm v9 מוצגת התכונה Arm Memory Tagging Extension ‏ (MTE), שהיא הטמעה בחומרה של זיכרון מתויג.

ברמה גבוהה, התכונה MTE מתייגת כל הקצאת זיכרון או ביטול הקצאה של זיכרון במטא-נתונים נוספים. ‫MTE מקצה תג למיקום בזיכרון, שאחר כך אפשר לשייך לו מצביעים שמפנים למיקום הזה בזיכרון. בזמן הריצה, ה-CPU בודק שסמן המיקום ותגי המטא-נתונים תואמים בכל טעינה ובכל אחסון.

ב-Android 12, אפשר להוסיף מטא-נתונים לכל הקצאה של זיכרון בערימה של ליבת המערכת ושל מרחב המשתמש. השימוש ב-MTE עוזר לזהות באגים מסוג use-after-free (שימוש אחרי שחרור) ובאגים מסוג buffer-overflow (גלישת חוצץ), שהם המקור הנפוץ ביותר לבאגים של בטיחות זיכרון בבסיסי הקוד שלנו.

מצבי הפעולה של MTE

ל-MTE יש שלושה מצבי פעולה:

  • מצב סינכרוני (SYNC)
  • מצב אסינכרוני (ASYNC)
  • מצב אסימטרי (ASYMM)

מצב סינכרוני (SYNC)

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

מומלץ להשתמש במצב הזה במהלך הבדיקות כחלופה ל-HWASan/KASAN או בסביבת ייצור כשתהליך היעד מייצג משטח תקיפה פגיע. בנוסף, אם מצב ASYNC מציין שיש באג, אפשר להשתמש בממשקי ה-API של זמן הריצה כדי להעביר את הביצוע למצב SYNC ולקבל דוח באגים מדויק.

כשמפעילים את הקצאת הזיכרון ב-Android במצב SYNC, המערכת מתעדת את עקבות המחסנית של כל ההקצאות והביטולים של הקצאות, ומשתמשת בהם כדי לספק דוחות שגיאות טובים יותר שכוללים הסבר על שגיאת זיכרון, כמו use-after-free או buffer-overflow, ועקבות המחסנית של אירועי הזיכרון הרלוונטיים. דוחות כאלה מספקים מידע נוסף לפי הקשר, ומקלים על איתור באגים ותיקונם.

מצב אסינכרוני (ASYNC)

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

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

מצב אסימטרי (ASYMM)

תכונה נוספת ב-Arm v8.7-A, מצב MTE אסימטרי, מספקת בדיקה סינכרונית של קריאות זיכרון ובדיקה אסינכרונית של כתיבות זיכרון, עם ביצועים דומים לאלה של מצב ASYNC. ברוב המקרים, המצב הזה עדיף על מצב ASYNC, ואנחנו ממליצים להשתמש בו במקום ASYNC בכל פעם שהוא זמין.

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

MTE במרחב המשתמש

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

הפעלה של MTE באמצעות מערכת ה-build

‫MTE הוא מאפיין של כל התהליך, ולכן הוא נשלט על ידי ההגדרה של משך זמן של תהליך build של קובץ ההפעלה הראשי. האפשרויות הבאות מאפשרות לשנות את ההגדרה הזו עבור קובצי הפעלה ספציפיים, או עבור ספריות משנה שלמות בעץ המקור. המערכת מתעלמת מההגדרה בספריות או בכל יעד שהוא לא קובץ הפעלה ולא בדיקה.

1. הפעלת MTE ב-Android.bp (דוגמה), לפרויקט מסוים:

מצב MTE הגדרה
MTE אסינכרוני
  sanitize: {
  memtag_heap: true,
  }
MTE סינכרוני
  sanitize: {
  memtag_heap: true,
  diag: {
  memtag_heap: true,
  },
  }

או בAndroid.mk:

מצב MTE הגדרה
Asynchronous MTE LOCAL_SANITIZE := memtag_heap
Synchronous MTE LOCAL_SANITIZE := memtag_heap
LOCAL_SANITIZE_DIAG := memtag_heap

2. הפעלת MTE בספריית משנה בעץ המקור באמצעות משתנה מוצר:

מצב MTE רשימת המיקומים שנכללו רשימת החרגות
async PRODUCT_MEMTAG_HEAP_ASYNC_INCLUDE_PATHS MEMTAG_HEAP_ASYNC_INCLUDE_PATHS PRODUCT_MEMTAG_HEAP_EXCLUDE_PATHS MEMTAG_HEAP_EXCLUDE_PATHS
סנכרון PRODUCT_MEMTAG_HEAP_SYNC_INCLUDE_PATHS MEMTAG_HEAP_SYNC_INCLUDE_PATHS

או

מצב MTE הגדרה
MTE אסינכרוני MEMTAG_HEAP_ASYNC_INCLUDE_PATHS
MTE סינכרוני MEMTAG_HEAP_SYNC_INCLUDE_PATHS

או על ידי ציון נתיב ההחרגה של קובץ הפעלה:

מצב MTE הגדרה
MTE אסינכרוני PRODUCT_MEMTAG_HEAP_EXCLUDE_PATHS MEMTAG_HEAP_EXCLUDE_PATHS
MTE סינכרוני

דוגמה (שימוש דומה ל-PRODUCT_CFI_INCLUDE_PATHS)

  PRODUCT_MEMTAG_HEAP_SYNC_INCLUDE_PATHS=vendor/$(vendor)
  PRODUCT_MEMTAG_HEAP_EXCLUDE_PATHS=vendor/$(vendor)/projectA \
                                    vendor/$(vendor)/projectB

הפעלת MTE באמצעות מאפייני מערכת

אפשר לשנות את הגדרות הבנייה שלמעלה בזמן הריצה על ידי הגדרת מאפיין המערכת הבא:

arm64.memtag.process.<basename> = (off|sync|async)

כאשר basename מייצג את שם הבסיס של קובץ ההפעלה.

לדוגמה, כדי להגדיר את /system/bin/ping או את /data/local/tmp/ping לשימוש ב-MTE אסינכרוני, משתמשים ב-adb shell setprop arm64.memtag.process.ping async.

הפעלת MTE באמצעות משתנה סביבה

דרך נוספת לשנות את הגדרת ה-build של תהליכים מקוריים (לא אפליקציות) היא באמצעות הגדרת משתנה הסביבה: MEMTAG_OPTIONS=(off|sync|async). אם גם משתנה הסביבה וגם מאפיין המערכת מוגדרים, המשתנה מקבל עדיפות.

הפעלת MTE באפליקציות

אם לא מציינים את ההגדרה הזו, MTE מושבת כברירת מחדל, אבל אפליקציות שרוצות להשתמש ב-MTE יכולות לעשות זאת על ידי הגדרת android:memtagMode בתוך התג <application> או <process> ב-AndroidManifest.xml.

android:memtagMode=(off|default|sync|async)

כשמגדירים את המאפיין בתג <application>, הוא משפיע על כל התהליכים שבהם האפליקציה משתמשת, ואפשר לבטל את ההגדרה שלו בתהליכים ספציפיים על ידי הגדרת התג <process>.

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

האפשרויות האלה מופיעות בתפריט ההגדרות הכללי בקטע מערכת > אפשרויות מתקדמות > אפשרויות למפתחים > שינויים בתאימות האפליקציות. הגדרת NATIVE_MEMTAG_ASYNC או NATIVE_MEMTAG_SYNC מפעילה את MTE באפליקציה מסוימת. לחלופין, אפשר להגדיר את זה באמצעות הפקודה am באופן הבא:

$ adb shell am compat enable NATIVE_MEMTAG_[A]SYNC my.app.name

יצירת קובץ אימג' של המערכת MTE

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

מומלץ מאוד להפעיל MTE במצב סינכרוני בכל הקבצים הבינאריים המקוריים במהלך הפיתוח

SANITIZE_TARGET=memtag_heap SANITIZE_TARGET_DIAG=memtag_heap m

כמו כל משתנה במערכת ה-build, אפשר להשתמש ב-SANITIZE_TARGET כמשתנה סביבה או כהגדרה של make (לדוגמה, בקובץ product.mk).

ההגדרה הזו מפעילה את MTE לכל התהליכים המקוריים, אבל לא לאפליקציות (שהן פיצול של zygote64) שאפשר להפעיל עבורן את MTE לפי ההוראות במאמר הפעלת MTE לאפליקציות.

הגדרת רמת ה-MTE המועדפת שספציפית למעבד

במעבדים מסוימים, הביצועים של MTE במצבי ASYMM או אפילו SYNC עשויים להיות דומים לביצועים של ASYNC. לכן כדאי להפעיל בדיקות מחמירות יותר במעבדים האלה כשמתבקש מצב בדיקה פחות מחמיר, כדי ליהנות מיתרונות זיהוי השגיאות של הבדיקות המחמירות בלי לפגוע בביצועים.

כברירת מחדל, תהליכים שמוגדרים לפעול במצב ASYNC פועלים במצב ASYNC בכל המעבדים. כדי להגדיר את ליבת המערכת להפעלת התהליכים האלה במצב SYNC במעבדים ספציפיים, צריך לכתוב את הערך sync בערך sysfs /sys/devices/system/cpu/cpu<N>/mte_tcf_preferred בזמן האתחול. אפשר לעשות את זה באמצעות סקריפט הפעלה. לדוגמה, כדי להגדיר את מעבדי ה-CPU 0-1 להרצת תהליכים במצב ASYNC במצב SYNC, ואת מעבדי ה-CPU 2-3 להרצה במצב ASYMM, אפשר להוסיף את ההגדרה הבאה לסעיף init של סקריפט init של ספק:

  write /sys/devices/system/cpu/cpu0/mte_tcf_preferred sync
  write /sys/devices/system/cpu/cpu1/mte_tcf_preferred sync
  write /sys/devices/system/cpu/cpu2/mte_tcf_preferred asymm
  write /sys/devices/system/cpu/cpu3/mte_tcf_preferred asymm

‫Tombstones מתהליכים במצב ASYNC שפועלים במצב SYNC מכילים דוח קריסות מדויק של המיקום של שגיאת הזיכרון. עם זאת, הם לא כוללים הקצאה או ביטול הקצאה של דוח קריסות. ה-stack traces האלה זמינים רק אם התהליך מוגדר לפעול במצב SYNC.

int mallopt(M_THREAD_DISABLE_MEM_INIT, level)

כאשר level הוא 0 או 1.

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

int mallopt(M_MEMTAG_TUNING, level)

כאשר level הוא:

  • M_MEMTAG_TUNING_BUFFER_OVERFLOW
  • M_MEMTAG_TUNING_UAF

בחירה של אסטרטגיית הקצאת תגים.

  • הגדרת ברירת המחדל היא M_MEMTAG_TUNING_BUFFER_OVERFLOW.
  • M_MEMTAG_TUNING_BUFFER_OVERFLOW מאפשר זיהוי דטרמיניסטי של באגים של הצפת מאגר זמני לינארי וגלישה אל מתחת לגבולות המאגר, על ידי הקצאת ערכי תגים שונים להקצאות סמוכות. במצב הזה יש סיכוי קטן יותר לזהות באגים מסוג use-after-free, כי רק חצי מהערכים האפשריים של התגים זמינים לכל מיקום בזיכרון. ה-MTE לא יכול לזהות הצפה בתוך גרעין תג זהה (חלק מיושר של 16 בייט), ויכול לפספס הצפות קטנות גם במצב הזה. גלישה כזו לא יכולה לגרום להשחתת הזיכרון, כי הזיכרון בגרנול אחד אף פעם לא משמש להקצאות מרובות.
  • M_MEMTAG_TUNING_UAF מאפשר תגים אקראיים באופן עצמאי, עם הסתברות אחידה של כ-93% לזיהוי באגים מרחביים (גלישת חוצץ) וזמניים (שימוש אחרי שחרור).

בנוסף לממשקי ה-API שמתוארים למעלה, משתמשים מנוסים יכולים להשתמש גם בממשקי ה-API הבאים:

  • הגדרתPSTATE.TCO hardware register יכולה להשבית זמנית את בדיקת התג (דוגמה). לדוגמה, כשמעתיקים טווח של זיכרון עם תוכן תג לא ידוע, או כשמטפלים בצוואר בקבוק של ביצועים בלולאה פעילה.
  • כשמשתמשים ב-M_HEAP_TAGGING_LEVEL_SYNC, המערכת לטיפול בקריסות מספקת מידע נוסף, כמו הקצאה וביטול הקצאה של דוחות מעקב אחר מחסנית. כדי להשתמש בפונקציונליות הזו צריך גישה לביטים של התג, והיא מופעלת על ידי העברת הדגל SA_EXPOSE_TAGBITS כשמגדירים את המטפל באותות. מומלץ שכל תוכנית שמגדירה מטפל משלה באותות ומקצה קריסות לא מוכרות למערכת תעשה את אותו הדבר.

MTE בליבה

כדי להפעיל את KASAN עם האצת MTE עבור הליבה, צריך להגדיר את הליבה באמצעות CONFIG_KASAN=y, ‏ CONFIG_KASAN_HW_TAGS=y. ההגדרות האלה מופעלות כברירת מחדל בקרנלים של GKI, החל מ-Android 12-5.10.

אפשר לשלוט בזה בזמן האתחול באמצעות ארגומנטים של שורת הפקודה:

  • kasan=[on|off] – הפעלה או השבתה של KASAN (ברירת מחדל: on)
  • kasan.mode=[sync|async] - בחירה בין מצב סינכרוני למצב אסינכרוני (ברירת מחדל: sync)
  • kasan.stacktrace=[on|off] – האם לאסוף עקבות מחסנית (ברירת מחדל: on)
    • איסוף של דוחות קריסות דורש גם stack_depot_disable=off.
  • kasan.fault=[report|panic] – האם להדפיס רק את הדוח או גם לגרום לבהלת ליבה (ברירת מחדל: report). ללא קשר לאפשרות הזו, בדיקת התגים מושבתת אחרי השגיאה הראשונה שמדווחת.

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

מומלץ מאוד להשתמש במצב ASYNC בסביבת ייצור. הכלי הזה מספק תקורה נמוכה לזיהוי באגים של בטיחות הזיכרון בתהליך, וגם הגנה נוספת לעומק. אחרי שמזהים באג, המפתח יכול להשתמש בממשקי ה-API של זמן הריצה כדי לעבור למצב SYNC ולקבל דוח קריסות מדויק ממערך משתמשים שנבחר לדגימה.

מומלץ מאוד להגדיר את רמת ה-MTE המועדפת הספציפית ל-CPU עבור ה-SoC. בדרך כלל, למצב Asymm יש מאפייני ביצועים זהים למצב ASYNC, וכמעט תמיד עדיף להשתמש בו. ליבות קטנות לפי סדר ההוראות לרוב מציגות ביצועים דומים בכל שלושת המצבים, ואפשר להגדיר אותן כך שיעדיפו סנכרון.

מפתחים צריכים לבדוק אם יש קריסות על ידי בדיקה של /data/tombstones, logcat או על ידי מעקב אחר צינור DropboxManager הנתונים של הספק כדי לזהות באגים אצל משתמשי הקצה. מידע נוסף על ניפוי באגים בקוד Native של Android זמין כאן.

רכיבי פלטפורמה עם MTE מופעל

ב-Android 12, מספר רכיבי מערכת קריטיים לאבטחה משתמשים ב-MTE ASYNC כדי לזהות קריסות של משתמשי קצה ולפעול כשכבת הגנה נוספת. אלו הם הרכיבים:

  • כלי עזר ודמונים של רשתות (למעט netd)
  • ‫Bluetooth,‏ SecureElement,‏ NFC HALs ואפליקציות מערכת
  • statsd דימון
  • system_server
  • zygote64 (כדי לאפשר לאפליקציות להשתמש ב-MTE)

היעדים האלה נבחרו על סמך הקריטריונים הבאים:

  • תהליך עם הרשאות (מוגדר כתהליך שיש לו גישה למשהו שאין לדומיין unprivileged_app SELinux)
  • עיבוד קלט לא מהימן (כלל השניים)
  • האטה מקובלת בביצועים (ההאטה לא יוצרת חביון שגלוי למשתמש)

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

אנחנו מתכננים להרחיב את רשימת רכיבי המערכת שבהם מופעלת MTE, בהתאם למאפייני הביצועים של עיצובי חומרה עתידיים.