Прямо сейчас в вашем ядре есть баги, которые не найдут ещё многие годы. Я знаю это, потому что проанализировал 125183 бага с отслеживаемой меткой Fixes: за 20-летнюю историю Git ядра Linux.
Прежде чем баг обнаружат, он в среднем живёт в ядре 2,1 года. Но в некоторых подсистемах ситуация гораздо хуже: для драйверов шины CAN этот срок в среднем составляет 4,2 года, для сетевого протокола SCTP — 4,0 года. Самый долгоживущий баг в моём датасете (переполнение буфера в ethtool) прятался в ядре 20,7 года. Баг, который я проанализирую в статье подробно (утечка refcount в netfilter), прожил 19 лет.
Я создал инструмент, перехватывающий 92% исторических багов в тестовом датасете на этапе коммитов. Ниже я расскажу, какую информацию мне это дало.
Вкратце о главном |
|
|---|---|
125183 |
Пар устранений багов с отслеживаемыми метками |
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 лет.

Но кое-что не давало мне покоя: в моём датасете содержались исправления только за 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% |

Откуда такая разница? Мой изначальный датасет за 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: ...")
Я написал майнер, выполняющий следующие действия:
Он прогоняет
git log --grep="Fixes:"для нахождения всех исправляющих коммитовИзвлекает хэш коммита, на который ссылается метка
Fixes:Извлекает даты из обоих коммитов
Классифицирует подсистему по файловым путям (более 70 паттернов)
Определяет тип бага по ключевым словам сообщения коммита
Вычисляет срок жизни
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 года |

Вероятно, баги драйверов шины 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 года |
|
Стефано Бривио добавил поддержку множеств с полями переменного диапазона. Для указания длин полей было добавлено |
Январь 2024 года |
|
Пабло Нейра заметил, что код не валидирует сумму длин этих полей с длиной ключа, и выпустил исправление. Сообщение коммита: «Мне не удалось вызвать вылет nft_set_pipapo при помощи несовпадающих длин полей и ключей множеств, но это неопределённое поведение, которое должно быть запрещено». |
Январь 2025 года |
|
Исследователь безопасности нашёл способ обхода. Исправление от 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) |
|
Память (3) |
|
Refcount (5) |
|
Блокировка (5) |
|
Указатели (4) |
|
Обработка ошибок (6) |
|
Семантика (13) |
|
Структура (11) |
|
Основные признаки паттернов багов:
'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-кривой. Измеряет качество ранжирования, то есть насколько хорошо модель отделяет баги от безопасных коммитов при всех пороговых значениях.
Модель корректно дифференцирует один и тот же баг на разных этапах:
Коммит |
Описание |
Риск |
|---|---|---|
|
Устранение UAF в xe_vfio |
12,4%, НИЗКИЙ ✓ |
|
Добавлено 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 |
|---|---|---|
|
1 |
Наличие |
|
0 |
Удалено |
|
1 |
Обнаружено несовпадение |
|
1 |
Использование |
|
1 |
Использование |
Прогноз модели: 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)

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", они в приципе были найдены в очень специфических условиях, не опасны и не проявлялись при обычных условиях.
Надо править если нашли ? Безусловно. Критичны ли они ? Безусловно нет.

Jijiki
13.01.2026 08:23щас чуть подтянув матчасть становится не по себе когда вчитываешься в баги, баги прошлого были не такие как щас вроде, может мне так кажется, я всё таки не експерт
aydar_tech
Баги в ядре Windows в среднем прячутся по 20 лет. Некоторые скрываются до 44 лет