awh::alloc
Распределитель памяти, заменяющий системный целиком у всего процесса. Не обёртка над malloc и не пул для отдельных нужд: после захвата через него идёт всякая выдача — наша, стандартной библиотеки и чужих библиотек, связанных с программой.
Четыре нужды, которых системный распределитель не закрывает
Учёт расхода
Сколько занято прикладным кодом прямо сейчас, каков был пик, сколько лежит в кэшах, сколько отдано системе. Системный либо молчит, либо отвечает о процессе целиком.
Отдача памяти
Сервер, отдающий страницы посреди наплыва запросов, платит задержкой. Порядок отдачи задаёт тот, кто пишет приложение, а не умолчания чужой библиотеки.
Разбор сбоя
Когда программа падает, надо знать: это нуль, обращение за концом блока, к освобождённому или к чужой памяти. Системный на это не отвечает вовсе.
Защита тайн
Ключам и паролям нужна память, не попадающая в снимок при падении, не уходящая в подкачку и затираемая при возврате. Снаружи такое свойство выдачи не добавить.
Всё это — вызовами, а не переменными окружения: приложение задаёт режим само и меняет его в работе.
Три приёма захвата, по одному на семейство систем
| Приём | Системы | Как работает |
|---|---|---|
| Подмена именами | Linux, все BSD, Solaris, illumos | наш файл определяет malloc, и связыватель ставит его на место системного — захват состоялся до первой строки main |
| Имена и зона | macOS | подмена имён уводит к нам обращения своего образа, а зона malloc_zone_t ловит выданное внутри libsystem |
| Переписывание входа | Windows | вход функций ucrtbase переписывается переходом на наш; выровненная выдача перехвачена отдельно |
Доказано решающим наблюдением: память, выданную внутри стандартной библиотеки (например std::string), щуп находит в нашей области. Пул, подключённый к своему коду, такого не даёт.
Пять слоёв и возможности сбоку
| Слой | Что делает |
|---|---|
| Захват | malloc, free, calloc, realloc, выровненная выдача |
| Cache | кэш потока: свой у потока, замка не берёт вовсе |
| Central | центральные списки, замок у разряда — обмен пачками |
| Pages | страничная куча — обмен областями |
| Source | источник страниц у системы, подменяемый потребителем |
- 62 разряда от 16 байт до 32 768, потеря на округлении не более восьмой доли блока.
- Страница кучи 8 КБ, кусок 4 МБ, выровненный по своему размеру: начало куска находится одной маской.
- Связь свободных блоков — в самих блоках. Указатели перемешиваются зерном процесса: переписав свободный блок, злоумышленник не получит управляемого указателя.
- Быстрый путь — десяток команд без единого замка: кэш потока, разряд таблицей без деления, блок с головы списка, учёт у потока.
Сбоку от лестницы: Huge — крупные выдачи, Guard — заслоны, Profile — учёт мест выдачи, Trace — разбор адреса сбоя.
Чего у системного распределителя нет вовсе
- Разбор адреса сбоя resolve() — нулевая страница, чужая память, живой блок, освобождённый, за концом блока, перед началом.
- Заслоны guardRate — выборочные блоки за закрытыми страницами: выход за границу даёт сбой на месте. Своя ASan, работающая в рабочей сборке.
- Ловля записи по освобождённому — карантин держит освобождённое засеянным; spoiled() называет число испорченных блоков, адрес и смещение.
- Учёт мест выдачи profileRate со съёмом стека — поиск утечек в работающей программе.
- Склад тайн secure() — память, укрытая от снимка, запертая от подкачки и затираемая при возврате. Что состоялось на деле, говорит shelter_t.
- Порядок отдачи памяти — не отдавать, по просьбе, по порогу при освобождении, потоком обслуживания по отсрочке.
- Опрос расхода — шесть величин: занято, пик, взято у системы, свободно в кэшах, в куче, отдано.
- Потолок heapLimit сторожит всё взятое у системы, а не одну кучу.
alloc::options_t options; options.purge = alloc::purge_t::ONFREE; // не отдавать · по просьбе · по порогу · потоком options.heapLimit = 2ull * 1024 * 1024 * 1024; // потолок всего взятого у системы options.hugeCache = 16 * 1024 * 1024; // придержка крупных областей options.guardRate = 1000; // один блок из тысячи — за закрытыми страницами if(!alloc::Allocator::capture(options)) /* захват не состоялся */;
Размеры разнородны — так работает настоящее приложение
Наносекунд на действие, меньше — лучше; размеры разложены по всей полосе разрядов.
| Система | AWH | системный | jemalloc | tcmalloc |
|---|---|---|---|---|
| macOS ARM64 | 2,81 | 11,27 | 10,73 | — |
| Debian x86-64 | 10,04 | 22,24 | 42,56 | 8,45 |
| DragonFly BSD | 11,09 | 26,90 | 650,10 | 9,16 |
| Alpine (musl) | 11,19 | 1 116,07 | 50,98 | 8,61 |
| NetBSD | 11,31 | 34,96 | 35,06 | — |
| FreeBSD | 11,84 | 40,31 | (он же системный) | 9,02 |
| OpenBSD | 21,92 | 2 563,45 | — | — |
jemalloc обойдён везде
От 3,1 до 3,9 раза на смешанной нагрузке, на DragonFly — в 58 раз.
tcmalloc впереди на 7–15 %
Он кладёт разряд в заголовок блока; мы опознаём блок по адресу — это и даёт разбор адреса сбоя.
Ровность — 22 %
Разброс по тринадцати системам 10,1–12,4 нс, у соперников — на порядки.
Рост на месте и придержка крупных областей
Рост перевыдачей на месте
Область растёт за счёт соседних свободных страниц, адрес не меняется. Цепочка удвоений 16 байт → 256 КБ: было 14 750 мкс, стало 3 259 при 2 634 у системного.
Придержка крупных
Свыше 4 МБ цена лежит в первом касании страниц. Освобождённая область придерживается под потолком hugeCache: «выдать и заполнить» 5 МБ — 15 761 → 108 799 МБ/с при 110 000 у системного.
Выключение возможности возвращает её память тут же: нуль в quarantine отдаёт всё, что держал карантин, не дожидаясь следующего освобождения.
Восемь систем, ворошитель и санитайзеры
| Система | Набор | TSan |
|---|---|---|
| macOS 26 | 63/64 | чисто |
| Debian 12 · Alpine · FreeBSD 15 | 49/49 | чисто |
| NetBSD 10.1 | 49/49 | — |
| OpenBSD 7.9 · Solaris 11.4 | 48/49 | — |
| Windows 11 ARM64 | прогнан | — |
| Эльбрус-8С2 (lcc) | перенос идёт | — |
Покрытие строк модуля — 79,65 %, и непокрытое разобрано по причинам: пути снятия распределителя внутри процесса непроверяемы, пути ветвления проверяются только на системах ELF, защитные ветви недостижимы по устройству.
Пропуски — записанные свойства систем: у OpenBSD нет syscall(2), у Sun прямое обращение к нему не поддерживается.
Под санитайзерами захват не состоится, и набор это утверждает: слои проверяются под ними напрямую.