Блог

Copy Fail (CVE-2026-31431): 732 байта Python — и любой пользователь становится root

CopyFail_CVE-2026-31431
CVE / Linux / Безопасность

Copy Fail (CVE-2026-31431): 732 байта Python — и любой пользователь становится root

Исследователь Taeyang Lee из Theori изучал, что происходит, когда Linux-подсистема криптографии получает данные из page cache через системный вызов splice(). Интуиция подсказывала: если непривилегированный пользователь может скормить странице кэша ядра зашифрованный блок, что-то должно пойти не так. Он воспользовался AI-инструментом Xint Code, чтобы прошерстить весь crypto-код, доступный из userspace, — и примерно через час на экране появился CVE-2026-31431.

Copy Fail — логический баг в криптографическом шаблоне authencesn ядра Linux (CWE-787, запись за пределами буфера). Уязвимость позволяет непривилегированному локальному пользователю записать ровно 4 байта в page cache любого доступного для чтения файла. Если целью выбрать setuid-binary — например, /usr/bin/su — это прямой путь к root. CVSS 7.8, High. PoC опубликован 29 апреля 2026 года и умещается в 732 байта Python. Патч уже в mainline ядра; дистрибутивы выпустили обновления через обычный механизм kernel package.

Что делает Copy Fail особенным на фоне остальных LPE-уязвимостей этого года — детерминированность. Dirty Cow требовал выиграть гонку условий и нередко роняла систему при неудаче. Dirty Pipe работал только на определённых версиях ядра. Copy Fail срабатывает без гонок, без повторных попыток, без crash-prone timing windows. Один и тот же 732-байтный скрипт без изменений даёт root на Ubuntu, Amazon Linux, RHEL и SUSE — никаких per-distro офсетов, никакой перекомпиляции. 100% стабильность.

ЧТО ТАКОЕ AF_ALG И PAGE CACHE

AF_ALG — это тип сокета, который открывает криптографическую подсистему ядра для непривилегированного userspace. Вы создаёте сокет, привязываетесь к любому AEAD-шаблону (Authenticated Encryption with Associated Data) и передаёте данные на шифрование или дешифрование. Никаких прав не нужно — это стандартная функциональность, доступная любому пользователю.

Page cache — это слой в памяти ядра, в котором хранятся содержимое файлов. Когда вы читаете файл, ядро помещает его страницы в page cache. Все последующие операции read(), mmap() и execve() работают именно с этим кэшем, а не с диском напрямую. Page cache общий для всей системы — включая все контейнеры на хосте.

Системный вызов splice() позволяет передавать данные между файловыми дескрипторами и пайпами без копирования — передаётся не содержимое, а ссылка на страницу в памяти. Когда пользователь делает splice() файла в AF_ALG-сокет, криптографическая подсистема получает прямые ссылки на страницы page cache этого файла. Не копии — именно те же физические страницы, из которых ядро будет читать файл при следующем execve().

КАК РАБОТАЕТ БАГ

При AEAD-дешифровании через AF_ALG входной поток выглядит так: AAD (ассоциированные данные) || шифротекст || authentication tag. Код в algif_aead.c настраивает операцию in-place: один и тот же scatterlist служит и входом, и выходом. AAD и шифротекст копируются из TX-буфера в RX-буфер через memcpy_sglist. Но tag — последние несколько байт входа — не копируется. Вместо этого страницы page cache с тегом прицепляются к выходному scatterlist через sg_chain(). В итоге выходной scatterlist выглядит так: RX-буфер пользователя || страницы page cache целевого файла.

Именно здесь в игру вступает authencesn. Это AEAD-обёртка для IPsec с поддержкой 64-битных Extended Sequence Numbers. У IPsec последовательный номер разбит на старшую (seqno_hi, байты 0–3 AAD) и младшую (seqno_lo, байты 4–7) части. Для вычисления HMAC authencesn должен переставить эти байты — и для этого использует destination scatterlist как scratch space. Конкретно: функция crypto_authenc_esn_decrypt() выполняет вызов scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1) — запись 4 байт в позицию assoclen + cryptlen, то есть сразу за authentication tag.

В нормальной ситуации это была бы область пользовательского буфера. Но когда tag-страницы прицеплены через sg_chain() к выходному scatterlist — scatterwalk переходит из RX-буфера прямо в page cache целевого файла и записывает туда 4 байта, которые полностью контролирует атакующий. HMAC-проверка провалится, recvmsg() вернёт ошибку — но 4 байта уже записаны. Ядро не помечает страницу dirty для writeback, поэтому файл на диске остаётся нетронутым. Изменение только в памяти — но именно из памяти ядро читает файл при execve().

КАК ЭКСПЛУАТИРУЕТСЯ

Атакующий полностью контролирует три параметра записи: какой файл (любой, доступный для чтения), какое смещение внутри файла (через выбор splice-офсета, длины и assoclen), и какое значение записать (байты 4–7 AAD в sendmsg). Это не «случайная перезапись» — это точечная, повторяемая, программируемая запись произвольного значения в произвольную позицию page cache любого файла.

Эксплойт нацелен на /usr/bin/su — setuid-root бинарник, присутствующий на всех протестированных дистрибутивах. Для начала открывается AF_ALG-сокет и привязывается к шаблону authencesn(hmac(sha256),cbc(aes)). Устанавливается ключ. Принимается request socket. Никаких привилегий — AF_ALG доступен любому пользователю по умолчанию.

Затем для каждого 4-байтного чанка полезной нагрузки конструируется пара sendmsg() + splice(). В AAD закладываются нужные 4 байта (seqno_lo). Splice-дескриптор несёт страницы /usr/bin/su с нужным смещением. Параметры assoclen, длина splice и офсет выбраны так, чтобы scratch-запись попала ровно в нужную позицию в .text-секции бинарника.

Вызов recv() запускает дешифрование. Внутри authencesn происходит scratch-запись в dst[assoclen + cryptlen] — scatterwalk переходит из RX-буфера в page cache и записывает 4 байта. HMAC-проверка немедленно проваливается, recvmsg() возвращает ошибку — но запись уже произошла. После того как все чанки шеллкода уложены, атакующий вызывает execve("/usr/bin/su"). Ядро загружает бинарник из page cache — а там уже шеллкод. Поскольку su setuid-root, код выполняется с UID 0.

КАК БАГ ПРОЖИЛ ДЕВЯТЬ ЛЕТ

История этой уязвимости — учебник по тому, как три независимо безопасных изменения образуют смертельную комбинацию. В 2011 году в ядро добавили authencesn для поддержки IPsec ESP с 64-битными порядковыми номерами (RFC 4303). Scratch-запись в destination scatterlist была безопасной: ассоциированные данные жили в отдельном scatterlist, а единственным вызывающим был внутренний xfrm-слой ядра.

В 2015 году AF_ALG получил поддержку AEAD, а authencesn был перенесён на новый AEAD-интерфейс. Но algif_aead.c тогда работал out-of-place: req->src и req->dst были разными scatterlist’ами. Страницы page cache попадали в src (read-only), scratch-запись шла в dst (буфер пользователя). Всё ещё безопасно.

В 2017 году в algif_aead.c добавили оптимизацию — операции in-place (commit 72548b093ee3). AAD и шифротекст копировались в RX-буфер, но tag-страницы прицеплялись по ссылке через sg_chain(), а потом req->src = req->dst. Страницы page cache теперь оказались в записываемом destination scatterlist. Scratch-запись authencesn пересекла границу буфера и вошла в них. Никто не связал оптимизацию 2017 года со scratch-записью authencesn из 2015-го и splice-path из того же года. Баг существовал на пересечении всех трёх изменений — и оставался невидимым почти десять лет.

ПОЧЕМУ НАХОДЯТ СЕЙЧАС

Copy Fail был найден не классическим fuzzing’ом и не ручным аудитом. Taeyang Lee сформулировал гипотезу — splice() может доставлять страницы page cache в crypto TX scatterlist — и передал её AI-инструменту Xint Code с задачей проверить всю crypto-подсистему на этот паттерн. Через час был готов результат с Copy Fail как наивысшим по severity.

Это часть более широкой тенденции 2026 года: AI-assisted vulnerability research начинает систематически покрывать поверхности атаки, которые годами не проверялись. Пересечение нескольких подсистем — crypto, VFS, socket API — именно тот класс мест, где человек-аудитор мог легко упустить связь между изменениями в разных файлах через несколько лет. Модель видит весь граф вызовов целиком и может проверить конкретную гипотезу за время, недоступное ручному анализу.

ХРОНОЛОГИЯ

23 марта 2026 года Taeyang Lee передал уязвимость команде безопасности ядра Linux. На следующий день пришло подтверждение, через два дня — патч был предложен и проверен. 1 апреля 2026 года патч вошёл в mainline ядро (commit a664bf3d603d). 22 апреля был присвоен CVE-2026-31431. 29 апреля — публичное раскрытие с полным writeup и PoC-эксплойтом на copy.fail.

Координированное раскрытие заняло 37 дней — от репорта до публикации. По меркам ядра Linux это быстро: команда безопасности подтвердила критичность с первых часов и форсировала мёрдж патча. Дистрибутивы выпускали обновления ядра в течение нескольких дней после публикации через штатный механизм kernel package updates.

ПОЧЕМУ ЭТО ВАЖНО

Copy Fail закрывает важную ментальную ловушку: «файл на диске не изменился, значит система чистая». Инструменты контроля целостности, сравнивающие хэши файлов с диском (AIDE в режиме проверки по диску, tripwire), Copy Fail не обнаружат — потому что файл на диске действительно не изменён. Изменён только page cache в памяти, а это то, из чего ядро реально исполняет код. FIM должен дополняться мониторингом anomalous process behavior, а не только файловыми хэшами.

Второй важный аспект — контейнеры. Page cache общий для хоста и всех его контейнеров. Copy Fail — это не только LPE: это container escape primitive. Один скомпрометированный pod с непривилегированным пользователем может получить root на всём Kubernetes-узле, модифицировав setuid-binary в общем page cache хоста. Xint Code анонсировали вторую часть writeup — «From Pod to Host» — именно об этом векторе.

Наконец, 9 лет незамеченного бага в активно развиваемом ядре Linux напоминают: периметр атаки на пересечении нескольких подсистем — самое слепое место аудита. authencesn, AF_ALG и splice() — три компонента, каждый из которых проверялся независимо. Никто не проверял их вместе.

ОБНОВЛЕНИЕ

Патч откатывает оптимизацию 2017 года: req->src теперь указывает на TX SGL (который может содержать страницы page cache из splice), req->dst — на RX SGL (буфер пользователя). Страницы page cache больше не попадают в записываемый destination scatterlist. Patch состоит из трёх upstream-коммитов; главный — a664bf3d603d, который удаляет in-place оптимизацию из algif_aead.c.

На Debian/Ubuntu начните с проверки текущей версии ядра — запомните вывод, чтобы убедиться что после обновления он изменится:

uname -r

Затем обновите и перезагрузитесь:

sudo apt update && sudo apt upgrade -y

После перезагрузки снова выполните uname -r — версия должна измениться. Установленный пакет без перезагрузки не защищает: система продолжает работать на уязвимом ядре в памяти.

Временный workaround через modprobe.d, который широко разошёлся по интернету после публикации, работает только на Debian и Ubuntu. На RHEL, AlmaLinux, CloudLinux и других дистрибутивах семейства RHEL модуль algif_aead собран прямо в ядро и не является загружаемым модулем. Команды выполняются без ошибок, но не делают ничего — и создают ложное ощущение защищённости. Проверить быстро можно так:

modinfo algif_aead | grep filename

Если вывод (builtin) — модуль встроен, workaround через modprobe.d не работает. Если показан путь к файлу .ko — модуль загружаемый, workaround сработает.

Для RHEL-семейства, если немедленное обновление невозможно, CloudLinux рекомендует рабочий временный workaround через initcall_blacklist — он требует перезагрузки, но закрывает attack surface без замены ядра:

sudo grubby --update-kernel=ALL --args="initcall_blacklist=algif_aead_init"
sudo reboot

После перезагрузки проверить, что параметр применён:

sudo grubby --info=ALL | grep initcall_blacklist

Когда патч установлен — workaround убирается и система перезагружается снова:

sudo grubby --update-kernel=ALL --remove-args="initcall_blacklist=algif_aead_init"
sudo reboot

Важно: dm-crypt/LUKS, kTLS, IPsec, SSH и стандартные сборки OpenSSL/GnuTLS не зависят от AF_ALG и не пострадают от этого workaround. Под удар попадут только приложения, явно использующие AF_ALG для AEAD-шифрования — это редкость на типичном web-сервере.

ВЫВОДЫ

Copy Fail — редкий случай уязвимости, где технический механизм одновременно элегантен и разрушителен. Три строки кода в одной функции, добавленные как оптимизация девять лет назад, превратили штатный API шифрования в инструмент записи в page cache любого файла на системе. Результат — детерминированный root за 732 байта Python на всех мажорных дистрибутивах.

Для сисадмина действие одно: обновить ядро и перезагрузиться. Workaround через modprobe.d не трогать, если вы на RHEL-семействе. Для тех, кто работает с Kubernetes: следить за второй частью writeup от Xint — container escape через этот примитив уже задокументирован, детали выходят отдельно.


Какие ядра уязвимы:

Все Linux-ядра выпущенные с 2017 года по март 2026 — то есть ядра версий от 4.14 до 6.18 включительно.

Какие НЕ уязвимы:

Ядра старше 2017 года (до версии ~4.14) — в них не было in-place оптимизации, которая создала баг. CloudLinux 7 (классический) — также не затронут.

Исправлено начиная с:

ДистрибутивПервая безопасная версия ядра
Debian / Ubuntuлюбое ядро с патчем после 1 апреля 2026 — через apt upgrade
RHEL 8 / AlmaLinux 8 / CL84.18.0-553.121.1.el8
RHEL 9 / AlmaLinux 9 / CL95.14.0-611.49.2.el9_7
RHEL 10 / AlmaLinux 10 / CL106.12.0-124.52.2.el10_1
Amazon Linux 2023через dnf upgrade после апреля 2026

Проверить свою версию:

uname -r

Leave your thought here

Ваш адрес email не будет опубликован. Обязательные поля помечены *