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 taggenST2G: Zwei 16‑Byte-Granulat-TagsDC 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 initialisierenSTZ2G: Zwei 16‑Byte-Granulat-Einheiten mit Tags versehen und mit Nullen initialisierenDC 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.