Введение

Представьте: ваш продакшн-кластер обрабатывает терабайты трафика, а мониторинг внезапно взрывается от редких kernel panic, которые невозможно воспроизвести на локальной машине (ведь у нас классическое «но на моей же машине всё работает»). Разработка низкоуровневого системного ПО — это всегда баланс между производительностью и абсолютной безопасностью. Когда речь заходит о ядре операционной системы, любая ошибка в управлении памятью или синхронизации потоков перестает быть локальной проблемой отдельного приложения. Она превращается в потенциальный вектор для повышения привилегий, кражи данных или масштабного падения системы. В этом postmortem мы детально разберем инцидент, зарегистрированный в нашей внутренней трекинговой системе под кодовым номером Kernel Soundness Bug #14576.

Данный баг проявился в подсистеме виртуальной памяти и долгое время ускользал от внимания инфраструктурных тестов. Он маскировался под случайные сбои оборудования и редкие race conditions, что делало его отладку поиском иголки в стоге сена. В этой статье мы пошагово восстановим хронологию событий, изучим архитектурные предпосылки возникновения дефекта, проанализируем фрагменты уязвимого и исправленного кода, а также сделаем выводы, которые помогут избежать подобных архитектурных промахов в будущем.

Предыстория и обнаружение проблемы

Первые симптомы Kernel Soundness Bug #14576 были зафиксированы в продакшн-кластере высокой нагрузки, обрабатывающем терабайты сетевого трафика в секунду. Системные администраторы начали замечать редкие, невоспроизводимые kernel panic с дампами памяти, указывающими на повреждение структур данных в куче ядра (kernel slab allocator). Ошибки выглядели следующим образом:

[ 4211.893241] BUG: Bad page state in process kworker/u64:2  pfn:0014ef2a
[ 4211.893892] page:ffffea00053bcab0 count:0 mapcount:0 mapping:0000000000000000 index:0x0
[ 4211.894611] flags: 0x17ffffc0000000(node=0|zone=2|upted*async)
[ 4211.895159] CPU: 1 PID: 1242 Comm: kworker/u64:2 Tainted: G        W  O      5.15.0-custom #1
[ 4211.896001] Call Trace:
[ 4211.896322]  <TASK>
[ 4211.896689]  dump_stack+0x6d/0x89
[ 4211.897042]  bad_page.cold+0x63/0x94
[ 4211.897455]  free_pages_prepare.cst+0x241/0x2b0
[ 4211.897931]  free_pcppages_bulk+0x3b/0x280
[ 4211.898382]  __put_page+0x50/0x60

Подобные сообщения от подсистемы управления страницами памяти (MM) всегда вызывают тревогу. Первоначальные подозрения пали на аппаратные сбои оперативной памяти (ECC RAM), однако прогон расширенных тестов stressapptest не выявил никаких аппаратных аномалий. Проблема носила чисто программный характер и была связана с нарушением инвариантов целостности ядра (soundness).

Команда инженеров задействовала отладчик kGDB и включила расширенное логирование SLUB-аллокатора (slub_debug=FZ), плавно сузив область поисков до конкретного драйвера. Это позволило локализовать область поиска: повреждение происходило в момент освобождения дескрипторов буферов ввода-вывода, когда несколько параллельных потоков пытались модифицировать один и тот же счетчик ссылок (reference counter) без должной атомарной синхронизации.

Анатомия дефекта: гонка данных в механизме управления страницами

Глубинной причиной проблемы оказалась классическая гонка данных (race condition) при работе со счетчиком ссылок страниц памяти. В исходной реализации драйвера подсистемы ввода-вывода использовался следующий паттерн освобождения ресурсов:

/* Уязвимый код в подсистеме ввода-вывода */
void buffer_desc_free(struct io_buffer *buf)
{
    if (--buf->ref_count == 0) {
        // Ошибка: между проверкой и вызовом __put_page может вклиниться другой поток
        __put_page(buf->page);
        kfree(buf);
    }
}

Если два параллельных потока одновременно вызывали buffer_desc_free() для одного и того же буфера, декремент buf->ref_count выполнялся неатомарно. В результате операция --buf->ref_cou