ARM-Speicher-Tagging-Erweiterung (MTE)

Warum MTE?

Speichersicherheitsfehler, also Fehler bei der Verarbeitung von Speicher in nativen Programmiersprachen, sind häufige Code-Probleme. Sie führen zu Sicherheitslücken und Stabilitätsproblemen.

Mit Armv9 wurde die Arm Memory Tagging Extension (MTE) eingeführt, eine Hardwareerweiterung, mit der Sie Use-After-Free- und Pufferüberlauffehler in Ihrem nativen Code erkennen können.

Support prüfen

Ab Android 13 unterstützen ausgewählte Geräte MTE. Führen Sie den folgenden Befehl aus, um zu prüfen, ob auf Ihrem Gerät MTE aktiviert ist:

adb shell grep mte /proc/cpuinfo

Wenn das Ergebnis Features : [...] mte ist, wird auf Ihrem Gerät MTE ausgeführt.

Auf einigen Geräten ist MTE nicht standardmäßig aktiviert, Entwickler können das Gerät aber mit aktiviertem MTE neu starten. Dies ist eine experimentelle Konfiguration, die für den normalen Gebrauch nicht empfohlen wird, da sie die Leistung oder Stabilität des Geräts beeinträchtigen kann. Für die App-Entwicklung kann sie jedoch nützlich sein. Um auf diesen Modus zuzugreifen, rufen Sie in den Einstellungen Entwickleroptionen > „Memory Tagging Extension“ auf. Wenn diese Option nicht vorhanden ist, unterstützt Ihr Gerät die Aktivierung von MTE auf diese Weise nicht.

Geräte mit MTE-Unterstützung

Die folgenden Geräte unterstützen MTE:

  • Pixel 8 (Shiba)
  • Pixel 8 Pro (Husky)
  • Pixel 8a (Akita)
  • Google Pixel 9 (Tokay)
  • Pixel 9 Pro (Caiman)
  • Pixel 9 Pro XL (Komodo)
  • Pixel 9 Pro Fold (Comet)
  • Google Pixel 9a (Tegu)
  • Pixel 10 (Frankel)
  • Google Pixel 10 Pro (Blazer)
  • Pixel 10 Pro XL (Mustang)
  • Pixel 10 Pro Fold (Rango)
  • Pixel 10a (Stallion)

MTE-Betriebsmodi

MTE unterstützt zwei Modi: SYNC und ASYNC. Der SYNC-Modus bietet bessere Diagnoseinformationen und eignet sich daher besser für Entwicklungszwecke. Der ASYNC-Modus bietet eine hohe Leistung, sodass er für veröffentlichte Apps aktiviert werden kann.

Synchroner Modus (SYNC)

Dieser Modus ist für die Fehlerbehebung optimiert und kann als präzises Tool zur Fehlererkennung verwendet werden, wenn ein höherer Leistungsaufwand akzeptabel ist. Wenn MTE SYNC aktiviert ist, dient es auch als Sicherheitsmaßnahme.

Bei einer Tag-Nichtübereinstimmung beendet der Prozessor den Prozess für die betreffende Lade- oder Speicheranweisung mit SIGSEGV (mit si_code SEGV_MTESERR) und vollständigen Informationen zum Speicherzugriff und zur fehlerhaften Adresse.

Dieser Modus ist beim Testen als schnellere Alternative zu HWASan nützlich, da Sie Ihren Code nicht neu kompilieren müssen. Er kann auch in der Produktion verwendet werden, wenn Ihre App eine anfällige Angriffsfläche darstellt. Wenn im ASYNC-Modus (siehe unten) ein Fehler gefunden wurde, kann außerdem ein genauer Fehlerbericht erstellt werden, indem die Ausführung mithilfe der Runtime-APIs in den SYNC-Modus umgeschaltet wird.

Außerdem zeichnet der Android-Allocator im SYNC-Modus den Stacktrace jeder Zuweisung und Freigabe auf und verwendet ihn, um bessere Fehlerberichte zu erstellen, die eine Erklärung eines Arbeitsspeicherfehlers wie „Use-After-Free“ oder „Pufferüberlauf“ sowie die Stacktraces der relevanten Arbeitsspeicherereignisse enthalten (weitere Informationen finden Sie unter MTE-Berichte verstehen). Solche Berichte enthalten mehr Kontextinformationen und machen es einfacher, Fehler zu verfolgen und zu beheben als im ASYNC-Modus.

Asynchroner Modus (ASYNC)

Dieser Modus ist auf Leistung statt auf Genauigkeit von Fehlerberichten optimiert und kann für die ressourcenschonende Erkennung von Memory-Safety-Bugs verwendet werden. Bei einer Tag-Fehlerübereinstimmung wird die Ausführung auf dem Prozessor bis zum nächsten Kerneleintrag (z. B. ein Syscall oder ein Timer-Interrupt) fortgesetzt. Dort wird der Prozess mit SIGSEGV (Code SEGV_MTEAERR) beendet, ohne die fehlerhafte Adresse oder den Speicherzugriff aufzuzeichnen.

Dieser Modus ist nützlich, um Speicher-Sicherheitslücken in der Produktion bei gut getesteten Codebases zu minimieren, bei denen die Dichte von Speicher-Sicherheitsfehlern bekanntermaßen gering ist. Dies wird durch die Verwendung des SYNC-Modus während des Tests erreicht.

MTE aktivieren

Für ein einzelnes Gerät

Bei Tests können mit Änderungen der App-Kompatibilität der Standardwert des Attributs memtagMode für eine Anwendung festgelegt werden, in deren Manifest kein Wert angegeben ist (oder "default" angegeben ist).

Sie finden sie im globalen Einstellungsmenü unter „System“ > „Erweitert“ > „Entwickleroptionen“ > „Änderungen bei der App-Kompatibilität“. Durch Festlegen von NATIVE_MEMTAG_ASYNC oder NATIVE_MEMTAG_SYNC wird MTE für eine bestimmte Anwendung aktiviert.

Alternativ kann dies mit dem Befehl am festgelegt werden:

  • Für den SYNC-Modus: $ adb shell am compat enable NATIVE_MEMTAG_SYNC my.app.name
  • Für den ASYNC-Modus: $ adb shell am compat enable NATIVE_MEMTAG_ASYNC my.app.name

In Gradle

Sie können MTE für alle Debug-Builds Ihres Gradle-Projekts aktivieren, indem Sie

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application android:memtagMode="sync" tools:replace="android:memtagMode"/>
</manifest>

in app/src/debug/AndroidManifest.xml. Dadurch wird die memtagMode-Einstellung in Ihrem Manifest für Debug-Builds überschrieben.

Alternativ können Sie MTE für alle Builds eines benutzerdefinierten Build-Typs aktivieren. Dazu erstellen Sie einen eigenen buildType und legen die XML-Datei in app/src/<name of buildType>/AndroidManifest.xml ab.

Für eine APK auf einem beliebigen kompatiblen Gerät

MTE ist standardmäßig deaktiviert. Apps, die MTE verwenden möchten, können dies tun, indem sie android:memtagMode unter dem Tag <application> oder <process> in der AndroidManifest.xml festlegen.

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

Wenn das Attribut für das <application>-Tag festgelegt ist, wirkt es sich auf alle von der Anwendung verwendeten Prozesse aus. Es kann für einzelne Prozesse überschrieben werden, indem das <process>-Tag festgelegt wird.

Mit Instrumentierung erstellen

Wenn Sie MTE wie oben beschrieben aktivieren, können Sie Arbeitsspeicherfehler im nativen Heap erkennen. Um Speicherbeschädigungen im Stack zu erkennen, muss der Code mit Instrumentierung neu erstellt werden. Außerdem muss MTE für die App aktiviert sein. Die resultierende App wird nur auf MTE-kompatiblen Geräten ausgeführt.

So erstellen Sie den nativen (JNI-)Code Ihrer App mit MTE:

ndk-build

In Ihrer Application.mk-Datei:

APP_CFLAGS := -fsanitize=memtag -fno-omit-frame-pointer -march=armv8-a+memtag
APP_LDFLAGS := -fsanitize=memtag -fsanitize-memtag-mode=sync -march=armv8-a+memtag

CMake

Für jedes Ziel in Ihrer CMakeLists.txt-Datei:

target_compile_options(${TARGET} PUBLIC -fsanitize=memtag -fno-omit-frame-pointer -march=armv8-a+memtag)
target_link_options(${TARGET} PUBLIC -fsanitize=memtag -fsanitize-memtag-mode=sync -march=armv8-a+memtag)

App ausführen

Nachdem Sie MTE aktiviert haben, können Sie Ihre App wie gewohnt verwenden und testen. Wenn ein Problem mit der Speichersicherheit erkannt wird, stürzt Ihre App mit einem Tombstone ab, der so ähnlich aussieht (beachten Sie das SIGSEGV mit SEGV_MTESERR für SYNC oder SEGV_MTEAERR für ASYNC):

pid: 13935, tid: 13935, name: sanitizer-statu  >>> sanitizer-status <<<
uid: 0
tagged_addr_ctrl: 000000000007fff3
signal 11 (SIGSEGV), code 9 (SEGV_MTESERR), fault addr 0x800007ae92853a0
Cause: [MTE]: Use After Free, 0 bytes into a 32-byte allocation at 0x7ae92853a0
x0  0000007cd94227cc  x1  0000007cd94227cc  x2  ffffffffffffffd0  x3  0000007fe81919c0
x4  0000007fe8191a10  x5  0000000000000004  x6  0000005400000051  x7  0000008700000021
x8  0800007ae92853a0  x9  0000000000000000  x10 0000007ae9285000  x11 0000000000000030
x12 000000000000000d  x13 0000007cd941c858  x14 0000000000000054  x15 0000000000000000
x16 0000007cd940c0c8  x17 0000007cd93a1030  x18 0000007cdcac6000  x19 0000007fe8191c78
x20 0000005800eee5c4  x21 0000007fe8191c90  x22 0000000000000002  x23 0000000000000000
x24 0000000000000000  x25 0000000000000000  x26 0000000000000000  x27 0000000000000000
x28 0000000000000000  x29 0000007fe8191b70
lr  0000005800eee0bc  sp  0000007fe8191b60  pc  0000005800eee0c0  pst 0000000060001000

backtrace:
      #00 pc 00000000000010c0  /system/bin/sanitizer-status (test_crash_malloc_uaf()+40) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #01 pc 00000000000014a4  /system/bin/sanitizer-status (test(void (*)())+132) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #02 pc 00000000000019cc  /system/bin/sanitizer-status (main+1032) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #03 pc 00000000000487d8  /apex/com.android.runtime/lib64/bionic/libc.so (__libc_init+96) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)

deallocated by thread 13935:
      #00 pc 000000000004643c  /apex/com.android.runtime/lib64/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::quarantineOrDeallocateChunk(scudo::Options, void*, scudo::Chunk::UnpackedHeader*, unsigned long)+688) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #01 pc 00000000000421e4  /apex/com.android.runtime/lib64/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::deallocate(void*, scudo::Chunk::Origin, unsigned long, unsigned long)+212) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #02 pc 00000000000010b8  /system/bin/sanitizer-status (test_crash_malloc_uaf()+32) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #03 pc 00000000000014a4  /system/bin/sanitizer-status (test(void (*)())+132) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)

allocated by thread 13935:
      #00 pc 0000000000042020  /apex/com.android.runtime/lib64/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::allocate(unsigned long, scudo::Chunk::Origin, unsigned long, bool)+1300) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #01 pc 0000000000042394  /apex/com.android.runtime/lib64/bionic/libc.so (scudo_malloc+36) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #02 pc 000000000003cc9c  /apex/com.android.runtime/lib64/bionic/libc.so (malloc+36) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #03 pc 00000000000010ac  /system/bin/sanitizer-status (test_crash_malloc_uaf()+20) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #04 pc 00000000000014a4  /system/bin/sanitizer-status (test(void (*)())+132) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
Learn more about MTE reports: https://source.android.com/docs/security/test/memory-safety/mte-report

Weitere Informationen finden Sie in der AOSP-Dokumentation unter MTE-Berichte. Sie können Ihre App auch mit Android Studio debuggen. Der Debugger hält dann an der Zeile an, die den ungültigen Speicherzugriff verursacht.

Fortgeschrittene Nutzer: MTE im eigenen Zuweisungstool verwenden

Wenn Sie MTE für Speicher verwenden möchten, der nicht über die normalen Systemzuweiser zugewiesen wurde, müssen Sie Ihren Zuweiser so ändern, dass Speicher und Zeiger getaggt werden.

Die Seiten für Ihren Zuweiser müssen mit PROT_MTE im prot-Flag von mmap (oder mprotect) zugewiesen werden.

Alle zugewiesenen Speicherbereiche mit Tags müssen 16‑Byte-ausgerichtet sein, da Tags nur für 16‑Byte-Blöcke (auch als Granulat bezeichnet) zugewiesen werden können.

Bevor Sie einen Zeiger zurückgeben, müssen Sie mit der Anweisung IRG ein zufälliges Tag generieren und im Zeiger speichern.

So taggen Sie den zugrunde liegenden Speicher:

  • STG: Ein einzelnes 16-Byte-Granulat taggen
  • ST2G: Zwei 16‑Byte-Granulat-Tags
  • DC GVA: Tag-Cachezeile mit demselben Tag

Alternativ können Sie den Speicher auch mit den folgenden Schritten mit Nullen initialisieren:

  • STZG: Ein einzelnes 16‑Byte-Granulat mit einem Tag versehen und mit Nullen initialisieren
  • STZ2G: Zwei 16‑Byte-Granulat-Einheiten mit Tags versehen und mit Nullen initialisieren
  • DC GZVA: Tag und Cachezeile mit demselben Tag auf null setzen

Diese Anleitung wird auf älteren CPUs nicht unterstützt. Sie müssen sie also bedingt ausführen, wenn MTE aktiviert ist. So prüfen Sie, ob MTE für Ihren Prozess aktiviert ist:

#include <sys/prctl.h>

bool runningWithMte() {
      int mode = prctl(PR_GET_TAGGED_ADDR_CTRL, 0, 0, 0, 0);
      return mode != -1 && mode & PR_MTE_TCF_MASK;
}

Die Scudo-Implementierung kann als Referenz hilfreich sein.

Weitere Informationen

Weitere Informationen finden Sie im MTE User Guide for Android OS von Arm.