Прямо сейчас в вашем ядре есть баги, которые не найдут ещё многие годы. Я знаю это, потому что проанализировал 125183 бага с отслеживаемой меткой Fixes: за 20-летнюю историю Git ядра Linux.

Прежде чем баг обнаружат, он в среднем живёт в ядре 2,1 года. Но в некоторых подсистемах ситуация гораздо хуже: для драйверов шины CAN этот срок в среднем составляет 4,2 года, для сетевого протокола SCTP — 4,0 года. Самый долгоживущий баг в моём датасете (переполнение буфера в ethtool) прятался в ядре 20,7 года. Баг, который я проанализирую в статье подробно (утечка refcount в netfilter), прожил 19 лет.

Я создал инструмент, перехватывающий 92% исторических багов в тестовом датасете на этапе коммитов. Ниже я расскажу, какую информацию мне это дало.

Вкратце о главном

125183

Пар устранений багов с отслеживаемыми метками Fixes:

123696

Валидных записей после фильтрации (0 < срок жизни < 27 лет)

2,1 года

Среднее время обнаружения бага

20,7 года

Самый долгоживущий баг (переполнение буфера ethtool)

0% → 69%

Количество багов, находимых за первый год (с 2010 по 2022 гг.)

92,2%

Recall VulnBERT на тестовом датасете за 2024 год

1.2%

Частота ложноположительных срабатываний (для ванильного CodeBERT — 48%)

Первоначальное исследование

Начал я с исследования последних 10000 коммитов в ядро Linux с метками Fixes:. Отфильтровав невалидные ссылки (коммиты, указывающие на хэши вне репозиториев, неправильные метки или коммиты слияния), я получил 9876 валидных записей об уязвимостях. Для анализа срока жизни я исключил из них 27 исправлений, внесённых в тот же день (баги, появившиеся и устранённые в течение считанных часов); после этого осталось 9849 багов с достаточно большими сроками жизни.

Результаты меня поразили:

Метрика

Значение

Проанализировано багов

9876

Средний срок жизни

2,8 года

Медианный срок жизни

1,0 год

Максимум

20,7 года

Почти 20% багов скрывалось в коде 5 с лишним лет. Особенно плохо выглядела сетевая подсистема: в среднем 5,1 года. Я нашёл утечку refcount в netfilter, скрывавшуюся в ядре 19 лет.

Initial Bug Lifetime Distribution
Первые выводы: половину багов находят в пределах года, но 20% скрывается больше 5 лет.

Но кое-что не давало мне покоя: в моём датасете содержались исправления только за 2025 год. Я изучил полную картину или увидел лишь верхушку айсберга?

Двигаемся вглубь: откапываем полную историю

Я переписал мой майнер, чтобы он находил все метки Fixes: с момента, когда Linux перешёл в 2005 году на Git. Шесть часов спустя у меня появилось 125183 записи об уязвимостях — в 12 раз больше, чем мой изначальный датасет!

Показатели существенно изменились:

Метрика

Только 2025 год

Вся история (2005-2025)

Проанализировано багов

9876

125183

Средний срок жизни

2,8 года

2,1 года

Медианный срок жизни

1,0 год

0,7 года

Баги старше 5 лет

19,4%

13,5%

Баги старше 10 лет

6,6%

4,2%

Full Dataset Bug Lifetime Distribution
Вся история: 57% багов находят в пределах года. Длинный хвост оказался меньше, чем казалось изначально.

Откуда такая разница? Мой изначальный датасет за 2025 год был перекошен. Исправления за 2025 год включают в себя:

  • Новые баги, появившиеся недавно и быстро обнаруженные

  • Древние баги, которые наконец-то обнаружили спустя долгие годы

Древние баги исказили среднее значение, повысив его. После создания датасета из полной истории, где все баги появились И были исправлены в тот же год, среднее значение упало с 2,8 до 2,1 года.

Настоящая история: мы ускоряемся (но не всё так просто)

Самый поразительный вывод из полного датасета заключается в том, что баги, добавленные в последние годы, как будто устраняются гораздо быстрее.

Год добавления

Баги

Средний срок жизни

% найденных меньше, чем за 1 год

2010

1033

9,9 года

0%

2014

3991

3,9 года

31%

2018

11334

1,7 года

54%

2022

11090

0,8 года

69%

На нахождение багов, добавленных в 2010 году, понадобилось почти 10 лет, а баги, появившиеся в 2024 году, находили за 5 месяцев. На первый взгляд кажется, что произошло улучшение в 20 раз!

Но здесь есть одна тонкость: эти данные цензурированы справа. Баги, добавленные в 2022 году не могут иметь срок жизни 10 лет, потому что сейчас идёт лишь 2026 год. Возможно, мы найдём в 2030 году больше багов из 2022 года, что повысит среднее значение.

Справедливее будет сравнивать по проценту обнаруженных за один год, и по этому параметру ситуация действительно улучшается: с 0% (2010 год) до 69% (2022 год). Это реальный прогресс, вероятно, вызванный следующими причинами:

  • Syzkaller (выпущен в 2015 году)

  • Санитайзеры KASAN, KMSAN, KCSAN

  • Улучшение статического анализа

  • Увеличение количества контрибьюторов, выполняющих ревью кода

Но существует бэклог. Если изучить только баги, устранённые в 2024-2025 годах, то выяснится, что:

  • 60% было добавлено за последние 2 года (новые баги, быстро пойманные)

  • 18% было добавлено 5-10 лет назад

  • 6,5% было добавлено больше 10 лет назад

Мы одновременно быстрее отлавливаем новые баги И медленнее обрабатываем примерно 5400 древних багов, скрывавшихся больше 5 лет.

Методология

У ядра есть правило: когда коммит устраняет баг, в него включается метка Fixes:, указывающая на коммит, в котором этот баг был добавлен.

commit de788b2e6227
Author: Florian Westphal <fw@strlen.de>
Date:   Fri Aug 1 17:25:08 2025 +0200

    netfilter: ctnetlink: fix refcount leak on table dump

    Fixes: d205dc40798d ("netfilter: ctnetlink: ...")

Я написал майнер, выполняющий следующие действия:

  1. Он прогоняет git log --grep="Fixes:" для нахождения всех исправляющих коммитов

  2. Извлекает хэш коммита, на который ссылается метка Fixes:

  3. Извлекает даты из обоих коммитов

  4. Классифицирует подсистему по файловым путям (более 70 паттернов)

  5. Определяет тип бага по ключевым словам сообщения коммита

  6. Вычисляет срок жизни

fixes_pattern = r'Fixes:\s*([0-9a-f]{12,40})'
match = re.search(fixes_pattern, commit_message)
if match:
    introducing_hash = match.group(1)
    lifetime_days = (fixing_date - introducing_date).days

Подробности о датасете:

Параметр

Значение

Версия ядра

v6.19-rc3

Дата майнинга

6 января 2026 года

Начальная дата поиска исправлений

2005-04-16 (эпоха git)

Общее количество записей

125183

Уникальные коммиты исправлений

119449

Уникальные авторы, добавившие баги

9159

С указанием CVE ID

158

С указанием Cc: stable

27875 (22%)

Примечание: в ядре есть примерно 448 тысяч коммитов, где в том или ином виде упоминается «fix», но только примерно в 124 тысячах (28%) используются правильные метки Fixes:. В моём датасете находятся хорошо задокументированные баги, то есть те, первопричину которых обнаружили мейнтейнеры.

Показатели зависят от подсистемы

В некоторых подсистемах есть баги, которые живёт гораздо дольше других:

Подсистема

Количество багов

Средний срок жизни

drivers/can

446

4,2 года

networking/sctp

279

4,0 год

networking/ipv4

1661

3,6 года

usb

2505

3,5 года

tty

1033

3,5 года

netfilter

1181

2,9 года

networking

6079

2,9 года

memory

2459

1,8 года

gpu

5,212

1,4 года

bpf

959

1,1 года

Bug Lifetime by Subsystem
Баги шины CAN и SCTP жили дольше всех. Баги BPF и GPU отлавливаются быстрее всего.

Вероятно, баги драйверов шины CAN и сетевого протокола SCTP жили дольше остальных потому, что это нишевые протоколы, меньше покрываемые тестами. Баги GPU (в особенности Intel i915) и BPF отлавливались быстрее всего, вероятно, благодаря специализированной инфраструктуре фаззинга.

Интересная находка при сравнении полной истории и только 2025 года:

Подсистема

Среднее для 2025 года

Среднее для всей истории

Разница

сетевые протоколы

5,2 года

2,9 года

-2,3 года

файловая система

3,8 года

2,6 года

-1,2 года

драйверы/сеть

3,3 года

2,2 года

-1,1 года

gpu

1,4 года

1,4 года

0 лет

Сетевые протоколы выглядят в данных 2025 года ужасно (5,2 года!), но в полной истории они ближе к среднему (2,9 года). Исправления 2025 года отлавливали бэклог древних багов сетевых протоколов. Результаты GPU одинаковы, эти баги стабильно отлавливаются быстро.

Некоторые типы багов прячутся дольше других

Сложнее всего находить состояния гонки, в среднем для их обнаружения требуется 5,1 года:

Тип бага

Количество

Средний срок жизни

Медиана

Состояние гонки

1188

5,1 года

2,6 года

Целочисленное переполнение

298

3,9 года

2,2 года

Использование после освобождения (use-after-free, UAF)

2963

3,2 года

1,4 года

Утечка памяти

2846

3,1 года

1,4 года

Переполнение буфера

399

3,1 года

1,5 года

Refcount

2209

2,8 года

1,3 года

Разыменование нулевого указателя

4931

2,2 года

0,7 года

Взаимная блокировка

1683

2,2 года

0,8 года

Как состояниям гонки удаётся прятаться так долго? Они недетерминированы и возникают только при специфичных условиях таймингов, которые могут возникать раз в миллион запусков кода. Даже санитайзеры наподобие KCSAN могут обнаруживать только наблюдаемые ими гонки.

30% багов — это самостоятельные исправления: человек, добавивший баг, в конечном итоге сам его удаляет. Наверно, владение кодом — это важно.

Почему некоторые баги прячутся дольше

Меньше покрытие фаззингом. Syzkaller отлично справляется с фаззингом системных вызовов, но испытывает сложности в случае протоколов, хранящих состояние. Для фаззинга netfilter, по сути, нужно генерировать валидные последовательности пакетов, проходящие через определённые состояния соединений.

Их сложнее вызвать. Для выявления многих сетевых багов требуются:

  • Конкретные последовательности пакетов

  • Состояния гонки между конкурентными потоками

  • Нехватка памяти при операциях с таблицами

  • Определённые топологии NUMA

Старый код, за которым следит меньшее количество глаз. Базовая сетевая инфраструктура наподобие nf_conntrack была написана в середине 2000-х. Она работает, поэтому её никто не переписывает. Но из-за её «стабильности» ревью её кода занимается меньшее число разработчиков.

Пример: 19 лет в ядре

Один из самых старых багов в моём датасете был добавлен в августе 2006 года и устранён в августе 2025 года:

// ctnetlink_dump_table() - забагованный путь исполнения кода
if (res < 0) {
    nf_conntrack_get(&ct->ct_general);  // инкремент refcount
    cb->args[1] = (unsigned long)ct;
    break;
}

Ирония: коммит d205dc40798d сам по себе был исправлением: «[NETFILTER]: ctnetlink: устранение взаимной блокировки в дампинге таблиц». Патрик Макхарди устранил взаимную блокировку, удалив вызов _put(). Этим он добавил утечку refcount, просуществовавшую в коде 19 лет.

Баг заключается в том, что код не проверяет равенство ct == last. Если текущая запись та же, которую мы уже сохранили, то мы выполняем её refcount дважды, но декремент выполняется только один раз. Объект никогда не освобождается.

// Что нужно было проверять:
if (res < 0) {
    if (ct != last)  // <-- этой проверки не хватало 19 лет
        nf_conntrack_get(&ct->ct_general);
    cb->args[1] = (unsigned long)ct;
    break;
}

Последствия: накопление утечек памяти. В конечном итоге nf_conntrack_cleanup_net_list() бесконечно ожидает, пока refcount станет равным нулю. Уничтожение сетевого пространства имён netns зависает. Если пользователь использует контейнеры, это бесконечно блокирует очистку контейнера.

Почему понадобилось 19 лет: необходимо было выполнять conntrack_resize.sh в цикле примерно 20 минут в условиях нехватки памяти. В коммите исправления написано следующее: «Воссоздать эту ошибку можно, выполняя в цикле внутренний тест conntrack_resize.sh. Для этого требуется около 20 минут на ядре с вытеснением задач». Эту конкретную последовательность тестирования никто не запускал в течение двух десятков лет.

Часто встречаются неполные исправления

Я часто наблюдаю следующий паттерн: кто-то замечает нежелательное поведение, выпускает исправление, но оно не полностью заделывает дыру.

Пример: валидация полей множеств netfilter

Дата

Коммит

Что произошло

Январь 2020 года

f3a2181e16f1

Стефано Бривио добавил поддержку множеств с полями переменного диапазона. Для указания длин полей было добавлено NFTA_SET_DESC_CONCAT.

Январь 2024 года

3ce67e3793f4

Пабло Нейра заметил, что код не валидирует сумму длин этих полей с длиной ключа, и выпустил исправление. Сообщение коммита: «Мне не удалось вызвать вылет nft_set_pipapo при помощи несовпадающих длин полей и ключей множеств, но это неопределённое поведение, которое должно быть запрещено».

Январь 2025 года

1b9335a8000f

Исследователь безопасности нашёл способ обхода. Исправление от 2024 года было неполным: по-прежнему оставались пути исполнения кода, которые могли не совпадать. Было выпущено реальное исправление.

Исправление от 2024 года стало признанием факта наличия ошибки, но Пабло не смог найти вылет, поэтому решение было консервативным. Год назад кто-то нашёл вылет.

Этот паттерн говорит о существовании возможности обнаружения багов: если в коммитах написано что-то типа «это неопределённое поведение» или «мне не удалось вызвать ошибку, но...», то это показатель. Автор знает, что что-то не так, но не смог полностью описать баг. Такие комментарии заслуживают пристального изучения.

Анатомия древнего бага

Изучив баги, прожившие больше 10 лет, я заметил общие паттерны:

1. Ошибки подсчёта ссылок

kref_get(&obj->ref);
// ... ошибочный путь выполняет возврат без kref_put()

Такие ошибки не сразу приводят к вылетам, а вызывают медленную утечку памяти. В долгоживущей системе вы можете этого не замечать, пока месяцы спустя не начнёт свою работу OOM killer.

2. Отсутствующие проверки на NULL после разыменования

struct foo *f = get_foo();
f->bar = 1;              // сначала происходит разыменование
if (!f) return -EINVAL;  // проверка выполняется слишком поздно

Компилятор может выполнить оптимизацию, убрав проверку на NULL, потому что разыменование уже произошло. Такие ошибки остаются незамеченными, потому что на практике указатель редко бывает NULL.

3. Целочисленное переполнение при вычислении размеров

size_t total = n_elements * element_size;  // может произойти переполнение
buf = kmalloc(total, GFP_KERNEL);
memcpy(buf, src, n_elements * element_size);  // копируется больше, чем было распределено

Если из пользовательского пространства поступает  n_elements, нападающий может вызвать распределение маленького буфера, за которым следует большая копия.

4. Состояния гонки в конечных автоматах

spin_lock(&lock);
if (state == READY) {
    spin_unlock(&lock);
    // окно, в течение которого другой поток может изменить состояние
    do_operation();  // предполагает, что состояние по-прежнему равно READY
}

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

Можно ли отлавливать такие баги автоматически?

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

Я разработал VulnBERT — модель, прогнозирующую внедрение уязвимости коммитом.

Эволюция модели:

Модель

Recall

FPR

F1

Примечания

Random Forest

76,8%

15,9%

0,80

Только вручную создаваемые признаки

CodeBERT (с fine-tuning)

89,2%

48,1%

0,65

Высокий recall, недопустимо высокая частота ложноположительных срабатываний

VulnBERT

92,2%

1,2%

0,95

Лучшее от обоих решений

Проблема ванильного CodeBERT: сначала я попробовал выполнить fine-tuning непосредственно CodeBERT. Результаты: 89% recall, но 48% ложноположительных срабатываний (замерено на том же тестовом наборе). Неприменимо на практике: проблемными помечается половина коммитов.

Почему результаты настолько плохие? CodeBERT обучился простой метрике: «большой diff = опасность», «много указателей = рискованно». Эти корреляции существуют в данных обучения, но не обобщаются. Сопоставление паттернов модели срабатывает на поверхностных признаках, а не на реальных паттернах багов.

Подход VulnBERT: сочетаем нейронное распознавание паттернов с человеческими знаниями предметной области.

┌─────────────────────────────────────────────────────────────────────┐
│                   ВХОДНЫЕ ДАННЫЕ: Git Diff                          │
└───────────────────────────────┬─────────────────────────────────────┘
                                │
                ┌───────────────┴───────────────┐
                ▼                               ▼
┌───────────────────────────┐   ┌───────────────────────────────────┐
│  Блочный кодировщик Diff  │   │   Настроенное вручную извлечение  │
│   (CodeBERT + Внимание)   │   │   признаков (51 спроектированный  │
│                           │   │              признак)             │
└─────────────┬─────────────┘   └─────────────────┬─────────────────┘
              │ [768-мерный]                      │ [51-мерный]
              └───────────────┬───────────────────┘
                              ▼
              ┌───────────────────────────────┐
              │     Смешение перекрёстного    │
              │            внимания           │
              │      Cross-Attention Fusion   │
              │     "Когда код похож на X,    │
              │        признак Y важнее"      │
              └───────────────┬───────────────┘
                              ▼
              ┌───────────────────────────────┐
              │     Классификатор рисков      │
              └───────────────────────────────┘

Три инновации, повысившие точность:

1. Блочное кодирование больших diff. Ограничение CodeBERT в 512 токенов отсекает большинство diff (часто они «весят» больше двух тысяч токенов). Я разбил их на блоки, закодировал каждый, а затем использовал изученное внимание (learned attention) для агрегирования:

# Изученное внимание для блоков
chunk_attention = nn.Sequential(
    nn.Linear(hidden_size, hidden_size // 4),
    nn.Tanh(),
    nn.Linear(hidden_size // 4, 1)
)
attention_weights = F.softmax(chunk_attention(chunk_embeddings), dim=1)
pooled = (attention_weights * chunk_embeddings).sum(dim=1)

Модель обучается тому, какие блоки важны, например, spin_lock без spin_unlock, а не бойлерплейт.

2. Объединение признаков при помощи перекрёстного внимания (cross-attention). Нейронным сетям не хватает паттернов конкретных предметных областей. При помощи регулярных выражений и анализу в стиле абстрактного синтаксического дерева diff я создал вручную 51 признак:

Категория

Признаки

Базовая (4)

lines_addedlines_removedfiles_changedhunks_count

Память (3)

has_kmallochas_kfreehas_alloc_no_free

Refcount (5)

has_gethas_putget_countput_countunbalanced_refcount

Блокировка (5)

has_lockhas_unlocklock_countunlock_countunbalanced_lock

Указатели (4)

has_derefderef_counthas_null_checkhas_deref_no_null_check

Обработка ошибок (6)

has_gotogoto_counthas_error_returnhas_error_labelerror_return_counthas_early_return

Семантика (13)

var_after_loopiterator_modified_in_looplist_iterationlist_del_in_loophas_container_ofhas_castcast_countsizeof_typesizeof_ptrhas_arithmetichas_shifthas_copycopy_count

Структура (11)

if_countelse_countswitch_countcase_countloop_countternary_countcyclomatic_complexitymax_nesting_depthfunction_call_countunique_functions_calledfunction_definitions

Основные признаки паттернов багов:

'unbalanced_refcount': 1,    # kref_get без kref_put → утечка
'unbalanced_lock': 1,        # spin_lock без spin_unlock → взаимная блокировка
'has_deref_no_null_check': 0,# *ptr без if(!ptr) → разыменование нулевого указателя
'has_alloc_no_free': 0,      # kmalloc без kfree → утечка памяти

Перекрёстное внимание обучается условным взаимосвязям. Когда CodeBERT видит паттерны блокировки И unbalanced_lock=1, то риск считается высоким. Ни одного из этих сигналов по отдельности недостаточно, важно их сочетание.

# Объединение признаков при помощи перекрёстного внимания
feature_embedding = feature_projection(handcrafted_features)  # 51 → 768
attended, _ = cross_attention(
    query=code_embedding,      # Какие паттерны есть в коде?
    key=feature_embedding,     # О чём говорят нам созданные вручную признаки?
    value=feature_embedding
)
fused = fusion_layer(torch.cat([code_embedding, attended], dim=-1))

3. Фокусная функция потерь в случае сложных примеров. Данные обучения несбалансированы в том смысле, что большинство коммитов безопасно. Стандартная перекрёстная энтропия тратит градиентные обновления на простые примеры. Фокусная функция потерь:

Стандартная функция потерь при p=0.95 (простые примеры):  0.05
Фокусная функция потерь при p=0.95:   0.000125  (в 400 раз меньше)

Модель фокусируется на коммитах с неопределённым статусом: на тех 5%, которые действительно важны.

Влияние каждого компонента (определено при помощи абляционных экспериментов):

Компонент

Оценка F1

Базовый CodeBERT

~76%

+ Фокусная функция потерь

~80%

+ Объединение признаков

~88%

+ Обучение с контрастированием

~91%

Весь VulnBERT

95,4%

Примечание: влияние отдельных компонентов вычислено приблизительно, из-за взаимодействия между ними точный вклад каждого оценить сложно.

Самое важное здесь заключается в том, что ни нейронные сети, ни вручную составленные правила по одиночке не обеспечат наилучших результатов, необходимо их сочетание.

Результаты валидации по времени (обучающий датасет ≤2023 годы, тестовый — 2024 год):

Метрика

Целевое значение

Результат

Recall

90%

92,2% ✓

FPR

<10%

1,2% ✓

Точность

98,7%

F1

95.4%

AUC

98,4%

Что означают эти метрики:

  • Recall (92,2%): из всех добавивших баги коммитов мы отловили 92,2%, пропустив 7,8% багов.

  • Ложноположительные срабатывания (FPR) (1,2%): из всех безопасных коммитов мы некорректно посчитали опасными 1,2%. Низкий FPR = меньше ложных сигналов.

  • Точность (98,7%): из всех коммитов, которые мы отметили как рискованные, 98,7% действительно были такими. Когда мы поднимаем тревогу, то почти всегда правы.

  • F1 (95,4%): среднее гармоническое точности и recall. Одно значение. характеризующее общую точность.

  • AUC (98,4%): площадь под ROC-кривой. Измеряет качество ранжирования, то есть насколько хорошо модель отделяет баги от безопасных коммитов при всех пороговых значениях.

Модель корректно дифференцирует один и тот же баг на разных этапах:

Коммит

Описание

Риск

acf44a2361b8

Устранение UAF в xe_vfio

12,4%, НИЗКИЙ ✓

1f5556ec8b9e

Добавлено UAF

83,8% ВЫСОКИЙ ✓

Что видит модель: 19-летний баг

При анализе внедрившего баг коммита d205dc40798d:

-    if (ct == last) {
-        nf_conntrack_put(&last->ct_general);  // удалено!
-    }
+    if (ct == last) {
+        last = NULL;
         continue;
     }
     if (ctnetlink_fill_info(...) < 0) {
         nf_conntrack_get(&ct->ct_general);  // по-прежнему на месте

Извлечённые признаки:

Feature

Value

Signal

get_count

1

Наличие nf_conntrack_get()

put_count

0

Удалено nf_conntrack_put()

unbalanced_refcount

1

Обнаружено несовпадение

has_lock

1

Использование read_lock_bh()

list_iteration

1

Использование list_for_each_prev()

Прогноз модели: 72%, риск: ВЫСОКИЙ

Признак unbalanced_refcount срабатывает, потому что _put() был удалён, но _get() сохранился. Классический паттерн утечки refcount.

Ограничения

Ограничения датасета:

  • В нём присутствуют только баги с метками Fixes: (а это примерно 28% от коммитов-исправлений). Перекос выборки: хорошо задокументированные баги обычно бывают более серьёзными.

  • Только основные ветви, не включены исправления только стабильных ветвей и патчи вендоров

  • Классификация подсистем основана на эвристиках (регулярные выражения с файловыми путями)

  • Распознавание типов багов основано на сопоставлении ключевых слов в сообщениях коммитов; многие баги относятся к типу «неизвестный»

  • При расчёте срока жизни используются даты авторов, а не коммитов, rebasing может искажать временные метки

  • Некоторые «баги» могут быть теоретическими (комментарии вида «устранено потенциальное состояние гонки» без подтверждённого состояния)

Ограничения модели:

  • Recall 92,2% на тестовом датасете за 2024 год, отсутствует гарантия для будущих багов

  • Не может отлавливать семантические баги (логические ошибки без синтаксического сигнала)

  • Слепые пятна кроссфункциональности (для багов, разбросанных по нескольким файлам)

  • Перекос обучающих данных (модель изучает паттерны по найденным багам, новые паттерны могут упускаться)

  • Ложноположительные срабатывания на преднамеренных паттернах (инициализация/подчистка разнесены в разные коммиты)

  • Протестировано только на коде ядра Linux; возможно, не обобщается для других кодовых баз

Статические ограничения:

  • Ошибка выжившего в сравнениях по годам (недавние баги не могут иметь долгого срока жизни)

  • Корреляция ≠ каузальность в различиях сроков жизни для подсистем/типов багов

Что это значит: VulnBERT — это инструмент приоритетизации, а не гарантия. Он отлавливает 92% багов с распознаваемыми паттернами. Оставшиеся 8% и новые классы багов по-прежнему требуют проверки человеком и фаззингом.

Что дальше

Recall 92,2% и 1,2% ложноположительных срабатываний — это уровень продакшена. Но предстоит ещё многое сделать:

  • Исследование на основе обучения с подкреплением (RL): вместо статического сопоставления паттернов следует обучить агента автономно исследовать пути исполнения кода и находить баги. На данный момент модель прогнозирует риск; RL-агент сможет генерировать входные данные, приводящие к срабатыванию.

  • Интеграция со Syzkaller: использовать покрытие фаззером в качестве сигнала подкрепления. Если модель отмечает коммит, а Syzkaller находит вылет по этому пути исполнения кода, то это сильный положительный сигнал.

  • Модели под конкретные подсистемы: паттерны сетевых багов отличаются от паттернов багов драйверов. Модель с fine-tuning под netfilter на коммитах netfilter может обогнать по точности обобщённую модель.

Задача модели — не заменить живых ревьюеров, а указать им на 10% коммитов, которые, скорее всего, вызовут проблемы, чтобы люди могли сосредоточить внимание на том, что важно.

Воспроизведение

При извлечении датасета использовалось правило использования метки Fixes: в ядре. Вот базовая логика:

def extract_fixes_tag(commit_msg: str) -> Optional[str]:
    """Extract the commit ID from a Fixes: tag"""
    pattern = r'Fixes:\s*([a-f0-9]{12,40})'
    match = re.search(pattern, commit_msg, re.IGNORECASE)
    return match.group(1) if match else None

# Майним все метки Fixes: из истории git
git log --since="2005-04-16" --grep="Fixes:" --format="%H"

# Для каждого коммита-исправления:
#   - Извлекаем хэш коммита, внёсшего баг 
#   - Получаем даты обоих коммитов
#   - Вычисляем срок жизни
#   - Классифицируем подсистему по файловым путям

Полный код майнера и датасет: github.com/quguanni/kernel-vuln-data


TL;DR

  • Проанализировано 125183 багов за 20 лет истории git ядра Linux (123696 с валидными сроками жизни)

  • Среднее время жизни бага: 2,1 года (в данных 2025 года — 2,8 года из-за ошибки выжившего в недавних исправлениях)

  • Количество багов, найденных за год: 0% → 69% (сравнение 2010 года и 2022 года) (реальное улучшение благодаря более качественному инструментарию)

  • 13,5% багов скрывается больше 5 лет (они опасны)

  • Состояния гонки прячутся дольше всего (в среднем 5,1 года)

  • VulnBERT отлавливает 92,2% багов в тестовом датасете за 2024 год всего с 1,2% FPR (98,4% AUC)

  • Датасет: github.com/quguanni/kernel-vuln-data

Комментарии (3)


  1. aydar_tech
    13.01.2026 08:23

    Баги в ядре Windows в среднем прячутся по 20 лет. Некоторые скрываются до 44 лет


  1. maquefel
    13.01.2026 08:23

    Ну во-первых было https://habr.com/ru/news/983898/.

    Во-вторых если просто брать дельту между появлением коммита и его Fixes:, без дополнительного анализа это нам вообще НИОЧЁМ не скажет. Например https://patchwork.kernel.org/project/linux-dmaengine/list/?series=856378&state=* - фиксы Intel IO/AT DMA "probing error path", они в приципе были найдены в очень специфических условиях, не опасны и не проявлялись при обычных условиях.

    Надо править если нашли ? Безусловно. Критичны ли они ? Безусловно нет.


    1. Jijiki
      13.01.2026 08:23

      щас чуть подтянув матчасть становится не по себе когда вчитываешься в баги, баги прошлого были не такие как щас вроде, может мне так кажется, я всё таки не експерт