Rozszerzenie Arm Memory Tagging Extension (MTE)

Dlaczego MTE?

Błędy dotyczące bezpieczeństwa pamięci, czyli błędy w zarządzaniu pamięcią w natywnąch językach programowania, są częstymi problemami z kodem. Prowadzą one do luk w zabezpieczeniach oraz problemów ze stabilnością.

W Armv9 wprowadzono rozszerzenie sprzętowe Arm Memory Tagging Extension (MTE), które pozwala wykrywać w kodzie natywnym błędy związane z odwołaniem do pamięci po jej zwolnieniu (use-after-free) i przepełnieniem bufora (buffer overflow).

Sprawdź, czy potrzebujesz pomocy

Począwszy od Androida 13 wybrane urządzenia obsługują MTE. Aby sprawdzić, czy na urządzeniu włączona jest funkcja MTE, uruchom to polecenie:

adb shell grep mte /proc/cpuinfo

Jeśli wynik to Features : [...] mte, urządzenie działa z włączoną funkcją MTE.

Niektóre urządzenia nie mają włączonego MTE domyślnie, ale pozwalają deweloperom na ponowne uruchomienie z włączonym MTE. Jest to eksperymentalna konfiguracja, która nie jest zalecana do normalnego użytku, ponieważ może obniżyć wydajność lub stabilność urządzenia. Może być jednak przydatna podczas tworzenia aplikacji. Aby uzyskać dostęp do tego trybu, otwórz aplikację Ustawienia i kliknij Opcje dla deweloperów > Rozszerzenie oznaczenia pamięci. Jeśli tej opcji nie ma, oznacza to, że Twoje urządzenie nie obsługuje włączania MTE w ten sposób.

Urządzenia z obsługą MTE

Wiadomo, że te urządzenia obsługują MTE:

  • Pixel 8 (Shiba)
  • Pixel 8 Pro (Husky)
  • Pixel 8a (Akita)
  • Pixel 9 (Tokay)
  • Pixel 9 Pro (Caiman)
  • Pixel 9 Pro XL (Komodo)
  • Pixel 9 Pro Fold (Comet)
  • Pixel 9a (Tegu)

Tryby działania MTE

MTE obsługuje 2 tryby: SYNC i ASYNC. Tryb synchronizacji zapewnia lepsze informacje diagnostyczne, dlatego jest bardziej odpowiedni do celów programistycznych, natomiast tryb asynchroniczny ma wysoką wydajność, co pozwala go włączyć w przypadku opublikowanych aplikacji.

Tryb synchroniczny (SYNC)

Ten tryb jest zoptymalizowany pod kątem możliwości debugowania, a nie wydajności, i może być używany jako precyzyjne narzędzie do wykrywania błędów, gdy akceptowalne jest większe obciążenie wydajności. Gdy jest włączona, synchronizacja MTE pełni też funkcję zabezpieczenia.

W przypadku niezgodności tagów procesor kończy proces na instrukcji ładowania lub przechowywania, która spowodowała błąd, za pomocą sygnału SIGSEGV (z si_code SEGV_MTESERR) oraz pełnych informacji o dostępie do pamięci i adresie błędu.

Ten tryb jest przydatny podczas testowania, ponieważ stanowi szybszą alternatywę dla HWASan, która nie wymaga ponownego kompilowania kodu. Jest on również przydatny w produkcji, gdy aplikacja stanowi podatny na ataki obszar. Ponadto, gdy w trybie asynchronicznym (opisanym poniżej) zostanie wykryty błąd, można uzyskać dokładny raport o błędzie, przełączając wykonanie do trybu synchronicznego za pomocą interfejsów API w czasie wykonywania.

Ponadto podczas działania w trybie synchronizacji przydzielnik Androida rejestruje zrzuty stosu dotyczące każdej alokacji i zwolnienia alokacji i wykorzystuje je do tworzenia lepszych raportów o błędach, które zawierają wyjaśnienie błędu pamięci, np. use-after-free lub przepełnienia bufora, oraz zrzuty stosu odpowiednich zdarzeń pamięci (więcej informacji znajdziesz w artykule Informacje o raportach MTE). Takie raporty zawierają więcej informacji kontekstowych i ułatwiają śledzenie oraz naprawianie błędów niż w trybie asynchronicznym.

Tryb asynchroniczny (ASYNC)

Ten tryb jest zoptymalizowany pod kątem wydajności, a nie dokładności raportów o błędach, i może służyć do wykrywania błędów związanych z bezpieczeństwem pamięci przy niskim narzucie. W przypadku niezgodności tagów procesor kontynuuje wykonywanie kodu do najbliższego wpisu w jądrze (takiego jak syscall lub przerwanie zegara), gdzie kończy proces za pomocą SIGSEGV (kod SEGV_MTEAERR) bez zapisywania adresu błędu lub dostępu do pamięci.

Ten tryb jest przydatny do ograniczania podatności na luki w zabezpieczeniach związanych z pamięcią w produkcji w dobrze przetestowanych bazach kodu, w których gęstość błędów związanych z zabezpieczeniami pamięci jest na ogół niska. Można to osiągnąć, używając trybu SYNC podczas testów.

Włączanie MTE

Na jednym urządzeniu

W celu eksperymentowania możesz użyć zmian dotyczących zgodności aplikacji, aby ustawić domyślną wartość atrybutu memtagMode w przypadku aplikacji, która nie określa żadnej wartości w pliku manifestu (lub określa wartość "default").

Znajdziesz je w menu ustawień globalnych w sekcji System > Zaawansowane > Opcje programisty > Zmiany dotyczące zgodności aplikacji. Ustawienie NATIVE_MEMTAG_ASYNC lub NATIVE_MEMTAG_SYNC włącza MTE w przypadku konkretnej aplikacji.

Możesz też ustawić tę wartość za pomocą polecenia am w ten sposób:

  • Tryb synchronizacji: $ adb shell am compat enable NATIVE_MEMTAG_SYNC my.app.name
  • Tryb asynchroniczny:$ adb shell am compat enable NATIVE_MEMTAG_ASYNC my.app.name

W Gradle

Możesz włączyć MTE dla wszystkich wersji debugowania projektu Gradle, umieszczając w pliku build.gradle.yml następujący kod:

<?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>

app/src/debug/AndroidManifest.xml. Spowoduje to zastąpienie pliku manifestu memtagMode synchronizacją dla wersji debugowania.

Możesz też włączyć MTE dla wszystkich wersji niestandardowego buildType. Aby to zrobić, utwórz własny parametr buildType i umieścić plik XML w polu app/src/<name of buildType>/AndroidManifest.xml.

APK na dowolnym urządzeniu z możliwością obsługi

Domyślnie MTE jest wyłączona. Aplikacje, które chcą korzystać z MTE, mogą to zrobić, ustawiając wartość android:memtagMode w tagu <application> lub <process> w elementach AndroidManifest.xml.

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

Gdy jest ustawiony w tagu <application>, atrybut wpływa na wszystkie procesy używane przez aplikację. Możesz go zastąpić w przypadku poszczególnych procesów, ustawiając tag <process>.

Kompilacja z użyciem instrumentacji

Włączenie MTE, jak opisano wcześniej, pomaga wykrywać błędy związane z uszkodzoną pamięcią w miejscu natywnym. Aby wykrywać uszkodzenia pamięci na stosie, oprócz włączenia w aplikacji opcji MTE kod musi zostać ponownie skompilowany z użyciem instrumentacji. Utworzona aplikacja będzie działać tylko na urządzeniach obsługujących MTE.

Aby skompilować kod natywny (JNI) aplikacji za pomocą MTE, wykonaj te czynności:

ndk-build

W pliku Application.mk:

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

W przypadku każdego celu w pliku 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)

Uruchamianie aplikacji

Po włączeniu MTE możesz normalnie korzystać z aplikacji i ją testować. Jeśli zostanie wykryty problem z zabezpieczeniami pamięci, aplikacja ulegnie awarii, a na ekranie pojawi się komunikat o wypadku podobny do tego (zamiast SIGSEGV wpisz SEGV_MTESERR, aby oznaczać synchronizację synchroniczną, lub SEGV_MTEAERR, aby oznaczać synchronizację asynchroniczną):

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

Aby dowiedzieć się więcej, zapoznaj się z artykułem Informacje o raportach MTE w dokumentacji AOSP. Możesz też debugować aplikację za pomocą Android Studio. Debuger zatrzyma się na linii, która powoduje nieprawidłowy dostęp do pamięci.

Użytkownicy zaawansowani: korzystanie z MTE w ramach własnego przydziału

Aby używać MTE do pamięci nieprzydzielonej przez zwykłych alokacji systemowych, musisz zmodyfikować alokator, aby oznaczać pamięć i wskaźniki.

Strony dla Twojego przydziału muszą być przypisane za pomocą wartości PROT_MTE w flagach prot (lub mprotect).mmap

Wszystkie przypisane tagami alokacje muszą być wyrównane co 16 bajtów, ponieważ tagi można przypisać tylko do 16-bajtowych fragmentów (zwanych też granulami).

Następnie, zanim zwrócisz wskaźnik, musisz użyć instrukcji IRG, aby wygenerować losowy tag i zapisać go w wskaźniku.

Aby oznaczyć pamięć podrzędną:

  • STG: otaguj pojedynczy 16-bajtowy granule
  • ST2G: taguj 2 granulaty 16-bajtowe
  • DC GVA: tagowanie linii pamięci podręcznej tym samym tagiem

Pamięć można też zainicjować do wartości 0, wykonując te instrukcje:

  • STZG: otagowanie i zerowanie pojedynczego ziarna o długości 16 bajtów
  • STZ2G: otaguj i zeruj 2 granule 16-bajtowe
  • DC GZVA: tagowanie i zerowanie linii pamięci podręcznej z tym samym tagiem

Pamiętaj, że te instrukcje nie są obsługiwane na starszych procesorach, dlatego musisz je warunkowo uruchamiać, gdy włączona jest funkcja MTE. Aby sprawdzić, czy MTE jest włączone w przypadku Twojego procesu:

#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;
}

Jako punkt odniesienia możesz wykorzystać implementację scudo.

Więcej informacji

Więcej informacji znajdziesz w przewodniku użytkownika MTE na system operacyjny Android opracowanym przez firmę Arm.