Rozszerzenie o oznaczanie pamięci

W architekturze Arm v9 wprowadzono rozszerzenie Arm Memory Tagging Extension (MTE), czyli sprzętową implementację pamięci z tagami.

W dużym uproszczeniu MTE taguje każdą alokację i dealokację pamięci dodatkowymi metadanymi. MTE przypisuje tag do lokalizacji pamięci, który można powiązać ze wskaźnikami odwołującymi się do tej lokalizacji. Podczas działania procesor sprawdza, czy wskaźnik i tagi metadanych pasują do siebie przy każdym wczytaniu i zapisie.

W Androidzie 12 alokator pamięci sterty jądra i przestrzeni użytkownika może uzupełniać każdą alokację o metadane. Pomaga to wykrywać błędy typu use-after-free i przepełnienia bufora, które są najczęstszym źródłem błędów bezpieczeństwa pamięci w naszych bazach kodu.

Tryby działania MTE

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 dokładności wykrywania błędów, a nie wydajności. Można go używać jako precyzyjnego narzędzia do wykrywania błędów, gdy dopuszczalny jest większy narzut na wydajność. Gdy jest włączony, MTE SYNC działa jako zabezpieczenie. W przypadku niezgodności tagów procesor natychmiast przerywa wykonywanie i kończy proces za pomocą SIGSEGV (kod SEGV_MTESERR) oraz pełnych informacji o dostępie do pamięci i adresie błędu.

Zalecamy używanie tego trybu podczas testowania jako alternatywy dla HWASan/KASAN lub w środowisku produkcyjnym, gdy proces docelowy stanowi podatną na ataki powierzchnię. Ponadto, gdy tryb ASYNC wskaże obecność błędu, można uzyskać dokładny raport o błędzie, używając interfejsów API środowiska wykonawczego do przełączenia wykonywania na tryb SYNC.

Gdy działa w trybie SYNC, alokator Androida rejestruje zrzuty stosu dla wszystkich alokacji i dealokacji oraz używa ich do generowania lepszych raportów o błędach, które zawierają wyjaśnienie błędu pamięci, np. błędu typu use-after-free lub przepełnienia bufora, oraz zrzuty 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 bezpieczeństwa pamięci z niskim narzutem. W przypadku niezgodności tagów procesor kontynuuje wykonywanie aż do najbliższego wejścia do jądra (np. wywołania systemowego lub przerwania timera), gdzie kończy proces za pomocą SIGSEGV (kod SEGV_MTEAERR) bez rejestrowania adresu błędu ani dostępu do pamięci.

Zalecamy używanie tego trybu w środowisku produkcyjnym w przypadku dobrze przetestowanych baz kodu, w których wiadomo, że występuje niewielka gęstość błędów bezpieczeństwa pamięci. Można to osiągnąć, używając trybu SYNC podczas testowania.

Tryb asymetryczny (ASYMM)

Dodatkowa funkcja w architekturze Arm v8.7-A, czyli asymetryczny tryb MTE, zapewnia synchroniczne sprawdzanie odczytów pamięci i asynchroniczne sprawdzanie zapisów pamięci. Jego wydajność jest podobna do wydajności trybu ASYNC. W większości sytuacji ten tryb jest lepszy od trybu ASYNC i zalecamy używanie go zamiast trybu ASYNC, gdy jest dostępny.

Z tego powodu żaden z opisanych poniżej interfejsów API nie wspomina o trybie asymetrycznym. Zamiast tego system operacyjny można skonfigurować tak, aby zawsze używał trybu asymetrycznego, gdy wymagany jest tryb asynchroniczny. Więcej informacji znajdziesz w artykule o konfigurowaniu preferowanego poziomu MTE dla danego procesora.

MTE w przestrzeni użytkownika

W sekcjach poniżej opisujemy, jak włączyć MTE w przypadku procesów systemowych i aplikacji. MTE jest domyślnie wyłączone, chyba że dla danego procesu ustawiono jedną z opcji poniżej (zobacz, w przypadku których komponentów MTE jest włączone poniżej).

Włączanie MTE za pomocą systemu kompilacji

Jako właściwość obejmująca cały proces MTE jest kontrolowane przez ustawienie głównego pliku wykonywalnego w czasie kompilacji. Poniższe opcje umożliwiają zmianę tego ustawienia w przypadku poszczególnych plików wykonywalnych lub całych podkatalogów w drzewie źródłowym. Ustawienie jest ignorowane w przypadku bibliotek lub dowolnego celu, który nie jest plikiem wykonywalnym ani a testem.

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

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

lub w pliku 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ódłowym za pomocą zmiennej produktu:

Tryb MTE Lista dołączeń Lista wykluczeń
asynchroniczny 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
Synchroniczne 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
Synchroniczne MTE

Przykład (podobny do użycia 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 systemu

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

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

Gdzie basename to nazwa podstawowa pliku wykonywalnego.

Aby na przykład ustawić /system/bin/ping lub /data/local/tmp/ping na używanie asynchronicznego MTE, użyj polecenia adb shell setprop arm64.memtag.process.ping async.

Włączanie MTE za pomocą zmiennej środowiskowej

Innym sposobem na zastąpienie ustawienia kompilacji w przypadku procesów natywnych (nie aplikacji) jest zdefiniowanie zmiennej środowiskowej: MEMTAG_OPTIONS=(off|sync|async) Jeśli zdefiniowano zarówno zmienną środowiskową, jak i właściwość systemu, zmienna ma pierwszeństwo.

Włączanie MTE w aplikacjach

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

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

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

Na potrzeby eksperymentowania można użyć zmian zgodności, aby ustawić domyślną wartość atrybutu memtagMode w przypadku aplikacji, która nie określa żadnej wartości w manifeście (lub określa wartość default).

Znajdują się one w menu ustawień globalnych w sekcji System > Zaawansowane > Opcje programisty > Zmiany zgodności aplikacji. Ustawienie NATIVE_MEMTAG_ASYNC lub NATIVE_MEMTAG_SYNC włącza MTE w przypadku konkretnej aplikacji. Można to też zrobić za pomocą polecenia am w ten sposób:

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

Kompilowanie obrazu systemu z włączonym MTE

Zdecydowanie zalecamy włączenie MTE we wszystkich binarnych plikach natywnych podczas tworzenia i uruchamiania. Pomaga to wykrywać błędy bezpieczeństwa pamięci na wczesnym etapie i zapewnia realistyczny zasięg użytkowników, jeśli jest włączone w kompilacjach testowych.

Zdecydowanie zalecamy włączenie MTE w trybie synchronicznym we wszystkich binarnych plikach natywnych podczas tworzenia.

SANITIZE_TARGET=memtag_heap SANITIZE_TARGET_DIAG=memtag_heap m

Podobnie jak w przypadku każdej zmiennej w systemie kompilacji, SANITIZE_TARGET można używać jako zmiennej środowiskowej lub ustawienia make (np. w pliku product.mk).

Włącza to MTE we wszystkich procesach natywnych, ale nie w aplikacjach (które są rozwidleniem procesu zygote64). W ich przypadku MTE można włączyć, wykonując instrukcje opisane w sekcji Włączanie MTE w aplikacjach.

Konfigurowanie preferowanego poziomu MTE dla danego procesora

W przypadku niektórych procesorów wydajność MTE w trybie ASYMM, a nawet SYNC, może być podobna do wydajności trybu ASYNC. Dlatego warto włączyć bardziej rygorystyczne sprawdzanie na tych procesorach, gdy wymagany jest mniej rygorystyczny tryb sprawdzania. Pozwoli to uzyskać korzyści z wykrywania błędów przez bardziej rygorystyczne sprawdzanie bez negatywnego wpływu na wydajność.

Domyślnie procesy skonfigurowane do działania w trybie ASYNC działają w tym trybie na wszystkich procesorach. Aby skonfigurować jądro tak, aby uruchamiało te procesy w trybie SYNC na określonych procesorach, należy zapisać wartość sync w sysfs wpisie /sys/devices/system/cpu/cpu<N>/mte_tcf_preferred podczas uruchamiania systemu. Można to zrobić za pomocą skryptu init. Aby na przykład skonfigurować procesory 0–1 tak, aby uruchamiały procesy w trybie ASYNC w trybie SYNC, a procesory 2–3 tak, aby działały w trybie ASYMM, można dodać do klauzuli init skryptu init dostawcy te wiersze:

  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

Tombstones z procesów w trybie ASYNC działających w trybie SYNC zawierają a dokładny zrzut stosu lokalizacji błędu pamięci. Nie zawierają jednak zrzutu stosu alokacji ani dealokacji. Te zrzuty stosu 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 inicjowanie pamięci w malloc i unika zmiany tagów pamięci chyba że jest to konieczne do prawidłowego działania.

int mallopt(M_MEMTAG_TUNING, level)

gdzie level to:

  • M_MEMTAG_TUNING_BUFFER_OVERFLOW
  • M_MEMTAG_TUNING_UAF

Wybiera strategię alokacji tagów.

  • Ustawienie domyślne to M_MEMTAG_TUNING_BUFFER_OVERFLOW.
  • M_MEMTAG_TUNING_BUFFER_OVERFLOW umożliwia deterministyczne wykrywanie liniowych błędów przepełnienia i niedopełnienia bufora przez przypisywanie różnych wartości tagów do sąsiednich alokacji. Ten tryb ma nieco mniejszą szansę na wykrycie błędów typu use-after-free, ponieważ dla każdej lokalizacji pamięci dostępna jest tylko połowa możliwych wartości tagów. MTE nie może wykryć przepełnienia w obrębie tego samego ziarna tagu (wyrównanego do 16 bajtów bloku) i może przeoczyć małe przepełnienia nawet w tym trybie. Takie przepełnienie nie może być przyczyną uszkodzenia pamięci, ponieważ pamięć w obrębie jednego ziarna nigdy nie jest używana do wielu alokacji.
  • M_MEMTAG_TUNING_UAF umożliwia niezależne losowe tagi które zapewniają równomierne prawdopodobieństwo wykrycia zarówno błędów przestrzennych (przepełnienie bufora), jak i czasowych (use-after-free) na poziomie około 93%.

Oprócz opisanych powyżej interfejsów API doświadczeni użytkownicy mogą też zainteresować się tymi informacjami:

  • Ustawienie rejestru sprzętowegoPSTATE.TCO może tymczasowo wyłączyć sprawdzanie tagów (przykład). Na przykład podczas kopiowania zakresu pamięci o nieznanej zawartości tagów lub rozwiązywania wąskiego gardła wydajności w pętli.
  • Gdy używasz M_HEAP_TAGGING_LEVEL_SYNC, program obsługi awarii systemu udostępnia dodatkowe informacje, takie jak zrzuty stosu alokacji i dealokacji. Ta funkcja wymaga dostępu do bitów tagów i jest włączana przez przekazanie flagi SA_EXPOSE_TAGBITS podczas ustawiania programu obsługi sygnałów. Każdy program, który ustawia własny program obsługi sygnałów i przekazuje nieznane awarie do programu systemowego, powinien postępować tak samo.

MTE w jądrze

Aby włączyć przyspieszany przez MTE KASAN w jądrze, skonfiguruj jądro za pomocą ustawień CONFIG_KASAN=y, CONFIG_KASAN_HW_TAGS=y. Te konfiguracje są domyślnie włączone w przypadku jąder GKI, począwszy od wersji Android 12-5.10.

Możesz to kontrolować podczas uruchamiania za pomocą tych argumentów wiersza poleceń:

  • kasan=[on|off] – włącza lub wyłącza KASAN (domyślnie: on)
  • kasan.mode=[sync|async] – wybiera tryb synchroniczny lub asynchroniczny (domyślnie: sync)
  • kasan.stacktrace=[on|off] – określa, czy zbierać zrzuty stosu (domyślnie: on)
    • zbieranie zrzutów stosu wymaga też ustawienia stack_depot_disable=off.
  • kasan.fault=[report|panic] – określa, czy tylko drukować raport, czy też powodować panikę jądra (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 SYNC podczas uruchamiania, tworzenia i testowania. Tę opcję należy włączyć globalnie w przypadku wszystkich procesów za pomocą zmiennej środowiskowej lub systemu kompilacji. W tym trybie błędy są wykrywane na wczesnym etapie procesu tworzenia, baza kodu jest szybciej stabilizowana, a koszty wykrywania błędów na późniejszym etapie produkcji są niższe.

Zdecydowanie zalecamy używanie trybu ASYNC w środowisku produkcyjnym. Zapewnia to narzędzie z niskim narzutem do wykrywania błędów bezpieczeństwa pamięci w procesie oraz dodatkową warstwę ochrony. Po wykryciu błędu deweloper może użyć interfejsów API środowiska wykonawczego, aby przełączyć się na tryb SYNC i uzyskać dokładny zrzut stosu z próbki użytkowników.

Zdecydowanie zalecamy skonfigurowanie preferowanego poziomu MTE dla danego procesora w przypadku SoC. Tryb ASYMM ma zwykle takie same parametry wydajności jak tryb ASYNC, i prawie zawsze jest od niego lepszy. Małe rdzenie in-order często wykazują podobną wydajność we wszystkich 3 trybach i można je skonfigurować tak, aby preferowały tryb SYNC.

Deweloperzy powinni sprawdzać występowanie awarii, sprawdzając /data/tombstones, logcat lub monitorując potok DropboxManager dostawcy pod kątem błędów użytkowników. Więcej informacji o debugowaniu kodu natywnego Androida znajdziesz informacje tutaj.

Komponenty platformy z włączonym MTE

W Androidzie 12 wiele krytycznych z punktu widzenia bezpieczeństwa komponentów systemu używa MTE ASYNC do wykrywania awarii użytkowników i jako dodatkowej warstwy ochrony. Te komponenty to:

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

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

  • proces z uprawnieniami (zdefiniowany jako proces, który ma dostęp do czegoś do czego nie ma dostępu domena SELinux unprivileged_app),
  • procesy, które przetwarzają niezaufane dane wejściowe (zasada dwóch),
  • dopuszczalne spowolnienie wydajności (spowolnienie nie powoduje opóźnienia widocznego dla użytkownika ).

Zachęcamy dostawców do włączania MTE w środowisku produkcyjnym w przypadku większej liczby komponentów, zgodnie z kryteriami wymienionymi powyżej. Podczas tworzenia zalecamy testowanie tych komponentów w trybie SYNC, aby wykrywać łatwe do naprawienia błędy i oceniać wpływ trybu ASYNC na ich wydajność.

Planujemy rozszerzyć listę komponentów systemu, w których włączono MTE , kierując się charakterystyką wydajności nadchodzących projektów sprzętowych.