
BPF-LSM смягчение для CVE-2026-31431 (Copy Fail) — запрещает создание сокетов AF_ALG в масштабе всего кластера
BPF-LSM-смягчение для CVE-2026-31431 («Copy Fail») и
RxRPC-варианта Dirty Frag, а также
для аналогичных уязвимостей повышения привилегий, которые зависят от
пользовательского доступа к внутриядерному криптопути с модификацией на месте,
достижимому через семейства сокетов AF_ALG или AF_RXRPC.
Небольшой DaemonSet подключает одну BPF-LSM-программу к хуку socket_create
на каждом узле. Программа возвращает -EPERM для любого пользовательского
вызова socket(AF_ALG, ...) или socket(AF_RXRPC, ...), независимо от
привилегий процесса, пространства имён или seccomp-профиля. Внутриядерные
вызывающие sock_create_kern() (например, fs/afs, стек IPsec)
пропускаются, поэтому легитимные внутриядерные пользователи продолжают работать.
Протестировано на Talos Linux (который поставляется с CONFIG_BPF_LSM=y и bpf в
стандартном LSM-стеке начиная с v1.10), работает на любом дистрибутиве с такой же
конфигурацией ядра.
Copy Fail (CVE-2026-31431) — логическая ошибка в algif_aead, которая позволяет
непривилегированному локальному пользователю выполнить 4-байтовую запись в page-cache
любого setuid-бинарника, получая root с помощью Python-скрипта размером 732 байта.
Эксплойту нужны только AF_ALG + splice(), оба из которых доступны из любого
непривилегированного процесса по умолчанию. Исправление в mainline — a664bf3d603d.
Dirty Frag — класс последующих уязвимостей, раскрытый в мае 2026 года той же
исследовательской линией. Он связывает две ошибки, которые «загрязняют» член frag
структуры sk_buff — xfrm-ESP Page-Cache Write и RxRPC Page-Cache Write.
RxRPC-вариант выполняет расшифровку pcbc(fcrypt) на месте над страницей page-cache,
закреплённой через splice(), внутри rxkad_verify_packet_1() и достигает root без
необходимости создания пользовательского пространства имён, что делает его более
универсально эксплуатируемой половиной цепочки на укреплённых дистрибутивах.
Исправление для xfrm-ESP попало в netdev как f4c50a4034e6 (2026-05-07);
дистрибутивы всё ещё переносят его на момент написания, а для RxRPC публичного
исправления пока нет — см. описание от исследователей
для хронологии раскрытия.
Оба эксплойта зависят от открытия сокета в затронутом семействе. Пока исправления
ядра не попали в ваш дистрибутив, поверхность атаки можно устранить, запретив
пользовательскому пространству создавать сокеты AF_ALG или AF_RXRPC.
По сравнению с альтернативами:
Этот проект — вариант без перезагрузки. Запустите его в кластере, затем планируйте постоянные исправления ядра в обычном цикле обновлений.
Примечание о ESP-варианте Dirty Frag. Половина
xfrm-ESP Page-Cache Writeиз Dirty Frag не закрывается этим DaemonSet — она срабатывает через XFRM netlink +UDP_ENCAP_ESPINUDP, а не через отдельное семейство сокетов, и чистый BPF-LSM-фильтр для неё либо сломает легитимный IPsec на хосте, либо потребует логики с учётом пользовательских пространств имён. Сейчас здесь не отслеживается — вклад приветствуется. На укреплённых дистрибутивах, блокирующих непривилегированные пользовательские пространства имён (например, политика AppArmor по умолчанию в Ubuntu), ESP-вариант в принципе недостижим, и блокировки RxRPC здесь достаточно.
bpf/blocker.c — короткая BPF-LSM-программа:
SEC("lsm/socket_create")
int BPF_PROG(block_socket_family, int family, int type, int protocol,
int kern, int ret)
{
if (ret)
return ret;
/* kern != 0 означает sock_create_kern() — пропускаем внутриядерных вызывающих. */
if (!kern && (family == AF_ALG || family == AF_RXRPC)) // 38, 33
return -EPERM;
return 0;
}
Загрузчик на Go (main.go, ~40 строк) загружает программу и подключает её через
bpf(BPF_LINK_CREATE). Ссылка удерживается в течение всего времени жизни пода. При
SIGTERM ссылка закрывается, и хук отсоединяется.
Требуется ядро, собранное с CONFIG_BPF_LSM=y и bpf в активном LSM-стеке
(lsm=...,bpf в параметрах ядра). Talos Linux поставляется с обоими
включёнными по умолчанию начиная с v1.10.
kubectl apply -f https://raw.githubusercontent.com/cozystack/copy-fail-blocker/v0.3.0/manifests/copy-fail-blocker.yaml
Для последнего коммита в main (может содержать невыпущенные изменения):
kubectl apply -f https://raw.githubusercontent.com/cozystack/copy-fail-blocker/main/manifests/copy-fail-blocker.yaml
Чарт не опубликован как OCI-артефакт (путь реестра общий с образом контейнера). Установка из тегированного клона:
git clone --branch v0.3.0 https://github.com/cozystack/copy-fail-blocker
cd copy-fail-blocker
helm upgrade --install copy-fail-blocker charts/copy-fail-blocker \
--namespace kube-system
Или через сокращения Makefile:
make apply # helm upgrade --install в kube-system
make diff # предпросмотр изменений относительно кластера
make delete # удаление
make manifest # перегенерация manifests/copy-fail-blocker.yaml
DaemonSet должен работать с привилегиями (он загружает BPF-программы и пишет в
bpffs). Разместите его в пространстве имён со стандартом Pod Security Standard
«privileged» или в kube-system, которое привилегировано по умолчанию.
Из любого пода на покрытом узле:
python3 -c '
import errno, socket
# Передаём каждому семейству тип, который семейственный create() реально
# поддерживает (AF_ALG → SOCK_SEQPACKET, AF_RXRPC → SOCK_DGRAM), чтобы
# на узле БЕЗ этого хука вызов либо завершился успехом (FAIL: сокет
# создан), либо ошибкой с errno, отличным от EPERM — оба случая
# отображаются как FAIL ниже.
# При активном хуке security_socket_create() возвращает -EPERM до
# выполнения pf->create(), поэтому тип не важен; мы всё равно передаём
# правильный, чтобы диагностика FAIL была однозначной.
for name, family, stype in [("AF_ALG", 38, socket.SOCK_SEQPACKET),
("AF_RXRPC", 33, socket.SOCK_DGRAM)]:
try:
socket.socket(family, stype, 0)
print(f"FAIL: {name} socket created")
except OSError as e:
if e.errno == errno.EPERM:
print(f"OK ({name}): blocked with EPERM")
else:
print(f"FAIL: {name} got {e.errno} ({e.strerror}), expected EPERM")'
Ожидаемый вывод:
OK (AF_ALG): blocked with EPERM
OK (AF_RXRPC): blocked with EPERM
Любой другой errno (например, ESOCKTNOSUPPORT 94, EAFNOSUPPORT 97) означает,
что хук не активен на этом узле — разберитесь, прежде чем считать себя защищённым.
make image # docker buildx build + push
make image REGISTRY=ghcr.io/myorg TAG=v0.3.0 # пользовательский тег
make image PUSH=0 LOAD=1 # локальная сборка без push
make image обновляет charts/copy-fail-blocker/values.yaml с
разрешённым digest образа, поэтому чарт всегда закрепляется по digest.
Зависимости сборки находятся в Containerfile (clang, libbpf-dev, Go). Локальному
хосту нужны только docker buildx, helm, yq (mikefarah), kubectl и
helm-diff.
charts/copy-fail-blocker/values.yaml:
AF_ALG и AF_RXRPC снова достижимы.
Для большинства моделей угроз это приемлемо; если нет — рассмотрите закрепление
BPF-ссылки в bpffs (сейчас не реализовано — вклад приветствуется).CAP_BPF и CAP_SYS_ADMIN на хосте может отсоединить хук.
Это не замена ограничениям привилегий в масштабе кластера.algif_skcipher / algif_hash и т.д. Программа отклоняет
всё семейство AF_ALG, но в настоящее время известно, что эксплуатируемым
является только algif_aead. Если будущая CVE потребует более тонкого фильтра
(например, хука bind() и проверки salg_type), это несложно добавить.AF_ALG или AF_RXRPC.
Существующие сокеты продолжают работать до закрытия.Apache License 2.0 — см. LICENSE.
| Смягчение | Покрытие | Перезагрузка? | Сохраняется? |
|---|
Параметр ядра module_blacklist=af_alg,rxrpc (обработчики семейств, не только algif_aead) | весь хост | да | да |
/etc/modprobe.d/*.conf с install af_alg /bin/false + install rxrpc /bin/false и rmmod уже загруженных модулей (соответствует рекомендациям Dirty Frag — обратите внимание, что обычный blacklist не останавливает внутриядерную автозагрузку request_module(), только install … /bin/false это делает) | весь хост | нет | да (пока файл присутствует) |
Пользовательское ядро без CRYPTO_USER_API / AF_RXRPC | весь хост | да | да |
| Пользовательский seccomp-профиль для каждого пода | только помеченные рабочие нагрузки | нет | да |
| copy-fail-blocker (этот проект) | весь хост, пользовательское пространство | нет | пока работает DS |
| Ключ | По умолчанию | Примечания |
|---|
image.repository | ghcr.io/cozystack/copy-fail-blocker | Автообновляется через make image |
image.tag | vX.Y.Z@sha256:... | Закреплён по digest, текущее значение в values.yaml |
priorityClassName | system-node-critical | Обеспечивает выживание демона при вытеснениях |
tolerations | [{operator: Exists}] | Работает на каждом узле, включая с taints |
resources.requests | 5m CPU / 16Mi memory | Простой след после подключения |
UDP_ENCAP_ESPINUDP, а не через отдельное семейство сокетов — сейчас не
отслеживается, вклад приветствуется. См. примечание в разделе Зачем.AF_RXRPC блокируются. RxRPC — сетевой протокол AFS.
Ограничение !kern означает, что встроенный модуль fs/afs (kAFS)
продолжает работать, поскольку открывает свои сокеты через sock_create_kern().
Пользовательские инструменты AFS (например, пользовательские демоны OpenAFS),
открывающие сокет AF_RXRPC напрямую через socket(2), будут отклонены — не
разворачивайте этот DaemonSet на узлах, где работает такое ПО.