
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
Cualquier otro errno (p. ej. ESOCKTNOSUPPORT 94, EAFNOSUPPORT 97) significa
que el hook no está activo en ese nodo — investiga antes de asumir que estás
cubierto.
make image # docker buildx build + push
make image REGISTRY=ghcr.io/myorg TAG=v0.3.0 # etiqueta personalizada
make image PUSH=0 LOAD=1 # compila localmente sin push
make image actualiza charts/copy-fail-blocker/values.yaml con el
digest de imagen resuelto para que el chart siempre fije por digest.
Las dependencias de compilación viven en el Containerfile (clang, libbpf-dev,
Go). El host local solo necesita docker buildx, helm, yq (mikefarah),
kubectl y helm-diff.
charts/copy-fail-blocker/values.yaml:
| Clave | Predeterminado | Notas |
|---|---|---|
image.repository | ghcr.io/cozystack/copy-fail-blocker | Actualizado automáticamente por make image |
image.tag | vX.Y.Z@sha256:... | Fijado por digest, valor actual en values.yaml |
priorityClassName | system-node-critical | Garantiza que el daemon sobreviva a desalojos |
tolerations | [{operator: Exists}] | Se ejecuta en cada nodo, incluidos los contaminados |
resources.requests | 5m CPU / 16Mi memory | Huella en reposo después del adjunto |
AF_ALG y AF_RXRPC de espacio de
usuario vuelven a ser accesibles. Para la mayoría de los modelos de amenaza
esto es aceptable; si no, considera fijar el enlace BPF a bpffs (no
implementado actualmente — se aceptan contribuciones).CAP_BPF y CAP_SYS_ADMIN en el host puede desadjuntar
el hook. Esto no sustituye las restricciones de privilegios en todo el
clúster.algif_skcipher / algif_hash / etc. El programa rechaza
toda la familia AF_ALG, pero solo se sabe que algif_aead es explotable
actualmente. Si un CVE futuro necesita un filtro más fino (p. ej. hookear
bind() e inspeccionar salg_type), esto es sencillo de añadir.AF_ALG o AF_RXRPC
abierto. Los sockets existentes siguen funcionando hasta que se cierran.UDP_ENCAP_ESPINUDP, no mediante una familia de sockets
dedicada — no se rastrea actualmente, se aceptan contribuciones. Consulta la
nota en Por qué.AF_RXRPC de espacio de usuario quedan bloqueados. RxRPC
es el protocolo de red de AFS. La protección !kern significa que el módulo
fs/afs (kAFS) del árbol sigue funcionando ya que abre sus sockets mediante
sock_create_kern(). Las herramientas AFS de espacio de usuario (p. ej.
daemons de espacio de usuario de OpenAFS) que abren un socket AF_RXRPC
directamente mediante socket(2) serán denegadas — no despliegues este
DaemonSet en nodos que ejecuten dichas herramientas.Apache License 2.0 — consulta LICENSE.