
Mitigação BPF-LSM para CVE-2026-31431 (Copy Fail) — nega a criação de sockets AF_ALG em todo o cluster
Mitigação BPF-LSM para CVE-2026-31431 ("Copy Fail") e
para a variante RxRPC do Dirty Frag, e
para vulnerabilidades semelhantes de escalonamento de privilégios que dependem
do acesso do espaço do usuário a um caminho de criptografia in-place no lado
do kernel, alcançável através das famílias de sockets AF_ALG ou AF_RXRPC.
Um pequeno DaemonSet anexa um único programa BPF-LSM ao hook socket_create
em cada nó. O programa retorna -EPERM para qualquer chamada do espaço do
usuário a socket(AF_ALG, ...) ou socket(AF_RXRPC, ...),
independentemente de capacidades do processo, namespace ou perfil seccomp.
Chamadores internos do kernel sock_create_kern() (ex.: fs/afs, a pilha
IPsec) são permitidos, então usuários legítimos dentro do kernel continuam
funcionando.
Testado no Talos Linux (que acompanha CONFIG_BPF_LSM=y e bpf na pilha
LSM padrão desde a v1.10), funciona em qualquer distribuição com a mesma
configuração de kernel.
Copy Fail (CVE-2026-31431) é uma falha lógica em algif_aead que permite
a um usuário local sem privilégios realizar uma escrita de 4 bytes no
page-cache de qualquer binário setuid, alcançando root com um script Python
de 732 bytes. O exploit precisa apenas de AF_ALG + splice(), ambos
alcançáveis a partir de qualquer processo sem privilégios por padrão. A
correção no mainline é a664bf3d603d.
Dirty Frag é uma classe de vulnerabilidades subsequente, divulgada em
maio de 2026 pela mesma linha de pesquisa. Ela encadeia dois bugs que
"sujam" o membro frag de sk_buff — xfrm-ESP Page-Cache Write e
RxRPC Page-Cache Write. A variante RxRPC realiza uma descriptografia
in-place pcbc(fcrypt) em uma página do page-cache fixada por splice()
dentro de rxkad_verify_packet_1() e alcança root sem precisar da criação
de user namespaces, o que a torna a metade da cadeia mais universalmente
explorável em distribuições endurecidas. A correção do xfrm-ESP chegou ao
netdev como f4c50a4034e6 (2026-05-07); as distribuições ainda estão
fazendo backport no momento da escrita, e o RxRPC não tem correção pública
ainda — veja o relatório upstream
para a linha do tempo da divulgação.
Ambos os exploits dependem da abertura de um socket na família afetada. Até
que as correções do kernel cheguem à sua distribuição, a superfície de ataque
pode ser removida impedindo que o espaço do usuário crie sockets AF_ALG ou
AF_RXRPC. Comparado às alternativas:
| Mitigação | Cobertura | Reinício? | Persiste? |
|---|---|---|---|
Linha de comando do kernel module_blacklist=af_alg,rxrpc (handlers de família, não apenas algif_aead) | host-wide | sim | sim |
/etc/modprobe.d/*.conf com install af_alg /bin/false + install rxrpc /bin/false e rmmod dos módulos já carregados (segue a orientação upstream do Dirty Frag — note que blacklist simples não impede o autoload request_module() dentro do kernel, apenas install … /bin/false o faz) | host-wide | não | sim (enquanto o arquivo estiver presente) |
Kernel personalizado sem CRYPTO_USER_API / AF_RXRPC | host-wide | sim | sim |
| Perfil seccomp personalizado por pod | apenas workloads rotulados | não | sim |
| copy-fail-blocker (este projeto) | espaço do usuário host-wide | não | enquanto o DS estiver em execução |
Este projeto é a opção sem reinício. Execute-o em todo o cluster e depois planeje as correções permanentes do kernel no seu ritmo normal de patches.
Nota sobre a variante ESP do Dirty Frag. A metade
xfrm-ESP Page-Cache Writedo Dirty Frag não é fechada por este DaemonSet — ela é acionada via netlink XFRM +UDP_ENCAP_ESPINUDP, não através de uma família de sockets dedicada, e um filtro BPF-LSM limpo para ela quebraria o IPsec legítimo no host ou exigiria lógica ciente de user namespaces. Não é rastreada atualmente aqui — contribuições são bem-vindas. Em distribuições endurecidas que bloqueiam user namespaces sem privilégios (ex.: a política AppArmor padrão do Ubuntu), a variante ESP é inalcançável em primeiro lugar e o bloqueio RxRPC aqui é suficiente.
bpf/blocker.c é um programa BPF-LSM curto:
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 significa sock_create_kern() — deixe chamadores do kernel passarem. */
if (!kern && (family == AF_ALG || family == AF_RXRPC)) // 38, 33
return -EPERM;
return 0;
}
O carregador Go (main.go, ~40 linhas) carrega o programa e o anexa via
bpf(BPF_LINK_CREATE). O link é mantido durante toda a vida do pod. No
SIGTERM, o link é fechado e o hook é desanexado.
Requer um kernel compilado com CONFIG_BPF_LSM=y e bpf na pilha LSM ativa
(lsm=...,bpf na linha de comando do kernel). O Talos Linux acompanha ambos
habilitados por padrão desde a v1.10.
kubectl apply -f https://raw.githubusercontent.com/cozystack/copy-fail-blocker/v0.3.0/manifests/copy-fail-blocker.yaml
Para o commit mais recente em main (pode incluir mudanças não lançadas):
kubectl apply -f https://raw.githubusercontent.com/cozystack/copy-fail-blocker/main/manifests/copy-fail-blocker.yaml
O chart não é publicado como artefato OCI (o caminho do registry é compartilhado com a imagem do contêiner). Instale a partir de um checkout com tag:
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
Ou através dos atalhos do Makefile:
make apply # helm upgrade --install em kube-system
make diff # pré-visualizar mudanças contra o cluster
make delete # desinstalar
make manifest # regenerar manifests/copy-fail-blocker.yaml
O DaemonSet deve ser executado com privilégios (ele carrega programas BPF e
escreve em bpffs). Coloque-o em um namespace com o Padrão de Segurança de Pod
privilegiado, ou em kube-system, que é privilegiado por padrão.
De qualquer pod em um nó coberto:
python3 -c '
import errno, socket
# Passe cada família com um tipo que o create() específico da família
# realmente suporta (AF_ALG → SOCK_SEQPACKET, AF_RXRPC → SOCK_DGRAM) para que
# em um nó SEM este hook a chamada ou teria sucesso (FALHA: socket criado)
# ou falharia com um errno não-EPERM — ambos aparecem como FALHA abaixo.
# Com o hook ativo, security_socket_create() retorna -EPERM antes de
# pf->create() ser executado, então o tipo não importa; ainda passamos o
# correto para manter o diagnóstico de FALHA inequívoco.
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"FALHA: socket {name} criado")
except OSError as e:
if e.errno == errno.EPERM:
print(f"OK ({name}): bloqueado com EPERM")
else:
print(f"FALHA: {name} obteve {e.errno} ({e.strerror}), esperado EPERM")'
Saída esperada:
OK (AF_ALG): bloqueado com EPERM
OK (AF_RXRPC): bloqueado com EPERM
Qualquer outro errno (ex.: ESOCKTNOSUPPORT 94, EAFNOSUPPORT 97) significa
que o hook não está ativo naquele nó — investigue antes de assumir que você
está coberto.
make image # docker buildx build + push
make image REGISTRY=ghcr.io/myorg TAG=v0.3.0 # tag personalizada
make image PUSH=0 LOAD=1 # compilar localmente sem push
make image atualiza charts/copy-fail-blocker/values.yaml com o digest da
imagem resolvido, para que o chart sempre fixe por digest.
As dependências de compilação estão no Containerfile (clang, libbpf-dev, Go).
O host local precisa apenas de docker buildx, helm, yq (mikefarah),
kubectl e helm-diff.
charts/copy-fail-blocker/values.yaml:
| Chave | Padrão | Notas |
|---|---|---|
image.repository | ghcr.io/cozystack/copy-fail-blocker | Atualizado automaticamente por make image |
image.tag | vX.Y.Z@sha256:... | Fixado por digest, valor atual em values.yaml |
priorityClassName | system-node-critical | Garante que o daemon sobreviva a evicções |
tolerations | [{operator: Exists}] | Executa em todos os nós, incluindo os com taint |
resources.requests | 5m CPU / 16Mi memória | Pegada ociosa após a anexação |
AF_ALG e AF_RXRPC do espaço
do usuário ficam alcançáveis novamente. Para a maioria dos modelos de
ameaça isso é aceitável; se não for, considere fixar o link BPF em bpffs
(não implementado atualmente — contribuições são bem-vindas).CAP_BPF e CAP_SYS_ADMIN no host pode desanexar
o hook. Isso não substitui restrições de privilégio em todo o cluster.algif_skcipher / algif_hash / etc. O programa rejeita
toda a família AF_ALG, mas apenas algif_aead é atualmente conhecido por
ser explorável. Se um CVE futuro precisar de um filtro mais fino (ex.: hook
bind() e inspecionar salg_type), isso é simples de adicionar.AF_ALG ou AF_RXRPC
aberto. Sockets existentes continuam funcionando até serem fechados.UDP_ENCAP_ESPINUDP, não através de uma família
de sockets dedicada — não é rastreado atualmente, contribuições são
bem-vindas. Veja a nota em Porquê.AF_RXRPC do espaço do usuário são bloqueados. RxRPC é o
protocolo de rede AFS. A proteção !kern significa que o módulo fs/afs
(kAFS) no kernel continua funcionando, pois ele abre seus sockets via
sock_create_kern(). Ferramentas AFS do espaço do usuário (ex.: daemons
OpenAFS do espaço do usuário) que abrem um socket AF_RXRPC diretamente
via socket(2) serão negadas — não implante este DaemonSet em nós que
executam tais ferramentas.Apache License 2.0 — veja LICENSE.