Санитайзеры C/C++: ASan, UBSan и TSan под нагрузкой
Тест зелёный, а в проде сегфолт раз в сутки. Разбираем, как ASan, UBSan и TSan ловят то, что не видит компилятор, и чем платят за это по памяти и скорости.

Знакомый сценарий: юнит-тесты зелёные, статический анализатор молчит, а прод падает с SIGSEGV раз в сутки под пиковой нагрузкой. Стек в дампе бесполезен — память уже затёрта, и настоящее место ошибки ушло за сотни строк до падения. Санитайзеры clang и gcc — инструментация во время компиляции, которая ловит эти баги в момент повреждения, а не в момент, когда оно всплыло. Речь о трёх основных: AddressSanitizer (ASan), UndefinedBehaviorSanitizer (UBSan) и ThreadSanitizer (TSan).
Материал про то, как они работают внутри, что каждый ловит, а что пропускает, и сколько это стоит по памяти и скорости. Разбор на clang 18 и gcc 14 — в обоих санитайзеры доступны через одинаковые флаги -fsanitize=.
Что ловит каждый и как это устроено
ASan — повреждения памяти
AddressSanitizer инструментирует каждый доступ к памяти и окружает выделенные блоки red zone — отравленными областями1. Компилятор вставляет перед каждым чтением и записью проверку теневой памяти (shadow memory): один байт тени на восемь байт реальной памяти. Если байт помечен как отравленный, ASan печатает отчёт и завершает процесс.
Что ловит:
- выход за границы буфера на стеке, куче и в глобальных переменных (heap/stack/global buffer overflow);
- использование памяти после освобождения (use-after-free) и после выхода из области видимости (use-after-scope);
- двойное освобождение и несовпадение
new[]/delete; - утечки памяти — через встроенный LeakSanitizer, который включается вместе с ASan по умолчанию на Linux.
Ключевой механизм для use-after-free — карантин. Освобождённая память не возвращается аллокатору сразу, а держится в очереди отравленной. Пока она в карантине, повторный доступ ловится. Размер карантина ограничен, поэтому очень отложенный use-after-free ASan пропустит: блок успеет вернуться в оборот и перестанет быть отравленным.
UBSan — неопределённое поведение
UndefinedBehaviorSanitizer проверяет операции, которые стандарт C/C++ объявляет неопределёнными: компилятор вправе делать с ними что угодно, и на разных уровнях оптимизации поведение отличается. UBSan вставляет проверки в точки, где UB может произойти.
- знаковое переполнение целых, сдвиг на число разрядов больше ширины типа;
- разыменование нулевого и невыровненного указателя;
- деление на ноль, приведение к типу, в который значение не влезает;
- вызов функции через указатель несовместимого типа (проверка
function), выход enum за диапазон.
UBSan — не один инструмент, а набор проверок. Каждую можно включать отдельно: -fsanitize=integer, -fsanitize=undefined, -fsanitize=bounds и другие. По умолчанию при найденной проблеме он печатает предупреждение и продолжает работу; чтобы падать на первой ошибке, добавьте -fno-sanitize-recover=all.
TSan — гонки данных
ThreadSanitizer ловит гонки данных — ситуации, когда два потока обращаются к одной ячейке памяти без синхронизации и хотя бы один из них пишет. Он строит граф отношения happens-before: отслеживает захваты мьютексов, атомарные операции, создание и join потоков. Если два доступа к одному адресу не упорядочены этим отношением — это гонка.
TSan работает по векторным часам и хранит историю недавних доступов к каждой ячейке. Гонка, которая на конкретном прогоне физически не воспроизвелась (потоки случайно не пересеклись), останется незамеченной — TSan видит только то, что реально произошло в этом запуске, но не требует, чтобы падение случилось.
Санитайзер не доказывает, что кода без ошибок. Он ловит ошибку, только если во время прогона исполнился путь, где она проявляется. Зелёный прогон под ASan — не сертификат, а отсутствие срабатываний на этих данных.
Цена: память и скорость
Санитайзеры замедляют программу и раздувают потребление памяти — это плата за инструментацию каждого доступа. Точные цифры зависят от нагрузки, но порядок величин из документации проектов такой:
| Санитайзер | Замедление | Память | Флаг |
|---|---|---|---|
| ASan | примерно ×2 | примерно ×3 (shadow + red zones) | -fsanitize=address |
| UBSan | небольшое, зависит от набора проверок | почти без роста | -fsanitize=undefined |
| TSan | ×5–15 | ×5–10 (shadow-память под каждый поток) | -fsanitize=thread |
Из этого следуют практические ограничения. TSan с его памятью ×5–10 не запустишь на боевом наборе данных, который и так занимает половину RAM. ASan с ×3 по памяти иногда приходится гонять на урезанном датасете. Свои цифры мерьте на своей нагрузке: разброс огромный и зависит от того, насколько программа memory-bound.
Что нельзя совмещать
ASan и TSan несовместимы в одной сборке — они по-разному переопределяют аллокатор и раскладку памяти. Собирать нужно два отдельных бинарника. UBSan совмещается и с ASan, и с TSan. Типичная стратегия CI — три сборки: ASan+UBSan, TSan+UBSan и обычный релиз.
Как собрать и запустить
Флаги ставятся и на компиляцию, и на линковку — иначе рантайм санитайзера не подтянется. Для читаемых отчётов добавьте -g и отключите инлайнинг оптимизациями не выше -O1:
# clang 18 / gcc 14, сборка с ASan и UBSan
clang++ -std=c++20 -g -O1 -fno-omit-frame-pointer \
-fsanitize=address,undefined \
-fno-sanitize-recover=all \
main.cpp -o app_asan
# отдельный бинарник с TSan
clang++ -std=c++20 -g -O1 -fno-omit-frame-pointer \
-fsanitize=thread,undefined \
main.cpp -o app_tsan
# запуск — символизация стека нужна для читаемого отчёта
ASAN_OPTIONS=detect_leaks=1:abort_on_error=1 ./app_asan
TSAN_OPTIONS=halt_on_error=1 ./app_tsanФлаг -fno-omit-frame-pointer нужен, чтобы в отчёте был нормальный стек, а не оборванный. Без символизатора (llvm-symbolizer для clang, он ставится вместе с LLVM) вместо имён функций вы увидите только адреса.
Пример: use-after-free глазами ASan
#include <cstdio>
int main() {
int* buffer = new int[4];
delete[] buffer;
// доступ к освобождённой памяти — ASan поймает,
// пока блок в карантине
buffer[2] = 42;
printf("%d\n", buffer[2]);
return 0;
}ASan напечатает heap-use-after-free, покажет стек записи, стек освобождения (freed by thread) и стек выделения (previously allocated by thread). Три стека вместо одного дампа — вот ради чего это.
Где санитайзеры ломаются и что пропускают
- Отложенный use-after-free. Если блок пролежал освобождённым дольше, чем держит карантин, ASan его переиспользует и промолчит. Увеличить окно можно опцией
quarantine_size_mbвASAN_OPTIONS, но ценой памяти. - Непроявившаяся гонка. TSan видит только реально исполненные доступы. Редкая гонка на нечастом пути кода не сработает, если тест её не спровоцировал. Помогает прогон под нагрузкой и стресс-тестами, а не единичный запуск.
- Чужой код без инструментации. Библиотека, собранная без санитайзера, — слепая зона. ASan может не заметить переполнение внутри неё, а TSan — принять её синхронизацию за гонку. Для сторонних примитивов синхронизации есть аннотации через
__tsan_*интерфейс, но проще пересобрать зависимость с флагом. - Интеринтерпозиция аллокатора. Если приложение подменяет
mallocсвоим аллокатором или использует пулы, ASan теряет контроль над границами блоков. Кастомные аллокаторы нужно вручную размечать через__asan_poison_memory_region. - Ложные срабатывания TSan на lock-free коде. Корректный код на
std::atomicс расслабленным порядком памяти TSan понимает. А самописные барьеры через inline-ассемблер — нет, и выдаст гонку там, где её нет.
Что делать дальше
Заведите в CI отдельные джобы под ASan+UBSan и TSan — гоняйте на них весь набор тестов, а не только smoke. Замерьте на своей нагрузке реальное замедление: если TSan ×15 не влезает в таймаут CI, выделите его в ночной прогон на подмножестве тестов. Для долгоживущих сервисов посмотрите в сторону непрерывного фаззинга (libFuzzer, AFL++) поверх ASan — фаззер генерирует входы, а санитайзер ловит на них повреждения памяти. Инвалидация карантина и тонкая настройка ASAN_OPTIONS под конкретную утечку — отдельная тема, здесь мы её не разворачиваем.
1 Red zone (красная зона) — отравленная область памяти, которую ASan вставляет вокруг каждого выделенного блока. Любой доступ в неё означает выход за границы буфера и немедленно ловится.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: AddressSanitizer — Clang Documentation, ThreadSanitizer — Clang Documentation
Частые вопросы
У меня gcc, а не clang — санитайзеры будут работать?
Санитайзер молчит — значит, багов нет?
Можно ли собрать бинарник с ASan и TSan одновременно?
Отчёт показывает только адреса вместо имён функций — что не так?
Стоит ли выкатывать санитайзеры в прод?
Санитайзер тормозит CI до таймаута — что делать?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.