Статья Bad Epoll (CVE-2026-46242): разбор Linux LPE уязвимости — от патча ядра до эксплуатации

Крупный план материнской платы с повреждённым чипом контроллера и выгоревшей дорожкой. На текстолите лазером выгравирована надпись «CVE-2026-46242 · BAD EPOLL · UAF».


Шесть машинных инструкций. Окно гонки шириной в шесть инструкций - и Jaeyoung Chung из Seoul National University Computer Security Lab превращает непривилегированный процесс в root с надёжностью 99 из 100 попыток. Эксплойт опубликован на oss-sec, баг зарегистрирован как CVE-2026-46242 (Bad Epoll) - use-after-free в fs/eventpoll.c, CVSS 7.8 HIGH, вектор AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H, CWE-416. И вот что неприятно: баг сидит в подсистеме, которую нельзя отключить. На epoll завязан весь I/O-мультиплексинг GNU/Linux - nginx, Node.js, Android event loop. Эксплойт подан как 0-day в Google kernelCTF (программа с вознаграждением от $71 337 за kernel exploit). Ниже - root cause из патча, цепочка эксплуатации до ROP-chain с root shell, затронутые версии и проверка экспозиции.

Зачем атакующему local privilege escalation в ядре Linux​

1783790752558.webp

Privilege escalation через уязвимость ядра - Exploitation for Privilege Escalation (T1068) по MITRE ATT&CK - один из самых ценных примитивов в цепочке атаки. Bad Epoll не даёт initial access: уязвимость локальная, нужно предварительное исполнение кода на целевом хосте. Но в этом и роль: конвертировать ограниченный foothold в полную компрометацию.

Место в kill chain:
  • До Bad Epoll: атакующий получил local code execution - через trojanized npm/pip-пакет, украденный SSH-ключ, RCE в CI/CD-задаче, browser exploit в рендерере или shell в контейнере
  • Bad Epoll (T1068): непривилегированный код -> root
  • После Bad Epoll: persistence через kernel modules (T1547.006), rootkit (T1014), credential access, lateral movement по инфраструктуре
[Применимо: внутренний пентест, post-compromise LPE, modern Linux kernel 6.4+. Не применимо: внешний пентест без начального доступа, ядра до v6.4.]

Три свойства делают Bad Epoll опаснее типичного linux kernel privilege escalation.

Нет kill switch. Epoll - базовая подсистема ядра. Copy Fail (CVE-2026-31431) можно нейтрализовать выгрузкой уязвимого модуля - с epoll такой фокус не пройдёт. На нём работает ОС, сетевые сервисы и браузеры. Единственный путь - патч.

Работает из Chrome renderer sandbox. По данным исследователя, Bad Epoll триггерится из-под renderer sandbox Chrome - того самого sandbox, который блокирует большинство kernel-багов. Связка renderer exploit + Bad Epoll даёт kernel code execution - аналог цепочки, продемонстрированной Project Zero.

Кандидат на rooting Android. Из примерно 130 уязвимостей, проэксплуатированных на Google kernelCTF, только около десяти пригодны для root Android. Bad Epoll - одна из них. На Pixel 10 (ядро v6.6+) UAF подтверждён, полный linux root exploit в разработке.

Подсистема epoll: почему lifetime объектов становится attack surface​

Epoll - механизм масштабируемого I/O-мультиплексинга. Программа создаёт экземпляр через epoll_create1(), добавляет наблюдаемые файловые дескрипторы через epoll_ctl(), ожидает событий через epoll_wait(). На epoll построены event loop'ы nginx, Node.js, Python asyncio, Go runtime, баз данных и Android.

За простым API скрыта плотная сеть зависимостей между kernel-объектами. Экземпляр epoll - сам struct file. Наблюдаемые дескрипторы тоже ссылаются на struct file - сокеты, pipe, eventfd, timerfd, другие epoll-экземпляры. Последний случай - epoll-watches-epoll - легитимная часть API, но она превращает управление lifetime в минное поле: один объект уведомлений зависит от другого, и оба могут быть закрыты конкурентно разными потоками.

Ментальная модель тут такая: epoll - не только event API, а граф ссылок между kernel-объектами. Баг возникает, когда ребро в графе удаляется раньше, чем нужно, а другой поток считает граф валидным и продолжает по нему ходить. Классическая проблема lifetime management, которая в ядре превращается в эксплуатируемый примитив.

Root cause CVE-2026-46242: use-after-free в epoll при конкурентном закрытии​

1783790813686.webp

Уязвимый коммит - 58c9b016e128 от 8 апреля 2023 года - уменьшал contention на ep_mutex в подсистеме epoll. Этот же коммит ввёл две отдельные race condition в примерно 2500 строк кода eventpoll.

Механика гонки​

Описание NVD указывает на проблему с хирургической точностью: ep_remove() через ep_remove_file() очищал file->f_ep под file->f_lock, но продолжал использовать @file в последующей критической секции. Это создавало окно, в котором конкурентный __fput() мог наблюдать транзитный NULL, пропустить eventpoll_release_file() и перейти к f_op->release / file_free().

Что происходит при конкурентном закрытии двух связанных epoll-дескрипторов:
  1. Thread A вызывает ep_remove(). Под file->f_lock очищает file->f_ep = NULL.
  2. Thread B конкурентно вызывает __fput() на том же struct file. Видит f_ep == NULL -> считает файл не связанным с epoll -> пропускает eventpoll_release_file() -> вызывает f_op->release / file_free().
  3. Thread A продолжает работу: is_file_epoll(), hlist_del_rcu() через epi->fllink.pprev, spin_unlock() - всё на уже освобождённых объектах.
Результат - два типа epoll use-after-free:
  • struct eventpoll UAF: в сценарии epoll-watches-epoll hlist_del_rcu() пишет через указатель в уже освобождённый kmalloc-192
  • struct file UAF: struct file использует SLAB_TYPESAFE_BY_RCU(https://www.kernel.org/doc/html/latest/mm/slub.html) - слот может быть переиспользован аллокатором, пока функция ещё держит указатель на объект, который уже не "тот самый"

Фикс и родственная CVE-2026-43074

Исправление - коммит a6dc643c6931 от 24 апреля 2026 года - захватывает ссылку на @file через epi_fget() в начале ep_remove(), до того как очищается ассоциация:
C:
static void ep_remove(struct eventpoll *ep, struct epitem *epi)
{
    struct file *file __free(fput) = NULL;
    ep_unregister_pollwait(ep, epi);
    if (unlikely(READ_ONCE(epi->dying)))
        return;
    file = epi_fget(epi); /* pin file ДО очистки f_ep */
    if (!file)
        return;
    spin_lock(&file->f_lock);
    ep_remove_file(ep, epi, file);
}
epi_fget() - вся суть патча. С ним struct file не может быть освобождён, пока ep_remove() не завершит работу. Концептуально просто - но в ядре «простая» фиксация ссылки требует аккуратного размещения: слишком поздно - гонка сохраняется; на неправильный объект - один класс UAF закрывается, другой остаётся; с нарушением lock ordering - deadlock.

Тот же коммит 2023 года ввёл CVE-2026-43074 (CWE-401, CVSS 7.8) - ещё одну race в epoll. Её обнаружила AI-модель Anthropic Mythos, и это само по себе примечательно: race-баги считаются одним из самых трудных классов для автоматического поиска. Фикс CVE-2026-43074 откладывал освобождение struct eventpoll через RCU grace period. Но UAF на struct file - Bad Epoll - остался: устранение одного симптома не закрыло всю lifetime-проблему в дизайне.

Почему Mythos пропустила именно Bad Epoll? Исследователь называет две вероятные причины. Окно гонки - шесть инструкций; точный interleaving потоков сложно визуализировать даже человеку, глядя на исходник. И после фикса CVE-2026-43074 Bad Epoll не триггерит KASAN - основной runtime-детектор memory errors ядра. Без этого сигнала у модели могло не хватить уверенности для репорта. Первый патч мейнтейнеров тоже не полностью закрыл проблему - корректный фикс появился только через два месяца после исходного репорта.

Эксплуатация уязвимости ядра Linux: от UAF к root shell​

1783790836743.webp

Требования к окружению для воспроизведения:
  • Ядро Linux 6.4 - 6.12.94 (или 6.6 - 6.6.143 для LTS-ветки) без фикса a6dc643c6931
  • Local code execution от непривилегированного пользователя
  • Компилятор C++ (exploit написан на C++)
  • Для безопасного тестирования: QEMU + pwndbg/gdb, 2+ GB RAM для VM
Публичный репозиторий Bad Epoll - полный write-up с кодом, показывающий путь от шестиинструкционного окна до надёжного LPE linux.

Высокоуровневая цепочка эксплуатации bad epoll exploit:
  1. Четыре epoll-объекта создаются и группируются в две пары epoll-watches-epoll. Одна пара - триггер гонки, другая - жертва.
  2. Конкурентное закрытие триггерной пары провоцирует close-vs-close race. Эксплойт расширяет окно через timer interrupt и запускает retry-цикл, который не крашит ядро при неудаче. Это принципиально - можно долбить гонку хоть тысячу раз без паники.
  3. 8-байтная UAF-запись в освобождённый kmalloc-192 через hlist_del_rcu(), который пишет через epi->fllink.pprev.
  4. Pivot на struct file UAF - cross-cache attack даёт полный контроль над содержимым file-объекта.
  5. Arbitrary kernel read через /proc/self/fdinfo - dangling struct file, подставленный под pipe, утекает kernel-адреса. Обход KASLR без отдельного info leak.
  6. Hijack control flow - перехват indirect call, захват instruction pointer.
  7. ROP-chain -> elevated privileges -> root shell.
Подтверждённая надёжность на публичных целях Google kernelCTF:
  • lts-6.12.67 - 99% reliability
  • cos-121-18867.294.100 (Container-Optimized OS) - 98% reliability
  • Pixel 10 (ядро v6.6+) - UAF подтверждён, полный root exploit в разработке. Pixel 8 и устройства на ядре v6.1 не затронуты.

Ограничения эксплуатации​

Не remote: обязательно предварительное local code execution. Без foothold на хосте Bad Epoll бесполезен.

Kernel CFI (CONFIG_CFI_CLANG): усложняет ROP, но не блокирует UAF-примитив. Сам use-after-free и arbitrary read работают независимо от CFI.

seccomp с жёстким whitelist: теоретически может блокировать epoll-syscalls, на практике epoll разрешён почти повсеместно - слишком много софта от него зависит.

grsecurity/PaX (RANDKSTACK и др.): усложняет heap spray и предсказание layout, не устраняет root cause. Затрудняет эксплуатацию, но не делает её невозможной.

Пропатченные ядра: 6.6.144+ и 6.12.95+ содержат фикс - exploit не работает.

Затронутые версии и проверка экспозиции Linux kernel​

По данным OSV.dev и upstream-фиксов:

Ветка ядраСтатус
до 6.4Не затронута
6.4 - 6.6.143Затронута, фикс в 6.6.144
6.7 - 6.12.94Затронута, фикс в 6.12.95
6.13+Затронута, проверяйте backport

Enterprise-дистрибутивы бэкпортируют код, и uname -r не отражает реальный patch status. Проверяйте по advisory, а не по major/minor:

ДистрибутивAdvisoryВерсия с фиксом
Debian trixieDSA-6381-1 (05.07.2026)linux 6.12.95-1
RHEL 10RHSA-2026:36541 (08.07.2026)kernel-0:6.12.0-211.31.1.el10_2
RHEL 9RHSA-2026:36645 (08.07.2026)kernel-0:5.14.0-687.23.1.el9_8
Azure Linux 3MSRC (31.05.2026)azl3 kernel 6.6.143.1-1

Red Hat классифицирует RHEL 6 и RHEL 7 как "Not affected" - ядра этих версий основаны на ветках до 6.4.
Bash:
# Текущая версия ядра
uname -r
# RHEL/CentOS: бэкпорт фикса в changelog
rpm -q --changelog kernel | grep -E "CVE-2026-46242|a6dc643c6931"
# Debian/Ubuntu: аналогичная проверка
apt changelog linux-image-$(uname -r) 2>/dev/null | grep -E "CVE-2026-46242|eventpoll"
Если ничего не нашлось - ядро с высокой вероятностью уязвимо. Ориентиры для ручной проверки - fix-коммиты: a6dc643c6931, ced39b6a8062, ef4ca02e9536.

Приоритет патчинга:
  1. Multi-tenant серверы и Kubernetes worker nodes - атакующий из одного пода может получить root на ноде, после чего - lateral movement по кластеру
  2. CI/CD runners - compromised dependency -> LPE -> секреты сборки и артефакты
  3. Developer workstations - stolen token или malicious package -> полная компрометация рабочей станции
  4. Android устройства на ядре 6.6+ (Pixel 10 и новее) - ожидайте OTA-обновление от Google

Детекция Bad Epoll и её ограничения​

Файловых IOC нет - Bad Epoll это exploitation primitive, а не malware-семейство. Детекция вынужденно контекстная.

KASAN не ловит Bad Epoll после фикса CVE-2026-43074. Исследователь указывает это как одну из причин, почему AI-модель пропустила баг. KFENCE (sampling-based детектор) теоретически может обнаружить UAF при удачном тайминге, но гарантий нет.

Audit syscalls (epoll_create, epoll_ctl, close) через auditd возможен, но шум колоссальный - epoll используется каждым сетевым сервисом. Паттерн "быстрое создание и уничтожение множества epoll-дескрипторов в tight loop" может быть поведенческим индикатором, но требует калибровки порога под конкретную нагрузку. На production-сервере с nginx под нагрузкой отличить эксплойт от легитимного трафика - та ещё задача.

Правило для incident response: если непривилегированный код исполнялся на хосте с уязвимым ядром в период до патчинга - предполагайте kernel-level компрометацию, даже если application logs показывают только userspace-событие. Стандартный набор: изоляция хоста, сбор volatile evidence, проверка на persistence - kernel modules, изменённые бинарники, crontab, SSH-ключи.

Bad Epoll vs Copy Fail: Linux kernel privilege escalation 2026​

📚 Часть контента скрыта. Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме

Два месяца от первого репорта до корректного патча для бага в 2500 строках кода - факт, который стоит запомнить. Мейнтейнеры ядра - одна из сильнейших команд в open source, и первый фикс оказался неполным. Race condition в core kernel subsystem - класс ошибок, где один code review или автоматический тест не гарантирует ничего. Frontier AI-модель Mythos нашла одну гонку в том же участке кода и пропустила соседнюю - это объективный индикатор сложности, а не анекдот.

Большинство команд ставят приоритет kernel LPE ниже, чем remote RCE. По CVSS формально логично: AV:L < AV:N. Но в реальных инцидентах 2026 года именно local exploitation оказывается звеном, которое конвертирует "один скомпрометированный CI runner с low-priv shell" в "root на Kubernetes ноде с доступом ко всему кластеру". Bad Epoll, Copy Fail, их предшественники - это паттерн: каждый год минимум 2-3 публичных kernel LPE с рабочим PoC, и каждый раз - недели на раскатку патчей по продакшн-серверам.

С ростом AI-assisted vulnerability research количество обнаруженных kernel race condition будет расти. Mythos нашла CVE-2026-43074, следующая итерация модели найдёт три подобных бага за тот же цикл. Окно между disclosure и patch availability сократится. А вот окно между patch availability и фактическим обновлением - нет. Кто по-прежнему считает «только локальный» аргументом для отложенного патчинга - рано или поздно обнаружит, что "локальный" давно стал "любой поток с shell-доступом в вашем кластере". Проверьте uname -r на своих нодах прямо сейчас. Если версия попадает в таблицу выше - патч нужен был вчера.
 
Последнее редактирование модератором:
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

Похожие темы

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →
🧭 Навигатор · ИБ 2026
Не знаешь, какой трек твой?
5 направлений ИБ, реальные зарплаты и точка входа для каждого — в одном треде.
JuniorSenior+
100K → 600K+ ₽ /мес
Открыть навигатор →
🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →
💼 Вакансии и заказы в ИБ
Pentest, SOC, DevSecOps, bug bounty — работа и проекты от проверенных компаний
Карьера в ИБ →

HackerLab