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

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

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

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

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

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

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

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

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

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

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

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

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

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

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).
אפשר למצוא את השינויים האלה בקטע System > Advanced > Developer options > App Compatibility Changes בתפריט ההגדרות הגלובלי. הגדרת NATIVE_MEMTAG_ASYNC או NATIVE_MEMTAG_SYNC מפעילה את MTE באפליקציה מסוימת.
אפשר גם להגדיר את ההגדרה הזו באמצעות הפקודה am באופן הבא:

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

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

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

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

SANITIZE_TARGET=memtag_heap SANITIZE_TARGET_DIAG=memtag_heap m

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

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

בחלק ממעבדי ה-CPU, הביצועים של MTE במצבים ASYMM או אפילו SYNC עשויים להיות דומים לביצועים של ASYNC. לכן כדאי להפעיל בדיקות מחמירות יותר במעבדי ה-CPU האלה כשמבקשים מצב בדיקה פחות מחמיר, כדי ליהנות מהיתרונות של זיהוי השגיאות בבדיקות המחמירות יותר בלי החסרונות בביצועים.
כברירת מחדל, תהליכים שהוגדרו לפעול במצב ASYNC יפעלו במצב ASYNC בכל מעבדי ה-CPU. כדי להגדיר את הליבה להפעלת התהליכים האלה במצב SYNC במעבדים ספציפיים, צריך לכתוב את סנכרון הערך ברשומה sysfs/sys/devices/system/cpu/cpu<N>/mte_tcf_preferred בזמן האתחול. אפשר לעשות זאת באמצעות סקריפט init. לדוגמה, כדי להגדיר את המעבדים 0-1 להרצת תהליכים במצב ASYNC במצב SYNC, ואת המעבדים 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

קבצים של Tombstone מתהליכים במצב ASYNC שפועלים במצב SYNC יכללו מעקב מדויק אחר סטאק של המיקום של שגיאת הזיכרון. עם זאת, הם לא יכללו מעקב סטאק של הקצאה או ביטול הקצאה. עקבות הסטאק האלה זמינים רק אם התהליך מוגדר לפעול במצב 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 – מאפשרת זיהוי דטרמיניסטית של באגים של זליגה של מאגר לינארי וזליגה של פחות מ-0 על ידי הקצאת ערכים שונים של תגים להקצאות סמוכות. במצב הזה יש סיכוי קטן יותר לזהות באגים מסוג 'שימוש אחרי שחרור', כי רק מחצית מערכי התגים האפשריים זמינים לכל מיקום זיכרון. חשוב לזכור ש-MTE לא יכול לזהות חריגה ממכסת הנתונים באותו גרגר תג (מקטע מיושר של 16 בייטים), ויכול לפספס חריגות קטנות ממכסת הנתונים גם במצב הזה. אין אפשרות שזרימת נתונים מוגזמת כזו תגרום לזיהום בזיכרון, כי הזיכרון בגרגר אחד אף פעם לא משמש למספר הקצאות.
  • M_MEMTAG_TUNING_UAF – מאפשרת להשתמש בתגים עם רנדומיזציה עצמאית, כדי לקבל סיכוי אחיד של כ-93% לזיהוי באגים מרחביים (overflow של מאגר) וזמניים (שימוש אחרי שחרור).

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

  • הגדרת הרישום של החומרהPSTATE.TCO יכולה להפסיק באופן זמני את בדיקת התגים (דוגמה). לדוגמה, כשמפעילים העתקה של טווח זיכרון עם תוכן תג לא ידוע, או כשמנסים לטפל בצווארון בקבוק בביצועים בלולאה חמה.
  • כשמשתמשים ב-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] – האם להדפיס רק את הדוח או גם להפעיל את מצב הלחץ (panic) בליבה (ברירת המחדל: report). ללא קשר לאפשרות הזו, בדיקת התגים מושבתת אחרי השגיאה הראשונה שדווחה.

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

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

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

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

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

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

  • שירותי דימון (daemons) וכלי שירות של רשתות (למעט netd)
  • Bluetooth,‏ SecureElement,‏ HAL ל-NFC ואפליקציות מערכת
  • דימון (daemon) של statsd
  • system_server
  • zygote64 (כדי לאפשר לאפליקציות להביע הסכמה לשימוש ב-MTE)

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

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

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