Rozszerzenie Arm Memory Tagging Extension (MTE)

Dlaczego MTE?

Typowe problemy z kodem to błędy dotyczące bezpieczeństwa pamięci, czyli błędy w obsłudze pamięci w językach programowania natywnego. Wiążą się one z lukami w zabezpieczeniach i stabilnością.

W firmie Armv9 wprowadziliśmy rozszerzenie Arm Memory Tagging Extension (MTE) – rozszerzenie sprzętowe, które umożliwia wychwytywanie błędów związanych z używaniem po zwolnieniu z pamięci i przepełnieniem bufora w kodzie natywnym.

Sprawdź, czy masz dostęp do pomocy

Wybrane urządzenia (od Androida 13) obsługują MTE. Aby sprawdzić, czy urządzenie działa z włączoną obsługą MTE, uruchom to polecenie:

adb shell grep mte /proc/cpuinfo

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

Niektóre urządzenia domyślnie nie włączają MTE, ale pozwalają deweloperom na restartowanie z włączoną obsługą MTE. To konfiguracja eksperymentalna, która nie jest zalecana do normalnego użytkowania, ponieważ może obniżyć wydajność i stabilność urządzenia, ale może być przydatna podczas tworzenia aplikacji. Aby uzyskać dostęp do tego trybu, w aplikacji Ustawienia otwórz Opcje programisty > Rozszerzenie Memory Tagging Extension. Jeśli nie widzisz tej opcji, Twoje urządzenie nie obsługuje MTE w ten sposób.

Tryby działania MTE

MTE obsługuje 2 tryby: SYNCHRONIZUJ i ASYNC. Tryb SYNCHRONIZACJA dostarcza dokładniejsze dane diagnostyczne, dzięki czemu lepiej nadaje się do celów programistycznych. Tryb ASYNC oferuje też wysoką wydajność, co pozwala na jego włączenie dla opublikowanych aplikacji.

Tryb synchroniczny (SYNC)

Ten tryb jest zoptymalizowany pod kątem debugowania, a nie wydajności, i może służyć jako precyzyjne narzędzie do wykrywania błędów, gdy dopuszczalne jest wymagania w zakresie wydajności. Po włączeniu MTE SYNC działa również jako ograniczenie bezpieczeństwa.

W przypadku niezgodności tagów procesor kończy proces przy nieprawidłowym obciążeniu lub instrukcji przechowywania za pomocą SIGSEGV (z si_code SEGV_MTESERR) i pełnych informacji o dostępie do pamięci oraz adresie błędu.

Ten tryb jest przydatny podczas testowania jako szybsza alternatywa dla HWASan, która nie wymaga ponownej kompilacji kodu, ani w środowisku produkcyjnym, gdy aplikacja reprezentuje lukę w zabezpieczeniach. Poza tym, gdy tryb ASYNC (opisany poniżej) wykryje błąd, można uzyskać dokładny raport o błędzie, przełączając działanie w tryb SYNC za pomocą interfejsów API środowiska wykonawczego.

Co więcej, gdy działa w trybie SYNCHRONIZACJI, alokacja Androida rejestruje ślad stosu każdego przydziału i umowy, a potem używa ich do generowania lepszych raportów o błędach zawierających wyjaśnienie błędu pamięci, np. użycie po zwolnieniu lub przepełnienie bufora, oraz ślady stosu odpowiednich zdarzeń pamięci (więcej informacji znajdziesz w opisie raportów MTE). Takie zgłoszenia dostarczają więcej informacji kontekstowych oraz ułatwiają śledzenie i naprawianie błędów niż w trybie ASYNC.

Tryb asynchroniczny (ASYNC)

Ten tryb jest zoptymalizowany pod kątem wydajności, a nie dokładności raportów o błędach. Można go używać do wykrywania błędów związanych z bezpieczeństwem pamięci. W przypadku niezgodności tagów procesor kontynuuje wykonywanie kodu do momentu wejścia w najbliższy wpis jądra (np. wywołania syscall lub przerwania licznika), gdzie kończy proces przy użyciu SIGSEGV (kod SEGV_MTEAERR) bez rejestrowania adresu błędu ani dostępu do pamięci.

Ten tryb jest przydatny do eliminowania luk w zabezpieczeniach związanych z bezpieczeństwem pamięci w środowisku produkcyjnym w dobrze przetestowanych bazach kodu, gdy gęstość błędów związanych z bezpieczeństwem pamięci jest rzadko utrzymywana. Można to osiągnąć podczas testowania w trybie SYNC.

Włącz MTE

Jedno urządzenie

W ramach eksperymentów zmiany zgodności aplikacji można wykorzystać, aby ustawić domyślną wartość atrybutu memtagMode dla aplikacji, która nie określa żadnej wartości w pliku manifestu (lub określa "default").

Znajdziesz je, wybierając System > Zaawansowane > Opcje programisty > Zmiany zgodności aplikacji w globalnym menu ustawień. Ustawienie NATIVE_MEMTAG_ASYNC lub NATIVE_MEMTAG_SYNC włącza MTE w konkretnej aplikacji.

Wartość tę można też ustawić za pomocą polecenia am w ten sposób:

  • W przypadku trybu SYNCHRONIZACJI: $ adb shell am compat enable NATIVE_MEMTAG_SYNC my.app.name
  • W przypadku trybu ASYNC: $ adb shell am compat enable NATIVE_MEMTAG_ASYNC my.app.name

W Gradle

Możesz włączyć MTE we wszystkich kompilacjach do debugowania projektu Gradle, umieszczając

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

w zakresie app/src/debug/AndroidManifest.xml. Spowoduje to zastąpienie pola memtagMode Twojego pliku manifestu synchronizacją kompilacji do debugowania.

Możesz też włączyć MTE w przypadku wszystkich kompilacji niestandardowego elementu buildType. Aby to zrobić, utwórz własny model buildType i umieść w nim kod XML w pliku app/src/<name of buildType>/AndroidManifest.xml.

Pliki APK na dowolnym urządzeniu obsługującym tę funkcję

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

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

Jeśli ustawisz atrybut w tagu <application>, będzie on miał wpływ na wszystkie procesy używane przez aplikację i będzie można go zastąpić dla poszczególnych procesów za pomocą tagu <process>.

Uruchamianie aplikacji

Po włączeniu MTE używaj i testuj aplikację jak zwykle. Jeśli zostanie wykryty problem z bezpieczeństwem pamięci, aplikacja ulegnie awarii, a nadmiarowy element nagrobkowy wygląda mniej więcej tak (zwróć uwagę na SIGSEGV z SEGV_MTESERR w przypadku SYNC lub SEGV_MTEAERR w przypadku 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

Więcej informacji znajdziesz w sekcji Omówienie raportów MTE w dokumentacji AOSP. Możesz też zdebugować aplikację za pomocą Android Studio. Debuger zatrzyma się na tym wierszu, co spowoduje nieprawidłowy dostęp do pamięci.

Użytkownicy zaawansowani: korzystanie z MTE w własnym przydzielaniu

Aby używać MTE do obsługi pamięci, która nie jest przydzielana za pomocą zwykłych przydziałów systemu, musisz zmodyfikować alokator tak, aby zawierał tagi w pamięci i wskaźnikach.

Strony w ramach przydzielania muszą być przydzielone za pomocą parametru PROT_MTE we fladze prot elementu mmap (czyli mprotect).

Wszystkie otagowane przydziały muszą mieć wyrównanie 16 bajtów, ponieważ tagi można przypisywać tylko dla fragmentów 16-bajtowych (nazywanych też granulatami).

Następnie, zanim zwrócisz wskaźnik, musisz za pomocą instrukcji IRG wygenerować losowy tag i zapisać go w wskaźniku.

Aby dodać tagi do bazowej pamięci, skorzystaj z tych instrukcji:

  • STG: dodawanie tagów do pojedynczego 16-bajtowego granulu
  • ST2G: dodaj tagi do 2 granulatów 16-bajtowych
  • DC GVA: tag pamięci podręcznej tym samym tagiem

Możesz też wykonać te instrukcje, aby zero zainicjować pamięć:

  • STZG: oznaczanie tagami i rozpoczynanie bez inicjowania pojedynczego granulu 16-bajtowego
  • STZ2G: oznaczanie tagami i rozpoczynanie bez inicjowania 2 granulatów 16-bajtowych
  • DC GZVA: dodawaj tagi i rozpoczynaj zerową inicjalizację pamięci podręcznej z tym samym tagiem.

Te instrukcje nie są obsługiwane w przypadku starszych procesorów, więc musisz je uruchomić warunkowo, gdy MTE jest włączone. Możesz sprawdzić, czy MTE jest włączone w Twoim procesie:

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

Pomocna może być dla Ciebie implementacja scudo.

Więcej informacji

Więcej informacji znajdziesz w przewodniku użytkownika MTE dla systemu Android autorstwa Arm.