Warum MTE?
Speichersicherheitsfehler, also Fehler bei der Verwaltung von Speicher in nativen Programmiersprachen, sind häufige Codeprobleme. 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 standardmäßig nicht 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 Geräteleistung oder -stabilität beeinträchtigen kann. Sie kann jedoch für die App-Entwicklung nützlich sein. Rufen Sie in den Einstellungen Entwickleroptionen > „Memory Tagging Extension“ auf, um auf diesen Modus zuzugreifen. Wenn diese Option nicht angezeigt wird, 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:
- Google Pixel 8 (Shiba)
- Google Pixel 8 Pro (Husky)
- Google Pixel 8a (Akita)
- Google Pixel 9 (Tokay)
- Google Pixel 9 Pro (Caiman)
- Google Pixel 9 Pro XL (Komodo)
- Google Pixel 9 Pro Fold (Comet)
- Google Pixel 9a (Tegu)
Betriebsmodi von MTE
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 und kann daher für veröffentlichte Apps aktiviert werden.
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 mit der fehlerhaften Lade- oder Speicheranweisung mit SIGSEGV (mit si_code SEGV_MTESERR) und vollständigen Informationen zum Speicherzugriff und zur Fehleradresse.
Dieser Modus ist während der Tests eine schnellere Alternative zu HWASan, bei der Sie Ihren Code nicht neu kompilieren müssen. Er eignet sich auch für die Produktion, wenn Ihre App eine anfällige Angriffsfläche darstellt. Wenn im ASYNC-Modus (siehe unten) ein Fehler gefunden wurde, kann mithilfe der Laufzeit-APIs ein genauer Fehlerbericht erstellt werden, indem die Ausführung in den SYNC-Modus gewechselt wird.
Außerdem zeichnet der Android-Allokator im SYNC-Modus den Stack-Trace jeder Zuweisung und Freigabe auf und verwendet diese, um bessere Fehlerberichte zu erstellen, die eine Erklärung zu einem Arbeitsspeicherfehler wie Use-After-Free oder Pufferüberlauf und die Stack-Traces der relevanten Arbeitsspeicherereignisse enthalten. Weitere Informationen finden Sie unter MTE-Berichte verstehen. Solche Berichte enthalten mehr Kontextinformationen und erleichtern das Aufspüren und Beheben von Fehlern im Vergleich zum ASYNC-Modus.
Asynchroner Modus (ASYNC)
Dieser Modus ist für die Leistung optimiert und nicht für die Genauigkeit von Fehlerberichten. Er kann für die Erkennung von Speichersicherheitsfehlern mit geringem Overhead verwendet werden. Bei einer Tag-Nichtübereinstimmung führt der Prozessor die Ausführung bis zum nächsten Kerneleintrag fort (z. B. ein Systemaufruf oder Timerunterbrechung), wo er den Prozess mit SIGSEGV (Code SEGV_MTEAERR) beendet, ohne die fehlerhafte Adresse oder den Speicherzugriff aufzuzeichnen.
Dieser Modus ist nützlich, um Speichersicherheitslücken in der Produktion bei gut getesteten Codebases zu minimieren, bei denen die Dichte der Speichersicherheitsfehler bekanntlich gering ist. Dies wird durch die Verwendung des SYNC-Modus während der Tests erreicht.
MTE aktivieren
Für ein einzelnes Gerät
Für Tests können Änderungen an der App-Kompatibilität verwendet werden, um den Standardwert des Attributs memtagMode
für eine Anwendung festzulegen, für die im Manifest kein Wert angegeben ist (oder "default"
).
Sie finden sie im Menü „Globale Einstellungen“ unter „System“ > „Erweitert“ > „Entwickleroptionen“ > „Änderungen an der App-Kompatibilität“. Wenn Sie NATIVE_MEMTAG_ASYNC
oder NATIVE_MEMTAG_SYNC
festlegen, wird die MTE für eine bestimmte Anwendung aktiviert.
Alternativ können Sie dies mit dem Befehl am
so festlegen:
- 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
Ihres Manifests für Debug-Builds überschrieben.
Alternativ können Sie MTE für alle Builds eines benutzerdefinierten BuildType aktivieren. Erstellen Sie dazu einen eigenen buildType und legen Sie 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. Wenn Sie MTE in Apps verwenden möchten, können Sie android:memtagMode
unter dem <application>
- oder <process>
-Tag in der AndroidManifest.xml
festlegen.
android:memtagMode=(off|default|sync|async)
Wenn das Attribut im <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 Fehler bei der Beschädigung des Arbeitsspeichers im nativen Heap erkennen. Um Speicherbeschädigungen im Stack zu erkennen, müssen Sie nicht nur die MTE für die App aktivieren, sondern auch den Code mit Instrumentierung neu erstellen. Die resultierende App kann nur auf MTE-kompatiblen Geräten ausgeführt werden.
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 CMakeLists.txt:
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 die 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 in etwa so aussieht (beachten Sie die 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 verstehen. Sie können auch Ihre App mit Android Studio debuggen. Der Debugger hält an der Zeile an, die den ungültigen Speicherzugriff verursacht.
Fortgeschrittene Nutzer: MTE in Ihrem eigenen Allocator verwenden
Wenn Sie MTE für Speicher verwenden möchten, der nicht über die normalen Systemallokatoren zugewiesen wird, müssen Sie Ihren Allokator so ändern, dass Speicher und Zeiger getaggt werden.
Die Seiten für Ihren Allocator müssen mit PROT_MTE
im prot
-Flag von mmap
(oder mprotect
) zugewiesen werden.
Alle getaggten Zuordnungen müssen auf 16 Byte ausgerichtet sein, da Tags nur für 16-Byte-Chunks (auch als Granulate bezeichnet) zugewiesen werden können.
Bevor Sie einen Verweis zurückgeben, müssen Sie mit der Anweisung IRG
ein zufälliges Tag generieren und im Verweis speichern.
So taggen Sie den zugrunde liegenden Arbeitsspeicher:
STG
: Taggt ein einzelnes 16-Byte-Granul.ST2G
: zwei 16-Byte-Granulate taggenDC GVA
: Cachezeile mit demselben Tag taggen
Alternativ können Sie den Speicher auch mit der folgenden Anleitung auf Null initialisieren:
STZG
: Taggen und Nullinitialisieren eines einzelnen 16-Byte-GranelsSTZ2G
: Zwei 16-Byte-Granulate taggen und auf Null initialisierenDC GZVA
: Cachezeile mit demselben Tag taggen und auf Null initialisieren
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 die 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;
}
Als Referenz kann die scudo-Implementierung hilfreich sein.
Weitere Informationen
Weitere Informationen finden Sie im MTE-Nutzerhandbuch für Android-Betriebssysteme von Arm.