
BPF-LSM-Mitigation für CVE-2026-31431 (Copy Fail) — verweigert die Erstellung von AF_ALG-Sockets clusterweit
BPF-LSM-Abschwächung für CVE-2026-31431 („Copy Fail“) und
die RxRPC-Variante von Dirty Frag sowie
für ähnliche Privilege-Escalation-Schwachstellen, die von Benutzerraum-Zugriff
auf einen kernelseitigen In-Place-Krypto-Pfad abhängen, der über die
Socket-Familien AF_ALG oder AF_RXRPC erreichbar ist.
Ein kleiner DaemonSet bindet ein einzelnes BPF-LSM-Programm an den
socket_create-Hook auf jedem Knoten. Das Programm gibt -EPERM für jeden
Benutzerraum-socket(AF_ALG, ...)- oder socket(AF_RXRPC, ...)-Aufruf
zurück, unabhängig von Prozess-Capabilities, Namespace oder Seccomp-Profil.
Kernelinterne sock_create_kern()-Aufrufer (z. B. fs/afs, der IPsec-Stack)
werden durchgelassen, sodass legitime Kernel-Benutzer weiterhin funktionieren.
Getestet auf Talos Linux (das seit v1.10 mit CONFIG_BPF_LSM=y und bpf im
Standard-LSM-Stack ausgeliefert wird), funktioniert auf jeder Distribution mit
derselben Kernel-Konfiguration.
Copy Fail (CVE-2026-31431) ist ein Logikfehler in algif_aead, der es
einem nicht privilegierten lokalen Benutzer ermöglicht, einen 4-Byte-Seitencache-
Schreibzugriff auf eine beliebige setuid-Binärdatei durchzuführen und mit einem
732-Byte-Python-Skript Root-Rechte zu erlangen. Der Exploit benötigt nichts
weiter als AF_ALG + splice(), beides ist standardmäßig von jedem nicht
privilegierten Prozess erreichbar. Der Mainline-Fix ist a664bf3d603d.
Dirty Frag ist eine Folge-Schwachstellenklasse, die im Mai 2026 von
derselben Forschungslinie offengelegt wurde. Sie verkettet zwei Bugs, die das
frag-Element von sk_buff „verschmutzen“ — xfrm-ESP Page-Cache Write und
RxRPC Page-Cache Write. Die RxRPC-Variante führt eine In-Place-pcbc(fcrypt)-
Entschlüsselung auf einer splice()-gepinnten Seitencache-Seite innerhalb von
rxkad_verify_packet_1() durch und erreicht Root, ohne dass eine
User-Namespace-Erstellung erforderlich ist, was sie zur universeller
ausnutzbaren Hälfte der Kette auf gehärteten Distributionen macht. Der
xfrm-ESP-Fix landete in netdev als f4c50a4034e6 (2026-05-07); Distributionen
portieren zum Zeitpunkt der Erstellung dieses Textes noch zurück, und für RxRPC
gibt es noch keinen öffentlichen Fix — siehe den
Upstream-Bericht
für den Offenlegungszeitplan.
Beide Exploits hängen vom Öffnen eines Sockets in der betroffenen Familie ab.
Bis die Kernel-Fixes in Ihrer Distribution eintreffen, kann die Angriffsfläche
entfernt werden, indem verhindert wird, dass Benutzerraum jemals AF_ALG- oder
AF_RXRPC-Sockets erstellt. Im Vergleich zu Alternativen:
Dieses Projekt ist die Option ohne Neustart. Führen Sie es clusterweit aus und planen Sie dann die dauerhaften Kernel-Fixes in Ihrem normalen Patch-Rhythmus.
Hinweis zur ESP-Variante von Dirty Frag. Die
xfrm-ESP Page-Cache Write- Hälfte von Dirty Frag wird von diesem DaemonSet nicht geschlossen — sie wird über XFRM-Netlink +UDP_ENCAP_ESPINUDPausgelöst, nicht über eine dedizierte Socket-Familie, und ein sauberer BPF-LSM-Filter dafür würde entweder legitimes IPsec auf dem Host brechen oder eine User-Namespace-bewusste Logik erfordern. Derzeit nicht hier verfolgt — Beiträge willkommen. Auf gehärteten Distributionen, die nicht privilegierte User-Namespaces blockieren (z. B. Ubuntus Standard-AppArmor-Richtlinie), ist die ESP-Variante von vornherein unerreichbar und der RxRPC-Block hier ist ausreichend.
bpf/blocker.c ist ein kurzes BPF-LSM-Programm:
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 bedeutet sock_create_kern() — Kernel-Aufrufer durchlassen. */
if (!kern && (family == AF_ALG || family == AF_RXRPC)) // 38, 33
return -EPERM;
return 0;
}
Der Go-Loader (main.go, ~40 Zeilen) lädt das Programm und bindet es über
bpf(BPF_LINK_CREATE) an. Der Link wird für die Lebensdauer des Pods gehalten.
Bei SIGTERM wird der Link geschlossen und der Hook löst sich.
Erfordert einen Kernel, der mit CONFIG_BPF_LSM=y und bpf im aktiven LSM-
Stack (lsm=...,bpf in der Kernel-Befehlszeile) gebaut wurde. Talos Linux wird
seit v1.10 standardmäßig mit beidem ausgeliefert.
kubectl apply -f https://raw.githubusercontent.com/cozystack/copy-fail-blocker/v0.3.0/manifests/copy-fail-blocker.yaml
Für den neuesten Commit auf main (kann unveröffentlichte Änderungen enthalten):
kubectl apply -f https://raw.githubusercontent.com/cozystack/copy-fail-blocker/main/manifests/copy-fail-blocker.yaml
Das Chart ist nicht als OCI-Artefakt veröffentlicht (der Registry-Pfad wird mit dem Container-Image geteilt). Installation aus einem getaggten Checkout:
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
Oder über die Makefile-Verknüpfungen:
make apply # helm upgrade --install in kube-system
make diff # Änderungen gegen den Cluster anzeigen
make delete # Deinstallation
make manifest # manifests/copy-fail-blocker.yaml neu generieren
Der DaemonSet muss privilegiert laufen (er lädt BPF-Programme und schreibt in
bpffs). Platzieren Sie ihn in einem Namespace mit dem privilegierten
Pod-Security-Standard oder in kube-system, das standardmäßig privilegiert ist.
Von jedem Pod auf einem abgedeckten Knoten:
python3 -c '
import errno, socket
# Jede Familie mit einem Typ übergeben, den die familien-spezifische create()
# tatsächlich unterstützt (AF_ALG → SOCK_SEQPACKET, AF_RXRPC → SOCK_DGRAM),
# sodass auf einem Knoten OHNE diesen Hook der Aufruf entweder erfolgreich
# wäre (FAIL: Socket erstellt) oder mit einem Nicht-EPERM-errno fehlschlägt —
# beides erscheint unten als FAIL. Bei aktivem Hook gibt
# security_socket_create() -EPERM zurück, bevor pf->create() läuft, daher
# spielt der Typ keine Rolle; wir übergeben trotzdem den korrekten, um die
# FAIL-Diagnose eindeutig zu halten.
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")'
Erwartete Ausgabe:
OK (AF_ALG): blocked with EPERM
OK (AF_RXRPC): blocked with EPERM
Jeder andere errno (z. B. ESOCKTNOSUPPORT 94, EAFNOSUPPORT 97) bedeutet,
dass der Hook auf diesem Knoten nicht aktiv ist — untersuchen Sie das, bevor Sie
annehmen, dass Sie abgedeckt sind.
make image # docker buildx build + push
make image REGISTRY=ghcr.io/myorg TAG=v0.3.0 # benutzerdefiniertes Tag
make image PUSH=0 LOAD=1 # lokal bauen ohne zu pushen
make image aktualisiert charts/copy-fail-blocker/values.yaml mit dem
aufgelösten Image-Digest, sodass das Chart immer per Digest pinnt.
Build-Abhängigkeiten befinden sich in der Containerfile (clang, libbpf-dev, Go).
Der lokale Host benötigt nur docker buildx, helm, yq (mikefarah),
kubectl und helm-diff.
charts/copy-fail-blocker/values.yaml:
AF_ALG und AF_RXRPC
wieder erreichbar sind. Für die meisten Bedrohungsmodelle ist das akzeptabel;
wenn nicht, erwägen Sie, den BPF-Link an bpffs zu pinnen (derzeit nicht
implementiert — Beiträge willkommen).CAP_BPF und CAP_SYS_ADMIN auf dem Host kann den Hook
lösen. Dies ist kein Ersatz für clusterweite Privilegienbeschränkungen.algif_skcipher / algif_hash / usw. Das Programm lehnt
die gesamte AF_ALG-Familie ab, aber nur algif_aead ist derzeit bekanntermaßen
ausnutzbar. Wenn ein zukünftiges CVE einen feineren Filter benötigt (z. B.
bind() hooken und salg_type prüfen), ist das unkompliziert hinzuzufügen.AF_ALG- oder
AF_RXRPC-Socket halten. Bestehende Sockets funktionieren weiter, bis sie
geschlossen werden.Apache License 2.0 — siehe LICENSE.
| Abschwächung | Abdeckung | Neustart? | Bleibt bestehen? |
|---|
Kernel-Cmdline module_blacklist=af_alg,rxrpc (Familien-Handler, nicht nur algif_aead) | hostweit | ja | ja |
/etc/modprobe.d/*.conf mit install af_alg /bin/false + install rxrpc /bin/false und rmmod bereits geladener Module (entspricht der Upstream-Dirty-Frag-Anleitung — beachten Sie, dass ein einfaches blacklist das In-Kernel-request_module()-Autoload nicht stoppt, nur install … /bin/false tut das) | hostweit | nein | ja (solange die Datei vorhanden ist) |
Benutzerdefinierter Kernel ohne CRYPTO_USER_API / AF_RXRPC | hostweit | ja | ja |
| Benutzerdefiniertes Seccomp-Profil pro Pod | nur gekennzeichnete Workloads | nein | ja |
| copy-fail-blocker (dieses Projekt) | hostweiter Benutzerraum | nein | solange der DS läuft |
| Schlüssel | Standard | Hinweise |
|---|
image.repository | ghcr.io/cozystack/copy-fail-blocker | Automatisch aktualisiert durch make image |
image.tag | vX.Y.Z@sha256:... | Per Digest gepinnt, aktueller Wert in values.yaml |
priorityClassName | system-node-critical | Stellt sicher, dass der Daemon Räumungen überlebt |
tolerations | [{operator: Exists}] | Läuft auf jedem Knoten, einschließlich tainteter |
resources.requests | 5m CPU / 16Mi memory | Leerlauf-Fußabdruck nach dem Anhängen |
UDP_ENCAP_ESPINUDP erreicht, nicht über eine dedizierte
Socket-Familie — derzeit nicht verfolgt, Beiträge willkommen. Siehe den Hinweis
unter Warum.AF_RXRPC-Clients werden blockiert. RxRPC ist das
AFS-Netzwerkprotokoll. Die !kern-Absicherung bedeutet, dass das In-Tree-
fs/afs-Modul (kAFS) weiterhin funktioniert, da es seine Sockets über
sock_create_kern() öffnet. Benutzerraum-AFS-Tooling (z. B. OpenAFS-
Benutzerraum-Daemons), das einen AF_RXRPC-Socket direkt über socket(2)
öffnet, wird verweigert — setzen Sie diesen DaemonSet nicht auf Knoten ein,
die solches Tooling ausführen.