РАЗБОР
На проверке
Race condition в ядре Linux: delay injection и MAccConc
Режим чтения
[ обложка статьи ]
Восемь лет. Ровно столько race condition CVE-2017-2636 (CVSS 7.0 HIGH) тихо жила в mainline-ядре Linux. Коммит
be10eb75893 внёс гонку в драйвер n_hdlc в 2009-м, syzkaller наткнулся на подозрительный крэш только в 2017-м - а между обнаружением и стабильным воспроизведением прошли дни ручной возни: пересборка ядра с точечными mdelay(), перебор задержек в наносекундном диапазоне, привязка потоков к ядрам CPU через sched_setaffinity(). Кто хоть раз занимался этим - знает ощущение: ты не исследуешь баг, ты играешь в рулетку с планировщиком.В сентябре 2026 года Project Zero выложил MAccConc - Memory Access Concurrency - инструментарий, который переводит этот процесс из шаманства в алгоритм. Трассировка обращений к памяти через ASAN outline и KCOV определяет точки потенциальных гонок, а stack-based delay injection автоматически проверяет каждое чередование. Ниже - разбор подхода от теории communication points до практики A-B-A тестирования, с анатомией реальных CVE и нюансами CWE-классификации, которые русскоязычные источники до сих пор не покрывали.
Почему race condition в ядре сложно подтвердить
Race condition - уязвимости, при которых многопоточное выполнение должно произойти с определённым чередованием (interleaving) для проявления бага. Публикация Project Zero (Testing race conditions with memory access tracing and stack-based delay injection) выделяет три сценария, где это создаёт головную боль. Подробнее - в нашем материале про бинарный анализ уязвимостей.Подтверждение кандидатов. При ручном аудите кода ядра ты находишь потенциальный race condition - но доказать или опровергнуть его без модификации ядра зачастую невозможно. Стандартный подход: перекомпиляция с условными вызовами
mdelay() (спинлок на заданное время) в стратегических точках. Условие обычно привязывается к имени текущего потока, иногда нужно сложнее. На платформах с DTrace (macOS, Windows) доступны пробы с chill(), но DTrace трассирует только на границах не-inline функций или явных tracepoint'ах - не на каждой инструкции. В обоих случаях - пробы и ошибки.Regression testing ядра. После исправления гонки нет надёжного способа написать регрессионный тест, который стабильно воспроизводит баг в CI/CD-пайплайне. Характерный признак: патчи для race condition в ядре Linux регулярно сопровождаются нарисованными от руки ASCII-диаграммами проблемных чередований с call graph'ами и обращениями к памяти. До MAccConc автоматизированного инструмента для генерации таких диаграмм не было.
Фаззинг ядра Linux. Фаззеру трудно перебрать все интересные чередования конкурентных операций или добраться до путей исполнения, которые активируются только при гонке. Syzkaller находит race condition «случайно» - через массовый параллельный запуск syscall'ов на многоядерных VM. Целенаправленное исследование чередований при фаззинге - нерешённая задача, и MAccConc закладывает для этого ядерную инфраструктуру.
MAccConc: инструментарий трассировки памяти ядра от Project Zero
MAccConc - набор инструментов для исследования возможных чередований многопоточных тест-кейсов в ядре Linux, доступный на GitHub. Три компонента:- Автоматический тестер - перебирает все A-B-A чередования тест-кейса, инжектируя задержки в каждой потенциальной точке гонки.
- Terminal UI - ручное исследование чередований в терминале с навигацией по communication points.
- GUI - графический интерфейс для визуализации чередований и трассировок.
Идейные предшественники: sockfuzzer Неда Вильямсона (пользовательский планировщик с перепланированием на примитивах синхронизации для исследования concurrency bugs) и проект SKI (запись обращений к памяти и управление планированием vCPU через патченный QEMU в TCG-режиме с VM-снапшотами для перебора чередований).
ASAN outline и KCOV: основа инструментации
Чтобы обнаружить потенциальные гонки, нужен механизм сбора трассировки обращений к памяти. SKI решал это патчем к QEMU TCG. MAccConc идёт принципиально другим путём - ASAN-инструментация в outline-режиме.При компиляции ядра с
CONFIG_KASAN_OUTLINE (backend-флаг asan-instrumentation-with-call-threshold=0) компилятор генерирует вызовы helper-функций при каждом обращении к памяти. Обычно ASAN объединяет callback'и для последовательных обращений - ядро Linux явно отключает эту оптимизацию backend-флагом asan-opt-same-temp, чтобы получить по одному callback'у на каждое обращение. По умолчанию ASAN не инструментирует обращения к глобальным переменным - это тоже отключается флагом asan-opt-globals.Для передачи трассировки в userspace MAccConc использует KCOV - механизм, изначально созданный для передачи покрытия базовых блоков пользовательским фаззерам (syzkaller работает через KCOV). Почему KCOV, а не ftrace? Публикация Project Zero даёт три аргумента: простое in-memory представление данных (критично для восстановления из крашнувшихся VM); статическая always-on инструментация с околонулевым оверхедом в выключенном состоянии; проектирование под высокочастотные трейс-события (обращений к памяти на порядки больше, чем вызовов функций).
Принципиальное решение - инструментация в ядре, а не в эмуляторе. Это позволяет потенциально собирать высокоуровневую информацию о захватах и освобождениях блокировок, а теоретически - тестировать на bare-metal.
Слепое пятно ASAN outline. ASAN спроектирован для детектирования use-after-free и не генерирует callback'ов для прямых обращений к стековой памяти (если нет потенциала out-of-bounds). Гонки с участием on-stack объектов - wait queues, completion structures - могут пролететь мимо. Альтернатива - TSAN-инструментация, заточенная под data race и дающая информацию об атомарности обращений. Но компиляторы не поддерживают одновременную генерацию ASAN и TSAN хуков - чтобы сохранить рабочий детектор UAF при использовании TSAN, пришлось бы запускать ASAN-реализацию ядра поверх TSAN-хуков или модифицировать компилятор. На момент публикации Project Zero этот барьер не преодолён.
Communication points: поиск потенциальных гонок
Концепция заимствована из работы SKI: communication points - пары обращений к памяти на разных потоках, которые могут взаимодействовать. Критерий: хотя бы одно обращение - запись, и диапазоны адресов пересекаются.Алгоритм поиска:
- Собрать трассировку обращений к памяти всех потоков тест-кейса через KCOV.
- Найти пары (поток A, поток B), удовлетворяющие критерию communication point.
- Для каждой пары проверить, приводит ли изменение порядка выполнения к различному поведению.
Count-augmented stack traces: стабильная идентификация обращений к памяти
Для тестирования различных порядков обращений нужен способ стабильно идентифицировать конкретное обращение между запусками тест-кейса. Два наивных подхода не работают:По адресу данных - если объект аллоцируется заново при каждом запуске, адрес меняется из-за ASLR и состояния slab-аллокатора. По адресу инструкции - не работает для обращений из generic-функций вроде
memcpy() или spin_lock(), вызываемых из кучи контекстов.Решение MAccConc - count-augmented stack traces: каждое обращение к памяти идентифицируется полным стек-трейсом, дополненным счётчиком - сколько раз этот же стек-трейс уже встречался в текущей сессии KCOV-сбора.
Конкретный пример: если
spin_lock() вызывается дважды из n_hdlc_send_frames() с идентичным стеком вызовов - первое обращение получает count=0, второе count=1. При следующем запуске тест-кейса обращения получат те же идентификаторы, даже если абсолютные адреса объектов уплыли. Автор MAccConc выделяет именно этот подход как центральный теоретический вклад проекта - и по делу: без стабильной идентификации весь автоматический перебор рассыпается.Stack-based delay injection: управление чередованием потоков
Когда communication points обнаружены и стабильно идентифицированы, MAccConc использует delay injection для принудительного изменения порядка обращений к памяти. «Stack-based» - потому что задержка привязана к стек-трейсу: не «где-то в функции X», а «при N-м обращении к памяти с данным count-augmented stack trace». Пересборка ядра не нужна - точка инъекции определяется алгоритмически при каждом запуске.Constraint-style и fully specified ordering
MAccConc реализует два режима инъекции задержек:Constraint-style (A-happens-before-B) задаёт единственное ограничение: обращение A на потоке 1 должно произойти до обращения B на потоке 2. Реализуется вставкой задержки на потоке 2 в точке B до тех пор, пока поток 1 не выполнит обращение A. Минимально инвазивный подход - фиксирует порядок одной пары, всё остальное выполняется свободно.
Fully specified ordering (context-switch-style) - полная спецификация порядка всех обращений к памяти, эмулирующая детерминированное переключение контекста. Позволяет проверить конкретный сценарий чередования вплоть до каждой инструкции. Вычислительно дороже, зато полный контроль над interleaving.
Принципиальное отличие от ручного подхода с
mdelay(): инъекция привязана к стабильному идентификатору (count-augmented stack trace), а не к позиции в исходном коде. Воспроизводимо без модификации ядра.Автоматическое тестирование и regression testing ядра
Автоматический тестер MAccConc перебирает все A-B-A чередования тест-кейса:- Запуск тест-кейса с трассировкой обращений к памяти через KCOV.
- Выявление communication points между потоками.
- Генерация constraint (A-happens-before-B) для каждой пары communication points.
- Повторный запуск с инжектированной задержкой.
- Фиксация результата: KASAN-отчёт, kernel panic, некорректное состояние.
Направления развития, обозначенные в публикации Project Zero:
- Human-readable memory access traces - добавление информации о типах к трассировке обращений.
- Высокоуровневая семантика: не просто адрес, а идентификация объекта ядра.
- Детектирование невозможных чередований через обнаружение deadlock'ов - ускорение перебора за счёт отсечения заведомо нереализуемых orderings.
- Построение тест-кейсов с потенциальными communication points по модели Snowboard (инкрементальное наращивание тест-кейса вместо полного перебора).
- Вывод KCOV в host-shared memory для снижения оверхеда при фаззинге ядра Linux.
Эксплуатация race condition в ядре: CVE-2017-2636 и классификация CWE
CVE-2017-2636 - модельный пример race condition, для подтверждения и regression testing которой MAccConc стал бы незаменим.Суть. Race condition в
drivers/tty/n_hdlc.c ядра Linux до версии 4.10.1. CVSS 7.0 HIGH, вектор CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H. Локальный доступ (AV:L), высокая сложность - нужно выиграть гонку (AC:H), низкие привилегии (PR:L), без взаимодействия пользователя (UI:N), полное воздействие на конфиденциальность, целостность и доступность. Эксплуатация приводит к повышению привилегий до root - Exploitation for Privilege Escalation (T1068, privilege-escalation).Механика гонки. Функции
flush_tx_queue() и n_hdlc_send_frames() конкурентно обращаются к указателю n_hdlc.tbuf. При ошибке передачи данных буфер сохраняется в этом указателе. При одновременном исполнении обе функции могут дважды поместить один буфер в список tx_free_buf_list - double free при вызове n_hdlc_release(). Буферы n_hdlc_buf аллоцируются в slab-кеше kmalloc-8192.Цепочка атаки. Модуль
n_hdlc загружается автоматически при установке протокола N_HDLC для последовательной линии - достаточно открыть псевдотерминал через /dev/ptmx и вызвать ioctl(ptmd, TIOCSETD, &ldisc). Дальше эксплоит (описан в публикации xakep.ru) использует sk_buff с destructor_arg в skb_shared_info для перезаписи указателя на функцию. Сетевой пакет размером ~7500 байт помещает skb_shared_info в kmalloc-8192 - тот самый кеш, где произошёл double free. Heap spraying через пары сокетов даёт два экземпляра sk_buff с полем head, ссылающимся на одну область памяти. Подход заимствован из эксплуатации CVE-2016-2384 (double free в snd_usbmidi_create, EDB-41999, автор Andrey Konovalov).Для воспроизведения гонки эксплоит использует синхронизацию потоков на
pthread_barrier, варьируемые задержки tmo1/tmo2 в холостом цикле и привязку потоков через sched_setaffinity(). MAccConc автоматизирует именно этот ручной delay injection: вместо перебора задержек с инкрементом loop % MAX_RACE_LAG_USEC - автоматическое определение communication points между flush_tx_queue() и n_hdlc_send_frames() и constraint-style injection в точках обращения к n_hdlc.tbuf.CWE-классификация: расхождение между источниками. NVD классифицирует CVE-2017-2636 одновременно как CWE-362 (Concurrent Execution using Shared Resource with Improper Synchronization) и CWE-415 (Double Free). Red Hat в RHSA-2017:0892 указывает только CWE-362 и оценивает CVSS v3 base на 7.8 (выше NVD-шных 7.0). При этом KASAN при срабатывании фиксирует ошибку как use-after-free (CWE-416), происходящий непосредственно перед вторым
kfree().Это не противоречие - это разные проекции одного бага. CWE-362 описывает корневую причину (race condition, отсутствие синхронизации потоков ядра). CWE-415 - прямое последствие (двойное освобождение одного блока). CWE-416 - то, как ошибка проявляется при эксплуатации, когда между первым и вторым
kfree() slab-аллокатор переиспользует блок для другого объекта. Аналогичное расхождение характерно для CVE-2016-2384: NVD описывает как «Double free vulnerability», Red Hat классифицирует как CWE-416 (Use After Free).Scope. Уязвимость затронула RHEL 6/7 (RHSA-2017:0892, пакет
kernel-0:2.6.32-696.1.1.el6), Fedora, SUSE, Debian (DSA-3804-1, исправлено в linux 3.16.39-1+deb8u2 для jessie) и Ubuntu (USN-3220-3, USN-3221-1/2). EPSS - 0.0102, 61-й перцентиль (выше медианы). Все дистрибутивы с CONFIG_N_HDLC=m оказались уязвимы.MSG_OOB и delay injection в контексте эксплуатации
Последние полтора года я использую KCOV и KASAN для воспроизведения race condition в подсистемах netfilter и io_uring. До MAccConc workflow выглядел так: syzkaller находит подозрительный крэш, я минимизирую reproducer, вставляю
mdelay() в стратегически выбранные точки, пересобираю ядро, запускаю в QEMU - и повторяю десятки раз, подбирая длительность с шагом в единицы миллисекунд. Count-augmented stack traces меняют правила: точка инъекции определяется алгоритмически, а не на ощупь.Но есть нюанс, который в публикации Project Zero упомянут вскользь, а на практике бьёт больно: слепое пятно ASAN outline на стековых объектах. Wait queues, completion structures, on-stack spinlock'и - всё это не генерирует callback'ов. Для гонок в io_uring, где wait queue инициализируется на стеке и используется через
io_cqring_wait(), это реальная дыра в покрытии. TSAN решил бы проблему, но несовместимость с ASAN на уровне компилятора - барьер, требующий изменений в GCC или Clang. Пока никто не взялся.Второе ограничение - масштабируемость автоматического тестера. A-B-A перебор работает для тест-кейсов с ограниченным числом communication points. Реальный syscall-шторм от syzkaller генерирует десятки тысяч обращений к памяти, и полный перебор становится вычислительно непрактичным. Интеграция с фаззером по модели Snowboard (инкрементальное наращивание тест-кейса из потенциальных communication points) - обозначенное, но нереализованное направление. До этого момента MAccConc - инструмент подтверждения конкретных гипотез, а не discovery engine.
И отдельно про CWE-классификацию race condition - она заслуживает системного подхода. Один и тот же баг получает CWE-362+CWE-415 от NVD и CWE-416 от Red Hat. Для exploit-разработчика разница между double free и use-after-free фундаментальна: UAF даёт read/write primitive напрямую, double free - контроль freelist. При подаче CVE стоит указывать все применимые CWE и объяснять их связь в advisory. Хочется руки в крови - pwn-задачи на HackerLab.pro позволяют отработать подобные kernel-примитивы на живом стенде.
AI-выжимка
сгенерировано ИИ
Тезисы статьи скоро
Статья читается полностью. Тезисы со ссылками на разделы появятся позже.
Содержание
Codeby Academy
Практика и мастерство
От основ до продвинутого — программы для практиков Codeby.
Перейти к курсу →
Поиск в обсуждении
Карта ветки
Продолжить чтение
Следующий разбор
API шпионаж - инструменты и трюки
Ещё по теме
- Статья
- Статья
Комментарии
0