
BPF-LSM mitigazione per CVE-2026-31431 (Copy Fail) — nega la creazione di socket AF_ALG a livello di cluster
Mitigazione BPF-LSM per CVE-2026-31431 ("Copy Fail") e
per la variante RxRPC di Dirty Frag, e
per vulnerabilità simili di escalation dei privilegi che dipendono dall'accesso
da spazio utente a un percorso crittografico in-place lato kernel raggiungibile
tramite le famiglie di socket AF_ALG o AF_RXRPC.
Un piccolo DaemonSet collega un singolo programma BPF-LSM all'hook socket_create
su ogni nodo. Il programma restituisce -EPERM per qualsiasi chiamata
da spazio utente a socket(AF_ALG, ...) o socket(AF_RXRPC, ...),
indipendentemente dalle capability del processo, dal namespace o dal profilo
seccomp. I chiamanti kernel-interni di sock_create_kern() (ad es. fs/afs,
lo stack IPsec) sono lasciati passare, quindi gli utenti kernel legittimi
continuano a funzionare.
Testato su Talos Linux (che include CONFIG_BPF_LSM=y e bpf nello stack LSM
predefinito dalla v1.10), funziona su qualsiasi distribuzione con la stessa
configurazione del kernel.
Copy Fail (CVE-2026-31431) è un difetto logico in algif_aead che consente
a un utente locale non privilegiato di eseguire una scrittura di 4 byte nella
page cache di qualsiasi binario setuid, ottenendo root con uno script Python
di 732 byte. L'exploit richiede solo AF_ALG + splice(), entrambi
raggiungibili da qualsiasi processo non privilegiato per impostazione
predefinita. La correzione mainline è a664bf3d603d.
Dirty Frag è una classe di vulnerabilità successiva, divulgata a maggio
2026 dalla stessa linea di ricerca. Combina due bug che "sporcano" il membro
frag di sk_buff — xfrm-ESP Page-Cache Write e RxRPC Page-Cache Write. La
variante RxRPC esegue una decrittazione in-place pcbc(fcrypt) su una pagina
della page cache bloccata con splice() all'interno di rxkad_verify_packet_1()
e raggiunge root senza bisogno di creare namespace utente, il che la rende la
metà della catena più universalmente sfruttabile sulle distribuzioni
indurite. La correzione per xfrm-ESP è arrivata in netdev come f4c50a4034e6
(2026-05-07); le distribuzioni stanno ancora facendo backport al momento della
scrittura, e RxRPC non ha ancora una correzione pubblica — vedi il
write-up upstream
per la cronologia della divulgazione.
Entrambi gli exploit dipendono dall'apertura di un socket nella famiglia
interessata. Finché le correzioni del kernel non arrivano nella tua
distribuzione, la superficie di attacco può essere rimossa impedendo allo
spazio utente di creare socket AF_ALG o AF_RXRPC. Rispetto alle alternative:
| Mitigazione | Copertura | Riavvio? | Persistente? |
|---|---|---|---|
Parametro kernel module_blacklist=af_alg,rxrpc (gestori di famiglia, non solo algif_aead) | a livello di host | sì | sì |
/etc/modprobe.d/*.conf con install af_alg /bin/false + install rxrpc /bin/false e rmmod dei moduli già caricati (in linea con le indicazioni upstream di Dirty Frag — nota che il semplice blacklist non ferma l'autoload request_module() nel kernel, solo install … /bin/false lo fa) | a livello di host | no | sì (finché il file è presente) |
Kernel personalizzato senza CRYPTO_USER_API / AF_RXRPC | a livello di host | sì | sì |
| Profilo seccomp personalizzato per pod | solo carichi di lavoro etichettati | no | sì |
| copy-fail-blocker (questo progetto) | spazio utente a livello di host | no | finché il DS è attivo |
Questo progetto è l'opzione senza riavvio. Eseguilo a livello di cluster, poi pianifica le correzioni permanenti del kernel con la tua normale cadenza di patch.
Nota sulla variante ESP di Dirty Frag. La metà
xfrm-ESP Page-Cache Writedi Dirty Frag non viene chiusa da questo DaemonSet — viene attivata tramite netlink XFRM +UDP_ENCAP_ESPINUDP, non tramite una famiglia di socket dedicata, e un filtro BPF-LSM pulito per essa romperebbe l'IPsec legittimo sull'host o richiederebbe una logica consapevole dei namespace utente. Non attualmente tracciata qui — i contributi sono benvenuti. Sulle distribuzioni indurite che bloccano i namespace utente non privilegiati (ad es. la policy AppArmor predefinita di Ubuntu), la variante ESP è irraggiungibile in primo luogo e il blocco RxRPC qui è sufficiente.
bpf/blocker.c è un breve programma 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 significa sock_create_kern() — lascia passare i chiamanti in-kernel. */
if (!kern && (family == AF_ALG || family == AF_RXRPC)) // 38, 33
return -EPERM;
return 0;
}
Il loader Go (main.go, ~40 righe) carica il programma e lo collega tramite
bpf(BPF_LINK_CREATE). Il link viene mantenuto per tutta la durata del pod. Su
SIGTERM, il link viene chiuso e l'hook si scollega.
Richiede un kernel compilato con CONFIG_BPF_LSM=y e bpf nello stack LSM
attivo (lsm=...,bpf sulla riga di comando del kernel). Talos Linux include
entrambi abilitati per impostazione predefinita dalla v1.10.
kubectl apply -f https://raw.githubusercontent.com/cozystack/copy-fail-blocker/v0.3.0/manifests/copy-fail-blocker.yaml
Per l'ultimo commit su main (potrebbe includere modifiche non rilasciate):
kubectl apply -f https://raw.githubusercontent.com/cozystack/copy-fail-blocker/main/manifests/copy-fail-blocker.yaml
Il chart non è pubblicato come artefatto OCI (il percorso del registry è condiviso con l'immagine del container). Installa da un checkout taggato:
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
Oppure tramite le scorciatoie del Makefile:
make apply # helm upgrade --install in kube-system
make diff # anteprima delle modifiche rispetto al cluster
make delete # disinstallazione
make manifest # rigenera manifests/copy-fail-blocker.yaml
Il DaemonSet deve essere eseguito con privilegi (carica programmi BPF e scrive
su bpffs). Posizionalo in un namespace con lo standard di sicurezza Pod
Security privilegiato, oppure in kube-system, che è privilegiato per
impostazione predefinita.
Da qualsiasi pod su un nodo coperto:
python3 -c '
import errno, socket
# Passa a ogni famiglia un tipo che la create() specifica della famiglia
# supporta effettivamente (AF_ALG → SOCK_SEQPACKET, AF_RXRPC → SOCK_DGRAM)
# così che su un nodo SENZA questo hook la chiamata avrebbe successo (FAIL:
# socket creato) o fallirebbe con un errno diverso da EPERM — entrambi
# emergono come FAIL sotto.
# Con l'hook attivo, security_socket_create() restituisce -EPERM prima che
# venga eseguita pf->create(), quindi il tipo non ha importanza; passiamo
# comunque quello corretto per mantenere inequivocabile la diagnosi di 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 creato")
except OSError as e:
if e.errno == errno.EPERM:
print(f"OK ({name}): bloccato con EPERM")
else:
print(f"FAIL: {name} ha ottenuto {e.errno} ({e.strerror}), atteso EPERM")'
Output atteso:
OK (AF_ALG): bloccato con EPERM
OK (AF_RXRPC): bloccato con EPERM
Qualsiasi altro errno (ad es. ESOCKTNOSUPPORT 94, EAFNOSUPPORT 97) significa
che l'hook non è attivo su quel nodo — indaga prima di presumere di essere
coperto.
make image # docker buildx build + push
make image REGISTRY=ghcr.io/myorg TAG=v0.3.0 # tag personalizzato
make image PUSH=0 LOAD=1 # build locale senza push
make image aggiorna charts/copy-fail-blocker/values.yaml con il digest
dell'immagine risolto, così il chart fissa sempre per digest.
Le dipendenze di build vivono nel Containerfile (clang, libbpf-dev, Go). L'host
locale richiede solo docker buildx, helm, yq (mikefarah), kubectl e
helm-diff.
charts/copy-fail-blocker/values.yaml:
| Chiave | Predefinito | Note |
|---|---|---|
image.repository | ghcr.io/cozystack/copy-fail-blocker | Aggiornato automaticamente da make image |
image.tag | vX.Y.Z@sha256:... | Fissato per digest, valore corrente in values.yaml |
priorityClassName | system-node-critical | Garantisce che il daemon sopravviva alle sfratti |
tolerations | [{operator: Exists}] | Esegue su ogni nodo, inclusi quelli tainted |
resources.requests | 5m CPU / 16Mi memoria | Impronta a riposo dopo il collegamento |
AF_ALG e AF_RXRPC da spazio utente
sono di nuovo raggiungibili. Per la maggior parte dei modelli di minaccia
questo è accettabile; in caso contrario, considera di fissare il link BPF a
bpffs (non attualmente implementato — i contributi sono benvenuti).CAP_BPF e CAP_SYS_ADMIN sull'host può scollegare
l'hook. Questo non sostituisce le restrizioni dei privilegi a livello di
cluster.algif_skcipher / algif_hash / ecc. Il programma rifiuta
l'intera famiglia AF_ALG, ma solo algif_aead è attualmente noto per
essere sfruttabile. Se una futura CVE richiede un filtro più fine (ad es.
hook su bind() e ispezione di salg_type), è semplice da aggiungere.AF_ALG o AF_RXRPC
aperto. I socket esistenti continuano a funzionare finché non vengono
chiusi.UDP_ENCAP_ESPINUDP, non tramite una famiglia di
socket dedicata — non attualmente tracciato, i contributi sono benvenuti.
Vedi la nota in Perché.AF_RXRPC da spazio utente sono bloccati. RxRPC è il protocollo
di rete AFS. La guardia !kern significa che il modulo in-tree fs/afs
(kAFS) continua a funzionare poiché apre i suoi socket tramite
sock_create_kern(). Gli strumenti AFS da spazio utente (ad es. i daemon
userspace OpenAFS) che aprono un socket AF_RXRPC direttamente tramite
socket(2) verranno negati — non distribuire questo DaemonSet su nodi che
eseguono tali strumenti.Apache License 2.0 — vedi LICENSE.