Ваш телефон может обнаруживать повреждение памяти в продакшене. Не с помощью полной инструментации, которую вы запускаете в CI, и не при каждом выделении памяти. Но оборудование в вашем кармане уже несколько лет поставляется с необходимыми примитивами, и растущее число продакшен-приложений тихо включает их.
Краткая версия: AddressSanitizer слишком дорог для продакшена. ARM Memory Tagging Extension — нет. И если вы ещё не можете полагаться на MTE, GWP-ASan даёт вам вероятностное покрытие практически без накладных расходов. Вместе они отвечают на вопрос, который раньше имел удручающий ответ.
Повреждение памяти — это баг, который вы отправляете
Use-after-free, buffer overflow и heap corruption — это баги, которые проходят любой набор тестов. Они живут в нативном коде, плохо воспроизводятся и часто падают где-то далеко от реальной ошибки. К моменту, когда пользователь сообщает о краше, первоначальное повреждение уже произошло, и ваше единственное доказательство — искажённый stack trace или SIGSEGV по адресу, который выглядит случайным.
Традиционные инструменты ловят их рано. Valgrind точен, но медленен. ASan быстрее, но всё же добавляет примерно в 2-3 раза больше накладных расходов памяти и в 2 раза больше накладных расходов процессора. Это нормально для фаззинга или юнит-тестов. Это не нормально для устройства с батарейным питанием, на котором работает Instagram.
Продакшен исторически был слепым пятном. Либо вы воспроизводили баг локально, либо гадали.
Как ARM MTE помечает память на аппаратном уровне
ARM Memory Tagging Extension, представленная в ARMv8.5-A, даёт процессору дешёвый способ проверять корректность указателя при каждом обращении к памяти. Это работает путём присвоения 4-битного тега каждой 16-байтовой грануле памяти и хранения соответствующего тега в старшем байте указателя.
При каждом load или store процессор сравнивает тег указателя с тегом памяти. Если они различаются, оборудование вызывает ошибку. Это не программная проверка. Это происходит в подсистеме памяти, и накладные расходы обычно составляют менее 5%.
Старший байт 64-битного указателя уже игнорировался оборудованием. MTE использует эти биты повторно. Указатель вида 0xb7f0_0000_1234_5678 несёт тег 0xb7. Память по этому адресу должна нести тот же тег, иначе обращение вызовет ошибку.
MTE доступна на устройствах с ARMv8.5-A или новее. Сюда входят Google Pixel 8 и новее, iPhone 15 Pro и новее, а также растущая доля Android-устройств среднего класса. Это не универсально, но уже не является экзотикой.
Ядро предоставляет MTE через флаги prctl. Приложение может запросить синхронную или асинхронную проверку. Синхронный режим немедленно вызывает ошибку при несовпадении тегов. Асинхронный режим ставит ошибку в очередь и доставляет её позже, что дешевле, но немного откладывает обнаружение.
GWP-ASan: когда вы не можете пометить всё
MTE отлична, но требует совместимого оборудования и помеченных выделений во всей вашей кодовой базе. Если вы поставляете продукцию на старые устройства или вам нужна только целевая защита, GWP-ASan — это прагматичная альтернатива.
GWP-ASan расшифровывается как Guarded Write Protection AddressSanitizer. Это сэмплирующий аллокатор, который помещает небольшую долю кучевых выделений в защищённые страницы, окружённые отравленными redzones. Если use-after-free или buffer overflow затрагивает эти redzones, аппаратный MMU немедленно вызывает ошибку.
Ключевая идея — вероятность. GWP-ASan может защищать 1 из 10 000 выделений. Это звучит бесполезно, пока вы не поймёте, что багованное приложение будет повреждать память тысячи раз до краша. При достаточном количестве пользовательских сессий даже сэмплирование в 0,01% ловит реальные баги в продакшене.
Google годами использует GWP-ASan в Chrome и системных сервисах Android. Он обнаружил сотни багов use-after-free, которые ускользнули как от тестирования, так и от фаззинга. Накладные расходы процессора пренебрежимо малы, поскольку лишь крошечная доля выделений проходит через защищённый путь. Накладные расходы памяти ограничены, потому что защищённый пул мал, а выделения в конечном итоге перерабатываются.
Компромиссы, о которых никто не хочет говорить
MTE и GWP-ASan не бесплатны. Они просто достаточно дешёвы, чтобы окупаться.
MTE требует старшего байта каждого указателя, а значит, ваша кодовая база должна компилироваться с -fsanitize=memtag или -march=armv8.5-a+memtag. Если у вас есть код, который маскирует указатели, хеширует их или пропускает через JNI без правильного тегирования, вы получите ложные срабатывания. Android NDK и современные реализации libc корректно обрабатывают это, но кастомные аллокаторы или схемы упаковки указателей сломаются.
MTE также ловит баги только внутри кучи и стека, которые вы явно пометили. Дикий указатель, прыгающий в немаркированный регион mmap, не будет пойман. Это лучше, чем ничего, но не полное покрытие.
GWP-ASan имеет противоположную проблему. Он ловит только те выделения, которые случайно попали в защищённый пул. Баг, повреждающий незащищённое выделение, останется незамеченным. Вы также платите небольшую задержку на выделение и освобождение для сэмплированного набора, и вам нужно корректно обрабатывать краши в вашем конвейере телеметрии.
Ни один из инструментов не заменяет ASan в вашем CI-конвейере. Они дополняют его. ASan даёт детерминированное высокопокрытное обнаружение во время тестирования. MTE и GWP-ASan дают подстраховку в поле.
Включение MTE на современном Android-устройстве
Если у вас Pixel 8 или новее, вы можете протестировать MTE уже сегодня. Android NDK поддерживает его с помощью единственного флага компилятора.
Сначала проверьте, поддерживает ли ваше устройство MTE:
#include <sys/prctl.h>
#include <linux/prctl.h>
#include <sys/auxv.h>
#include <asm/hwcap.h>
bool mte_supported() {
unsigned long hwcap2 = getauxval(AT_HWCAP2);
return (hwcap2 & HWCAP2_MTE) != 0;
}
Затем соберите вашу нативную библиотеку с включённым MTE в вашем CMakeLists.txt:
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=memtag")
Если вы хотите синхронный faulting, что обычно нужно в продакшене, запросите его при старте:
#include <sys/prctl.h>
void enable_mte() {
if (mte_supported()) {
prctl(PR_SET_TAGGED_ADDR_CTRL,
PR_TAGGED_ADDR_ENABLE | PR_MTE_TCF_SYNC | (0xfffe << PR_MTE_TAG_SHIFT),
0, 0, 0);
}
}
Флаг PR_MTE_TCF_SYNC сообщает ядру немедленно вызывать ошибку при несовпадении тегов. Маска тега 0xfffe исключает нулевой тег, который всё ещё используется некоторым системным кодом.
Настройка GWP-ASan для продакшена
GWP-ASan проще включить, потому что он не требует специального оборудования. На Android вы можете слинковаться с обёрткой аллокатора GWP-ASan или включить его через системный аллокатор на более новых уровнях API.
Для минимальной интеграции оберните ваши точки входа аллокатора:
extern "C" void* malloc(size_t size) {
if (__gwp_asan_sample()) {
return __gwp_asan_guarded_malloc(size);
}
return __libc_malloc(size);
}
На практике вы будете использовать Android GWP-ASan runtime или реализацию LLVM compiler-rt. Ключевой параметр конфигурации — частота сэмплирования. Начинайте консервативно:
// Одно защищённое выделение на 10 000
__gwp_asan_set_sample_rate(10000);
Собирайте полученные краши через вашу существующую телеметрию. Краш GWP-ASan выглядит как стандартный SIGSEGV, но faulting-адрес попадёт в защищённую страницу. Ваш crash reporter может обнаружить это, проверив, попадает ли faulting-адрес в пул GWP-ASan.
Что вам действительно следует делать
Если вы поставляете нативный код на мобильных устройствах, вы уже должны запускать ASan в CI и фаззинге. Вопрос в том, что происходит после поставки.
Если ваше минимально поддерживаемое устройство включает чипы ARMv8.5-A, включите MTE в синхронном режиме. Накладные расходы достаточно малы, чтобы пользователи их не заметили, а краши, которые вы получите, будут содержать точную информацию о несовпадении тегов вместо случайного повреждения кучи.
Если вы поддерживаете старые устройства, включите GWP-ASan с консервативной частотой сэмплирования. Он не поймает каждый баг, но поймает баги, которые ничто другое не ловит. На протяжении миллионов сессий это не теория. Так Chrome нашёл use-after-free в своём декодере изображений в прошлом году.
Оборудование вашего телефона уже способно ловить баги безопасности памяти, которых вы больше всего боитесь. Единственный вопрос — включили ли вы его.