Włączanie rozszerzenia otagowania pamięci

W wersji Arm 9 wprowadzono rozszerzenie dotyczące tagowania pamięci (MTE), które jest wdrożeniem sprzętowym tagowanej pamięci.

Ogólnie rzecz biorąc, MTE oznacza każdą alokację/zwolnienie pamięci dodatkowymi metadanymi. Przypisuje on tag do lokalizacji pamięci, która może być powiązana z wskaźnikami odwołującymi się do tej lokalizacji. Podczas działania procesor sprawdza, czy wskaźnik i tagi metadanych są zgodne przy każdym wczytaniu i zapisywaniu.

W Androidzie 12 alokator pamięci stosu w rdzeniu i w przestrzeni użytkownika może uzupełniać każdą alokację o metadane. Pomaga to wykrywać błędy związane z użyciem po zwolnieniu i przepełnieniem bufora, które są najczęstszym źródłem błędów związanych z bezpieczeństwem pamięci w naszych bazach kodu.

Tryby działania MTE

Rozszerzenie MTE ma 3 tryby działania:

  • Tryb synchroniczny (SYNC)
  • Tryb asynchroniczny (ASYNC)
  • Tryb asymetryczny (ASYMM)

Tryb synchroniczny (SYNC)

Ten tryb jest zoptymalizowany pod kątem prawidłowego wykrywania błędów, a nie wydajności. Można go używać jako precyzyjnego narzędzia do wykrywania błędów, gdy akceptowalne jest większe obciążenie wydajności. Gdy jest włączona, synchronizacja MTE działa jako zabezpieczenie. W przypadku niezgodności tagów procesor natychmiast przerywa wykonywanie kodu i kończy proces z wartością SIGSEGV (kod SEGV_MTESERR) oraz pełnymi informacjami o dostępie do pamięci i adresie błędu.

Zalecamy używanie tego trybu podczas testowania jako alternatywy dla HWASan/KASAN lub w wersji produkcyjnej, gdy docelowy proces reprezentuje podatny na ataki obszar. Ponadto, gdy tryb asynchroniczny wskaże obecność błędu, można uzyskać dokładny raport o błędzie, przełączając wykonanie w tryb synchroniczny za pomocą interfejsów API w czasie wykonywania.

W trybie synchronizacji algorytm alokacji Androida rejestruje ścieżki stosu dla wszystkich alokacji i zwolnień 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łnienie bufora, oraz ścieżki stosu odpowiednich zdarzeń pamięci. Takie raporty zawierają więcej informacji kontekstowych i ułatwiają śledzenie oraz naprawianie błędów.

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 przy niskim nakładzie pracy.
W przypadku niezgodności tagów procesor kontynuuje wykonywanie kodu do najbliższego wpisu w jądrze (na przykład wywołania systemu lub przerwania zegara), gdzie kończy proces z wartością SIGSEGV (kod SEGV_MTEAERR) bez zapisywania adresu błędu lub dostępu do pamięci.
Zalecamy używanie tego trybu w produkcji w przypadku dobrze przetestowanych baz kodu, w których gęstość błędów związanych z bezpieczeństwem pamięci jest niska. Można to osiągnąć, używając trybu synchronizacji podczas testowania.

Tryb asymetryczny (ASYMM)

Dodatkowa funkcja w procesorach Arm w wersji 8.7-A, tryb asymetryczny MTE, zapewnia synchroniczne sprawdzanie odczytów pamięci i asynchroniczne sprawdzanie zapisów pamięci z wydajnością zbliżoną do trybu ASYNC. W większości sytuacji ten tryb jest lepszy niż tryb ASYNC, dlatego zalecamy używanie go zamiast ASYNC, o ile jest dostępny.

Z tego powodu żaden z interfejsów API opisanych poniżej nie wspomina o trybie asymetrycznym. Zamiast tego system operacyjny może być skonfigurowany tak, aby zawsze używać trybu asymetrycznego, gdy żądanie jest asynchroniczne. Więcej informacji znajdziesz w sekcji „Konfigurowanie preferowanego poziomu MTE dla konkretnego procesora”.

MTE w przestrzeni użytkownika

W następnych sekcjach wyjaśniamy, jak włączyć MTE w przypadku procesów systemowych i aplikacji. MTE jest domyślnie wyłączone, chyba że dla danego procesu wybrano jedną z opcji poniżej (poniżejznajdziesz informacje o tym, dla których komponentów MTE jest włączone).

Włączanie MTE za pomocą systemu kompilacji

Jako właściwość dotyczącą całego procesu, MTE jest kontrolowana przez ustawienie czasu kompilacji głównego pliku wykonywalnego. Te opcje umożliwiają zmianę tego ustawienia w przypadku poszczególnych plików wykonywalnych lub całych podkatalogów w drzewie źródłowym. To ustawienie jest ignorowane w przypadku bibliotek i dowolnego celu, który nie jest ani plik wykonywalny, ani test.

1. Włączanie MTE w Android.bp (przykład) w przypadku konkretnego projektu:

Tryb MTE Ustawienie
Asynchroniczne MTE
  sanitize: {
  memtag_heap: true,
  }
Synchroniczny MTE
  sanitize: {
  memtag_heap: true,
  diag: {
  memtag_heap: true,
  },
  }

lub Android.mk:

Tryb MTE Ustawienie
Asynchronous MTE LOCAL_SANITIZE := memtag_heap
Synchronous MTE LOCAL_SANITIZE := memtag_heap
LOCAL_SANITIZE_DIAG := memtag_heap

2. Włączanie MTE w podkatalogu w drzewie źródeł za pomocą zmiennej produktu:

Tryb MTE Lista uwzględnień Lista wykluczeń
async PRODUCT_MEMTAG_HEAP_ASYNC_INCLUDE_PATHS MEMTAG_HEAP_ASYNC_INCLUDE_PATHS PRODUCT_MEMTAG_HEAP_EXCLUDE_PATHS MEMTAG_HEAP_EXCLUDE_PATHS
synchronizacja PRODUCT_MEMTAG_HEAP_SYNC_INCLUDE_PATHS MEMTAG_HEAP_SYNC_INCLUDE_PATHS

lub

Tryb MTE Ustawienie
Asynchroniczne MTE MEMTAG_HEAP_ASYNC_INCLUDE_PATHS
Synchroniczny MTE MEMTAG_HEAP_SYNC_INCLUDE_PATHS

lub przez określenie ścieżki wykluczenia pliku wykonywalnego:

Tryb MTE Ustawienie
Asynchroniczne MTE PRODUCT_MEMTAG_HEAP_EXCLUDE_PATHS MEMTAG_HEAP_EXCLUDE_PATHS
Synchroniczny MTE

Przykład (podobny do PRODUCT_CFI_INCLUDE_PATHS)

  PRODUCT_MEMTAG_HEAP_SYNC_INCLUDE_PATHS=vendor/$(vendor)
  PRODUCT_MEMTAG_HEAP_EXCLUDE_PATHS=vendor/$(vendor)/projectA \
                                    vendor/$(vendor)/projectB

Włączanie MTE za pomocą właściwości systemowych

Powyższe ustawienia kompilacji można zastąpić w czasie wykonywania, ustawiając tę właściwość systemu:

arm64.memtag.process.<basename> = (off|sync|async)

Gdzie basename oznacza podstawową nazwę pliku wykonywalnego.

Na przykład, aby ustawić /system/bin/ping lub /data/local/tmp/ping, aby użyć asynchronicznego MTE, użyj adb shell setprop arm64.memtag.process.ping async.

Włączanie MTE za pomocą zmiennej środowiskowej

Innym sposobem zastąpienia ustawienia kompilacji jest zdefiniowanie zmiennej środowiskowej: MEMTAG_OPTIONS=(off|sync|async)Jeśli zdefiniowana jest zarówno zmienna środowiskowa, jak i właściwość systemowa, pierwszeństwo ma zmienna środowiskowa.

Włączanie MTE w aplikacjach

Jeśli nie jest określone, MTE jest domyślnie wyłączone, ale aplikacje, które chcą korzystać z MTE, mogą to zrobić, ustawiając wartość android:memtagMode w tagu <application> lub <process> w pliku AndroidManifest.xml.

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

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

Aby przetestować zmiany zgodności, możesz 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).
Zmiany te można znaleźć w menu ustawień globalnych na poziomie System > Advanced > Developer options > App Compatibility Changes. Ustawienie NATIVE_MEMTAG_ASYNC lub NATIVE_MEMTAG_SYNC włącza MTE dla konkretnej aplikacji.
Możesz też ustawić to ustawienie za pomocą polecenia am w następujący sposób:

$ adb shell am compat enable NATIVE_MEMTAG_[A]SYNC my.app.name

Tworzenie obrazu systemu MTE

Zdecydowanie zalecamy włączenie MTE we wszystkich natywnych plikach binarnych podczas tworzenia i testowania aplikacji. Pomaga to w wczesnym wykrywaniu błędów związanych z bezpieczeństwem pamięci i zapewnia realistyczne pokrycie użytkowników, jeśli jest włączone w kompilacji testowej.

Podczas tworzenia zalecamy włączanie MTE w trybie synchronicznym w przypadku wszystkich natywnych plików binarnych.

SANITIZE_TARGET=memtag_heap SANITIZE_TARGET_DIAG=memtag_heap m

Podobnie jak w przypadku innych zmiennych w systemie kompilacji, SANITIZE_TARGET może być używana jako zmienna środowiskowa lub ustawienie make (np. w pliku product.mk ).
Pamiętaj, że ta opcja włącza MTE dla wszystkich procesów natywnych, ale nie dla aplikacji (które są pochodnymi aplikacji zygote64), dla których można włączyć MTE zgodnie z instrukcjami podanymi wyżej.

Konfigurowanie preferowanego poziomu MTE w zależności od procesora

W przypadku niektórych procesorów wydajność MTE w trybie ASYMM lub nawet SYNC może być podobna do wydajności ASYNC. Dlatego warto włączyć bardziej rygorystyczne kontrole na tych procesorach, gdy żądany jest mniej rygorystyczny tryb sprawdzania, aby uzyskać korzyści z wykrywania błędów w ramach bardziej rygorystycznych kontroli bez negatywnego wpływu na wydajność.
Domyślnie procesy skonfigurowane do działania w trybie ASYNC będą działać w tym trybie na wszystkich procesorach. Aby skonfigurować jądro do uruchamiania tych procesów w trybie synchronizacji na określonych procesorach, podczas rozruchu systemu należy zapisać wartość synchronizacji w rekordzie sysfs /sys/devices/system/cpu/cpu<N>/mte_tcf_preferred. Można to zrobić za pomocą skryptu inicjującego. Na przykład, aby skonfigurować procesory 0–1 do uruchamiania procesów w trybie ASYNC w trybie SYNC, a procesory 2–3 do uruchamiania w trybie ASYMM, do klauzuli init skryptu init dostawcy można dodać następujący kod:

  write /sys/devices/system/cpu/cpu0/mte_tcf_preferred sync
  write /sys/devices/system/cpu/cpu1/mte_tcf_preferred sync
  write /sys/devices/system/cpu/cpu2/mte_tcf_preferred asymm
  write /sys/devices/system/cpu/cpu3/mte_tcf_preferred asymm

Nagrobki z procesów w trybie asynchronicznym działających w trybie synchronicznym będą zawierać dokładny ślad stosu z lokalizacją błędu pamięci. Nie zawierają jednak informacji o przypisaniu i odpisaniu pamięci. Te ścieżki wywołań są dostępne tylko wtedy, gdy proces jest skonfigurowany do działania w trybie SYNC.

int mallopt(M_THREAD_DISABLE_MEM_INIT, level)

gdzie level to 0 lub 1.
Wyłącza inicjalizację pamięci w malloc i unika zmiany znaczników pamięci, chyba że jest to konieczne dla prawidłowego działania.

int mallopt(M_MEMTAG_TUNING, level)

gdzie level to:

  • M_MEMTAG_TUNING_BUFFER_OVERFLOW
  • M_MEMTAG_TUNING_UAF

Wybiera strategię przypisywania tagów.

  • Ustawienie domyślne to M_MEMTAG_TUNING_BUFFER_OVERFLOW.
  • M_MEMTAG_TUNING_BUFFER_OVERFLOW – umożliwia deterministyczne wykrywanie błędów przepełnienia i niedopełnienia bufora liniowego poprzez przypisywanie sąsiadującym alokacjom odrębnych wartości tagów. W tym trybie wykrywanie błędów związanych z użyciem po zwolnieniu pamięci jest nieco mniej prawdopodobne, ponieważ dla każdej lokalizacji pamięci dostępna jest tylko połowa możliwych wartości tagów. Pamiętaj, że MTE nie może wykryć przepełnienia w ramach tego samego ziarna tagu (wyrównanego fragmentu o długości 16 bajtów) i może pomijać małe przepełnienia nawet w tym trybie. Takie przepełnienie nie może być przyczyną uszkodzenia pamięci, ponieważ pamięć w ramach jednego ziarna nigdy nie jest używana do wielu alokacji.
  • M_MEMTAG_TUNING_UAF – umożliwia losowanie tagów niezależnie od siebie, co daje jednolite prawdopodobieństwo wykrycia błędów o wielokrotności około 93%, zarówno przestrzennych (przepełnienie bufora), jak i czasowych (możliwość użycia po wygaśnięciu).

Oprócz opisanych wyżej interfejsów API doświadczeni użytkownicy powinni wiedzieć o tych kwestiach:

  • Ustawienie rejestru sprzętowego PSTATE.TCO może tymczasowo wyłączyć sprawdzanie tagów (przykład). Przykładem może być kopiowanie zakresu pamięci z nieznanym zawartością tagu lub rozwiązywanie problemu z wąskim gardłem wydajności w pętli gorącej.
  • Gdy używasz M_HEAP_TAGGING_LEVEL_SYNC, system obsługi awarii udostępnia dodatkowe informacje, takie jak ścieżki alokacji i deallokwacji. Ta funkcja wymaga dostępu do bitów tagu i jest włączana przez przekazanie flagi SA_EXPOSE_TAGBITS podczas ustawiania modułu obsługi sygnału. Zaleca się, aby każdy program, który ma swój własny sygnał obsługi i przekazuje nieznane awarie do obsługi przez system, robił to samo.

MTE w jądrze

Aby włączyć KASAN z przyspieszeniem MTE dla jądra, skonfiguruj jądro za pomocą opcji CONFIG_KASAN=y i CONFIG_KASAN_HW_TAGS=y. Te konfiguracje są domyślnie włączone w jądrach GKI, począwszy od wersji Android 12-5.10.
Można to kontrolować podczas uruchamiania za pomocą tych argumentów wiersza poleceń:

  • kasan=[on|off] – włączanie i wyłączanie KASAN (domyślnie: on).
  • kasan.mode=[sync|async] – wybór trybu synchronicznego lub asynchronicznego (domyślnie: sync).
  • kasan.stacktrace=[on|off] – czy zbierać ścieżki wywołań (domyślnie: on)
    • Zbieranie ścieżki do wywołania wymaga również:stack_depot_disable=off.
  • kasan.fault=[report|panic] – czy raport ma być tylko wydrukowany, czy też ma wywołać panikę w rdzeniu (domyślnie: report). Niezależnie od tej opcji sprawdzanie tagów jest wyłączane po pierwszym zgłoszonym błędzie.

Zdecydowanie zalecamy używanie trybu synchronizacji podczas wdrażania, tworzenia i testowania. Ta opcja powinna być włączona globalnie w przypadku wszystkich procesów korzystających z zmiennej środowiskowej lub z systemu kompilacji. W tym trybie błędy są wykrywane na wczesnym etapie procesu programowania, kod źródłowy jest szybciej stabilizowany, a koszt wykrywania błędów na późniejszym etapie produkcji jest mniejszy.

Zdecydowanie zalecamy używanie trybu ASYNC w środowisku produkcyjnym. Jest to narzędzie o małym nakładzie pracy, które umożliwia wykrywanie błędów związanych z bezpieczeństwem pamięci w procesie, a także dalszą ochronę. Po wykryciu błędu deweloper może skorzystać z interfejsów API w czasie wykonywania, aby przejść do trybu synchronizacji i uzyskać dokładny ślad stosu na podstawie próbki użytkowników.

Zdecydowanie zalecamy skonfigurowanie preferowanego poziomu MTE dla SoC w zależności od procesora. Tryb asymetryczny ma zwykle takie same właściwości wydajnościowe jak tryb asynchroniczny i prawie zawsze jest od niego lepszy. Małe rdzenie na zamówienie często osiągają podobną wydajność we wszystkich 3 trybach i można je skonfigurować tak, aby preferowały SYNC.

Deweloperzy powinni sprawdzić, czy nie ma awarii, korzystając z funkcji /data/tombstones, logcat lub monitorując ścieżkę DropboxManager dostawcy pod kątem błędów po stronie użytkownika. Więcej informacji o debugowaniu kodu natywnego na Androida znajdziesz tutaj.

Komponenty platformy z włączonym MTE

W Androidzie 12 wiele ważnych pod względem bezpieczeństwa komponentów systemu korzysta z MTE ASYNC do wykrywania awarii na poziomie użytkownika i działania jako dodatkowa warstwa zabezpieczeń. Są to:

  • Demony i narzędzia sieciowe (z wyjątkiem netd)
  • Bluetooth, SecureElement, interfejsy HAL NFC i aplikacje systemowe
  • statsd demon
  • system_server
  • zygote64 (aby umożliwić aplikacjom korzystanie z MTE)

Te cele zostały wybrane na podstawie tych kryteriów:

  • proces o przywilejach (zdefiniowany jako proces, który ma dostęp do czegoś, do czego domena SELinux unprivileged_app nie ma dostępu);
  • Przetwarzanie niewiarygodnych danych wejściowych (reguła dwóch)
  • Dopuszczalne spowolnienie działania (nie powoduje widocznego dla użytkownika opóźnienia)

Zachęcamy dostawców do włączenia MTE w produkcji w przypadku większej liczby komponentów zgodnie z wymienionymi powyżej kryteriami. Podczas tworzenia zalecamy testowanie tych komponentów w trybie synchronicznym, aby wykrywać łatwo eliminowane błędy i ocenić wpływ asynchroniczności na wydajność.
W przyszłości planujemy rozszerzyć listę komponentów systemu, w których przypadku włączona jest funkcja MTE, kierując się przy tym wydajnością przyszłych projektów sprzętowych.