Architektura Arm v9 wprowadza rozszerzenie Memory Tagging Extension (MTE), czyli implementację sprzętową pamięci z tagami.
MTE dodaje do każdej alokacji i dealokacji pamięci dodatkowe metadane. Przypisuje tag do lokalizacji pamięci, który można następnie powiązać ze wskaźnikami odwołującymi się do tej lokalizacji. Podczas działania programu 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 związanych z bezpieczeństwem pamięci w naszych bazach kodu.
Tryby pracy 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 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 dopuszczalne są większe wymagania dotyczące wydajności. Gdy ta funkcja jest włączona, MTE SYNC działa jako zabezpieczenie.
W przypadku niezgodności tagu procesor natychmiast przerywa wykonywanie i kończy proces z kodem SIGSEGV
(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 środowisku produkcyjnym, gdy proces docelowy stanowi podatną na ataki powierzchnię. Gdy tryb ASYNC wskaże obecność błędu, dokładny raport o błędzie można uzyskać, używając interfejsów API środowiska wykonawczego do przełączenia wykonywania na tryb SYNC.
W trybie SYNC alokator Androida rejestruje ślady stosu wszystkich alokacji i dealokacji oraz wykorzystuje je do generowania lepszych raportów o błędach, które zawierają wyjaśnienie błędu pamięci, np. odwołania do pamięci po jej zwolnieniu (use-after-free) lub przepełnienia bufora (buffer-overflow), a także ślady stosu odpowiednich zdarzeń związanych z 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 obciążeniu.
W przypadku niezgodności tagów procesor kontynuuje wykonywanie do najbliższego wejścia do jądra (np. wywołania systemowego lub przerwania timera), gdzie kończy proces z błędem 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 występowanie błędów związanych z bezpieczeństwem pamięci jest niewielkie. 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 z pamięci i asynchroniczne sprawdzanie zapisów w pamięci. Wydajność jest podobna do trybu ASYNC. W większości sytuacji ten tryb jest lepszy od trybu ASYNC i zalecamy używanie go zamiast trybu ASYNC, gdy tylko jest dostępny.
Z tego powodu żaden z interfejsów API opisanych poniżej nie wspomina o trybie Asymmetric. Zamiast tego system operacyjny można skonfigurować tak, aby zawsze używał trybu asymetrycznego, gdy wymagany jest tryb asynchroniczny. Więcej informacji znajdziesz w sekcji „Konfigurowanie preferowanego poziomu MTE dla konkretnego 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, dla których komponentów MTE jest włączone, poniżej).
Włączanie MTE za pomocą systemu kompilacji
Jako właściwość całego procesu MTE jest kontrolowane przez ustawienie czasu kompilacji głównego pliku wykonywalnego. 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 i wszystkich elementów docelowych, które nie są plikami wykonywalnymi ani testami.
1. Włączanie MTE w 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 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 uwzględnień | 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 podając ścieżkę wykluczenia pliku wykonywalnego:
Tryb MTE | Ustawienie |
---|---|
Asynchroniczne MTE | PRODUCT_MEMTAG_HEAP_EXCLUDE_PATHS
MEMTAG_HEAP_EXCLUDE_PATHS |
Synchroniczne MTE |
Przykład (podobne zastosowanie jak w przypadku 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 ten atrybut systemowy:
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 adb shell setprop arm64.memtag.process.ping async
.
Włączanie MTE za pomocą zmiennej środowiskowej
Innym sposobem zastąpienia ustawienia kompilacji dla 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 zostanie określona, funkcja MTE jest domyślnie wyłączona, ale aplikacje, które chcą jej używać, mogą to zrobić, ustawiając android:memtagMode
w tagu <application>
lub <process>
w AndroidManifest.xml
.
android:memtagMode=(off|default|sync|async)
Jeśli atrybut zostanie ustawiony w tagu <application>
, będzie wpływać na wszystkie procesy używane przez aplikację. Można go zastąpić w przypadku poszczególnych procesów, ustawiając tag <process>
.
W przypadku eksperymentów można użyć zmian zgodności, aby ustawić wartość domyślną atrybutu memtagMode
w przypadku aplikacji, która nie określa żadnej wartości w pliku manifestu (lub określa wartość default
).
Można je znaleźć w sekcji System > Advanced > Developer options
> App Compatibility Changes
w menu ustawień globalnych. Ustawienie NATIVE_MEMTAG_ASYNC
lub NATIVE_MEMTAG_SYNC
włącza MTE w przypadku konkretnej aplikacji.
Możesz też ustawić to za pomocą polecenia am
w ten 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 programowania i uruchamiania. Pomaga to wczesne wykrywanie błędów związanych z bezpieczeństwem pamięci i zapewnia realistyczne pokrycie użytkowników, jeśli jest włączone w wersjach testowych.
Zdecydowanie zalecamy włączenie MTE w trybie synchronicznym we wszystkich natywnych plikach binarnych podczas programowania.
SANITIZE_TARGET=memtag_heap SANITIZE_TARGET_DIAG=memtag_heap m
Podobnie jak w przypadku innych zmiennych w systemie kompilacji, zmiennej SANITIZE_TARGET
można używać jako zmiennej środowiskowej lub ustawienia make
(np. w product.mk
pliku).
Pamiętaj, że włącza to MTE dla wszystkich procesów natywnych, ale nie dla aplikacji (które są rozwidleniem zygote64
), w przypadku których MTE można włączyć zgodnie z instrukcjami powyżej.
Konfigurowanie preferowanego poziomu MTE dla konkretnego procesora
W przypadku niektórych procesorów wydajność MTE w trybach ASYMM, a nawet SYNC może być podobna do wydajności w trybie ASYNC. Dlatego warto włączyć bardziej rygorystyczne sprawdzanie tych procesorów, gdy wymagany jest mniej rygorystyczny tryb sprawdzania, aby 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 będą działać w tym trybie na wszystkich procesorach. Aby skonfigurować jądro tak, aby uruchamiało te procesy w trybie SYNC na określonych procesorach, wartość sync musi zostać zapisana w pozycji sysfs
/sys/devices/system/cpu/cpu<N>/mte_tcf_preferred
podczas uruchamiania. Można to zrobić za pomocą skryptu inicjującego. Aby na przykład 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 inicjowania dostawcy można dodać 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
Pliki z informacjami o błędach z procesów w trybie ASYNC uruchomionych w trybie SYNC będą zawierać dokładny ślad stosu z informacjami o lokalizacji błędu pamięci. Nie będą jednak zawierać śladu stosu przydzielania ani zwalniania pamięci. Te ślady stosu są dostępne tylko wtedy, gdy proces jest skonfigurowany do działania w trybie SYNCHRONICZNYM.
int mallopt(M_THREAD_DISABLE_MEM_INIT, level)
gdzie level
to 0 lub 1.
Wyłącza inicjowanie pamięci w funkcji malloc i zapobiega zmianie tagów pamięci
o ile nie 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ę przydzielania tagów.
- Ustawienie domyślne to
M_MEMTAG_TUNING_BUFFER_OVERFLOW
. M_MEMTAG_TUNING_BUFFER_OVERFLOW
– włącza deterministyczne wykrywanie błędów przepełnienia i niedopełnienia bufora liniowego przez przypisywanie różnych wartości tagów do sąsiednich przydziałów. W tym trybie prawdopodobieństwo wykrycia błędów typu use-after-free jest nieco mniejsze, ponieważ w przypadku 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 (16-bajtowego bloku wyrównanego) i może przeoczyć niewielkie przepełnienia nawet w tym trybie. Takie przepełnienie nie może być przyczyną uszkodzenia pamięci, ponieważ pamięć w jednym ziarnie nigdy nie jest używana do wielu alokacji.M_MEMTAG_TUNING_UAF
– włącza niezależnie randomizowane tagi, które zapewniają równomierne prawdopodobieństwo wykrycia błędów przestrzennych (przepełnienie bufora) i czasowych (użycie po zwolnieniu pamięci) na poziomie około 93%.
Oprócz opisanych powyżej interfejsów API zaawansowani użytkownicy mogą chcieć poznać te:
- Ustawienie rejestru sprzętowego może tymczasowo wstrzymać sprawdzanie tagów (przykład).
PSTATE.TCO
Na przykład podczas kopiowania zakresu pamięci o nieznanej zawartości tagu lub rozwiązywania problemu z wydajnością w pętli. - Gdy używasz
M_HEAP_TAGGING_LEVEL_SYNC
, program obsługi awarii systemu dostarcza dodatkowe informacje, takie jak ślady stosu alokacji i dealokacji. Ta funkcja wymaga dostępu do bitów tagu i jest włączana przez przekazanie flagiSA_EXPOSE_TAGBITS
podczas ustawiania procedury obsługi sygnału. Każdy program, który ustawia własny moduł obsługi sygnałów i przekazuje nieznane awarie do systemu, powinien postępować w ten sam sposób.
MTE w jądrze
Aby włączyć w jądrze przyspieszoną przez MTE funkcję KASAN, skonfiguruj jądro za pomocą poleceń CONFIG_KASAN=y
i 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]
– wybierz tryb synchroniczny lub asynchroniczny (domyślnie:sync
)kasan.stacktrace=[on|off]
– czy zbierać ślady stosu (domyślnie:on
).- Zbieranie śladu stosu wymaga też
stack_depot_disable=off
.
- Zbieranie śladu stosu wymaga też
kasan.fault=[report|panic]
– czy wydrukować tylko raport, czy też spowodować panikę jądra (domyślnie:report
). Niezależnie od tej opcji sprawdzanie tagów jest wyłączane po pierwszym zgłoszonym błędzie.
Zalecane użycie
Zdecydowanie zalecamy korzystanie z trybu SYNC podczas uruchamiania, programowania i testowania. Ta opcja powinna być włączona globalnie dla wszystkich procesów korzystających ze 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ą unikane.
Zdecydowanie zalecamy używanie w środowisku produkcyjnym trybu ASYNC. Jest to narzędzie o niskim narzucie, które wykrywa błędy związane z bezpieczeństwem pamięci w procesie, a także zapewnia dodatkową ochronę. Po wykryciu błędu deweloper może użyć interfejsów API środowiska wykonawczego, aby przełączyć się na tryb SYNCHRONICZNY i uzyskać dokładny ślad stosu od wybranej grupy użytkowników.
Zdecydowanie zalecamy skonfigurowanie preferowanego poziomu MTE dla procesora w przypadku układu SoC. Tryb asymetryczny ma zwykle takie same charakterystyki wydajności jak tryb ASYNC i niemal zawsze jest od niego lepszy. Małe rdzenie w kolejności wykonywania instrukcji często wykazują podobną wydajność we wszystkich 3 trybach i można je skonfigurować tak, aby preferowały tryb SYNC.
Deweloperzy powinni sprawdzać, czy występują awarie, korzystając z /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 na Androida znajdziesz tutaj.
Komponenty platformy z włączonym rozszerzeniem MTE
W Androidzie 12 wiele kluczowych dla bezpieczeństwa komponentów systemu używa MTE ASYNC do wykrywania awarii u użytkowników i stanowi dodatkową warstwę ochrony. Te komponenty to:
- demony i narzędzia sieciowe (z wyjątkiem
netd
); - Bluetooth, SecureElement, HAL NFC i aplikacje systemowe
statsd
demonasystem_server
zygote64
(aby umożliwić aplikacjom korzystanie z MTE)
Te miejsca docelowe zostały wybrane na podstawie tych kryteriów:
- proces uprzywilejowany (zdefiniowany jako proces, który ma dostęp do czegoś, do czego nie ma dostępu domena SELinux unprivileged_app);
- Przetwarzanie niewiarygodnych danych wejściowych (zasada dwóch)
- Dopuszczalne spowolnienie działania (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 SYNCHRONICZNYM, aby wykrywać łatwe do naprawienia błędy i oceniać wpływ trybu ASYNCHRONICZNEGO na ich wydajność.
W przyszłości planujemy rozszerzyć listę komponentów systemu Android, w których włączona jest funkcja MTE. Będziemy się przy tym kierować charakterystyką wydajnościową przyszłych projektów sprzętu.