
BPF-LSM mitigación para CVE-2026-31431 (Copy Fail) — deniega la creación de sockets AF_ALG en todo el clúster
Mitigación BPF-LSM para CVE-2026-31431 ("Copy Fail") y
la variante RxRPC de Dirty Frag, y
para vulnerabilidades similares de escalada de privilegios que dependen del
acceso de espacio de usuario a una ruta criptográfica in-place del lado del
kernel accesible a través de las familias de sockets AF_ALG o
AF_RXRPC.
Un pequeño DaemonSet adjunta un único programa BPF-LSM al hook socket_create
en cada nodo. El programa devuelve -EPERM para cualquier llamada
socket(AF_ALG, ...) o socket(AF_RXRPC, ...) de espacio de usuario,
independientemente de las capacidades del proceso, el namespace o el perfil
seccomp. Los llamadores internos del kernel sock_create_kern() (p. ej.
fs/afs, la pila IPsec) sí pueden pasar, por lo que los usuarios legítimos
dentro del kernel siguen funcionando.
Probado en Talos Linux (que incluye CONFIG_BPF_LSM=y y bpf en la pila
LSM predeterminada desde v1.10), funciona en cualquier distribución con la
misma configuración de kernel.
Copy Fail (CVE-2026-31431) es un fallo lógico en algif_aead que permite
a un usuario local sin privilegios realizar una escritura de 4 bytes en la
caché de páginas de cualquier binario setuid, logrando root con un script
Python de 732 bytes. El exploit solo necesita AF_ALG + splice(), ambos
accesibles desde cualquier proceso sin privilegios por defecto. El fix en
mainline es a664bf3d603d.
Dirty Frag es una clase de vulnerabilidad posterior divulgada en mayo de
2026 por la misma línea de investigación. Encadena dos bugs que "ensucian" el
miembro frag de sk_buff — xfrm-ESP Page-Cache Write y RxRPC Page-Cache Write. La variante RxRPC realiza un descifrado pcbc(fcrypt) in-place sobre
una página de caché de páginas fijada con splice() dentro de
rxkad_verify_packet_1() y alcanza root sin necesidad de crear namespaces de
usuario, lo que la convierte en la mitad más explotable universalmente de la
cadena en distribuciones endurecidas. El fix de xfrm-ESP llegó a netdev
como f4c50a4034e6 (2026-05-07); las distribuciones aún están haciendo
backport al momento de escribir esto, y RxRPC no tiene fix público todavía —
consulta el write-up upstream
para la cronología de divulgación.
Ambos exploits dependen de abrir un socket en la familia afectada. Hasta que
los fixes del kernel lleguen a tu distribución, la superficie de ataque se
puede eliminar impidiendo que el espacio de usuario cree sockets AF_ALG o
AF_RXRPC. Comparado con las alternativas:
| Mitigación | Cobertura | ¿Reinicio? | ¿Persiste? |
|---|---|---|---|
Línea de comandos del kernel module_blacklist=af_alg,rxrpc (manejadores de familia, no solo algif_aead) | todo el host | sí | sí |
/etc/modprobe.d/*.conf con install af_alg /bin/false + install rxrpc /bin/false y rmmod de los módulos ya cargados (coincide con la guía upstream de Dirty Frag — ten en cuenta que blacklist simple no detiene el autoload request_module() dentro del kernel, solo install … /bin/false lo hace) | todo el host | no | sí (mientras el archivo esté presente) |
Kernel personalizado sin CRYPTO_USER_API / AF_RXRPC | todo el host | sí | sí |
| Perfil seccomp personalizado por pod | solo cargas de trabajo etiquetadas | no | sí |
| copy-fail-blocker (este proyecto) | espacio de usuario en todo el host | no | mientras el DS se ejecute |
Este proyecto es la opción sin reinicio. Ejecútalo en todo el clúster y luego planifica los fixes permanentes del kernel en tu cadencia habitual de parches.
Nota sobre la variante ESP de Dirty Frag. La mitad
xfrm-ESP Page-Cache Writede Dirty Frag no se cierra con este DaemonSet — se activa a través de netlink XFRM +UDP_ENCAP_ESPINUDP, no mediante una familia de sockets dedicada, y un filtro BPF-LSM limpio para ella rompería el IPsec legítimo en el host o requeriría lógica consciente de namespaces de usuario. Actualmente no se rastrea aquí — se aceptan contribuciones. En distribuciones endurecidas que bloquean namespaces de usuario sin privilegios (p. ej. la política AppArmor predeterminada de Ubuntu), la variante ESP es inalcanzable de todos modos y el bloqueo de RxRPC aquí es suficiente.
bpf/blocker.c es un programa BPF-LSM corto:
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() — deja pasar a los llamadores del kernel. */
if (!kern && (family == AF_ALG || family == AF_RXRPC)) // 38, 33
return -EPERM;
return 0;
}
El cargador Go (main.go, ~40 líneas) carga el programa y lo adjunta mediante
bpf(BPF_LINK_CREATE). El enlace se mantiene durante toda la vida del pod. En
SIGTERM, el enlace se cierra y el hook se desadjunta.
Requiere un kernel compilado con CONFIG_BPF_LSM=y y bpf en la pila LSM
activa (lsm=...,bpf en la línea de comandos del kernel). Talos Linux incluye
ambos habilitados por defecto desde v1.10.
kubectl apply -f https://raw.githubusercontent.com/cozystack/copy-fail-blocker/v0.3.0/manifests/copy-fail-blocker.yaml
Para el último commit en main (puede incluir cambios no publicados):
kubectl apply -f https://raw.githubusercontent.com/cozystack/copy-fail-blocker/main/manifests/copy-fail-blocker.yaml
El chart no se publica como artefacto OCI (la ruta del registro se comparte con la imagen del contenedor). Instala desde un checkout etiquetado:
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
O mediante los atajos del Makefile:
make apply # helm upgrade --install en kube-system
make diff # previsualiza los cambios contra el clúster
make delete # desinstala
make manifest # regenera manifests/copy-fail-blocker.yaml
El DaemonSet debe ejecutarse con privilegios (carga programas BPF y escribe
en bpffs). Colócalo en un namespace con el Estándar de Seguridad de Pods
privilegiado, o en kube-system, que es privilegiado por defecto.
Desde cualquier pod en un nodo cubierto:
python3 -c '
import errno, socket
# Pasa cada familia con un tipo que el create() específico de la familia
# realmente soporte (AF_ALG → SOCK_SEQPACKET, AF_RXRPC → SOCK_DGRAM) para que
# en un nodo SIN este hook la llamada tenga éxito (FAIL: socket creado)
# o falle con un errno distinto de EPERM — ambos aparecen como FAIL abajo.
# Con el hook activo, security_socket_create() devuelve -EPERM antes de que
# se ejecute pf->create(), por lo que el tipo no importa; aun así pasamos
# el correcto para mantener el diagnóstico de FAIL 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"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")'
Salida esperada:
OK (AF_ALG): blocked with EPERM
OK (AF_RXRPC): blocked with EPERM