Defense-in-depth toolkit for the Copy Fail Linux kernel bug class.
Covers three live LPE chains that share the same splice() →
MSG_SPLICE_PAGES → in-place page-cache write primitive:
| CVE | Sink | Primitive | |
|---|---|---|---|
| cf1 | CVE-2026-31431 | algif_aead AEAD scratch-write |
4-byte STORE via seqno_lo |
| cf2 | (no CVE yet) | esp_input skip_cow |
4-byte STORE via seq_hi |
| Dirty Frag | (embargo broken, no CVE) | esp_input + rxkad_verify_packet_1 |
4-byte and 8-byte STORE |
Userspace primitives stack into a single dnf install: an LD_PRELOAD
shim, a kernel-module-entry-point cut, kernel-enforced systemd
restrictions, and a read-only host posture auditor that reports
per-class coverage. Signed RPMs for EL8 / EL9 / EL10.
Install · Verify · Coverage · Defense in depth · Audit · Subpackages · Overrides · Signatures · Limitations
Note
Upgrading from afalg-defense v1.0.x or copyfail-defense v2.0.0 is a single
command: dnf upgrade copyfail-defense. The v2.0.0 -> v2.0.1 path
auto-suppresses any conflicting drop-ins detected on your host
(see Auto-detection below).
sudo curl -sSL https://rfxn.github.io/copyfail/copyfail.repo \
-o /etc/yum.repos.d/copyfail.repo
sudo dnf install -y copyfail-defense
sudo /usr/sbin/copyfail-shim-enableOne repo file works on EL8/EL9/EL10. RPMs are GPG-signed; dnf imports the public key on first use. Cross-check the fingerprint when prompted:
6001 1CDC EA2F F52D 975A FDEE 6D30 F32C D5E8 0F80
The meta package pulls four subpackages:
| Subpackage | Coverage |
|---|---|
copyfail-defense-shim |
LD_PRELOAD AF_ALG block (cf1 primary) |
copyfail-defense-modprobe |
kernel-module entry-point cuts (cf1 + cf2 + Dirty Frag) |
copyfail-defense-systemd |
per-unit RestrictAddressFamilies=~AF_ALG ~AF_RXRPC + RestrictNamespaces=~user ~net (all three classes) |
copyfail-defense-auditor |
read-only host posture auditor with per-class coverage report |
Auditor only (no LD_PRELOAD, for hot infrastructure):
sudo dnf install -y copyfail-defense-auditorAfter install + activation:
# cf1: AF_ALG socket creation should fail
python3 -c 'import socket; socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)'
# expect: PermissionError [Errno 1] Operation not permitted
# Holistic per-class coverage report
sudo copyfail-local-checkThe auditor renders a surface-area matrix at the bottom showing, per bug-class, whether the kernel sink is reachable on this host AND which mitigation layers are active:
Surface area / mitigation matrix:
Class Sink reachable? Mitigated? Active layers
cf1 (CVE-2026-31431) YES yes ld_preload_shim, systemd_af_alg, modprobe_blacklist
cf2 (xfrm-ESP) YES yes modprobe_blacklist, systemd_restrict_ns
Dirty Frag-ESP YES yes modprobe_blacklist, systemd_restrict_ns
Dirty Frag-RxRPC YES yes modprobe_blacklist, systemd_af_rxrpc
Bug-class coverage: cf1=mitigated cf2=mitigated dirtyfrag-esp=mitigated dirtyfrag-rxrpc=mitigated
sudo copyfail-local-check # human-readable, only flags non-OK
sudo copyfail-local-check --json # SIEM ingestion (posture.bug_classes_covered)
sudo copyfail-local-check --emit-remediation # bash script of suggested fixesRead-only by design: writes only to mkdtemp() sentinels, never modifies
/usr/bin or /etc, runs unprivileged (some checks degrade gracefully
without root). Five categories: ENV, KERNEL, MITIGATION,
HARDENING, DETECTION.
Exit codes (unchanged from v1.0.1): 0 clean · 2 VULN
(no userspace mitigation) · 3 VULN-but-mitigated · 4 hardening
recommendations only.
JSON output (--json) includes:
posture.bug_classes_covered: array of class IDs where mitigation is active. Single SIEM filter for "is this host hardened against the cf class?"posture.bug_classes: per-class map withapplicable,mitigated,kernel_sink, and per-layer activation booleans for dashboards.posture.verdict: headline string from v1.0.x, preserved for backwards compat.
sudo /usr/sbin/copyfail-shim-disable
sudo dnf remove copyfail-defense%preun scrubs /etc/ld.so.preload on full erase as a safety net,
and the modprobe drop file is removed; %config(noreplace) means the
operator's hand-edits to systemd drop-ins survive package upgrade.
Which rung blocks which bug class. ✅ = primary mitigation; · = not applicable; superscripts mark caveated coverage (notes below).
Rows below are mitigation rungs the package installs. Operator-applied hardening (suid lockdown, auditd rules) is in its own table below because no subpackage performs those actions; the auditor only recommends them conditionally.
| Mitigation rung | cf1 | cf2 | DF-ESP | DF-RxRPC |
|---|---|---|---|---|
LD_PRELOAD shim (AF_ALG hook) |
✅ | · | · | ¹ |
modprobe algif_aead family |
² | · | · | · |
modprobe esp4 esp6 xfrm_user xfrm_algo |
· | ✅ | ✅ | · |
modprobe rxrpc |
· | · | · | ✅ |
systemd RestrictAddressFamilies=~AF_ALG |
✅ | · | · | · |
systemd RestrictAddressFamilies=~AF_RXRPC |
· | · | · | ✅ |
systemd RestrictNamespaces=~user ~net |
· | ✅ | ✅ | · |
¹ Catches the cksum step in the public DF-RxRPC PoC, not the kernel
sink itself. Useful as defense-in-depth, not as a primary stop.
² No-op on RHEL stock kernels: CRYPTO_USER_API* is built-in, so the
blacklist line cannot prevent load. Listed for completeness on custom
or non-RHEL kernels where algif_aead ships modular.
| Class | Upstream patch | Audit signature |
|---|---|---|
| cf1 | a664bf3d |
socket(a0=38) |
| cf2 | f4c50a4034 |
unshare(NEWUSER) |
| DF-ESP | f4c50a4034 |
unshare(NEWUSER) |
| DF-RxRPC | none upstream | add_key("rxrpc",...) |
The auditor emits a page-cache integrity probe (cached IOC) for every
class; see --json posture.bug_classes[*].kernel_sink.
These are surfaced via --emit-remediation. No subpackage applies
them, since each can break legitimate workloads on a busy fleet.
Review every line before pasting.
| Action | Targets | When the auditor recommends it |
|---|---|---|
chmod 4750 /usr/bin/su && chgrp wheel /usr/bin/su |
cf2, DF-ESP | Suppressed when /etc/passwd shows non-wheel/admin interactive users (cPanel-style tenant fleets); chmod 4750 would break their su workflow. |
auditd rule cf_userns (unshare(CLONE_NEWUSER)) |
cf2, DF-ESP | Hosts where auditd is tuned for userns events (otherwise high alert noise). |
auditd rule cf_addkey (add_key("rxrpc",...)) |
DF-RxRPC | Always; rxrpc keyring activity is rare enough that the false-positive rate stays low. |
auditd rule afalg_attempt (socket(a0=38)) |
cf1 | Hosts already running auditd; pairs with the LD_PRELOAD shim as a tripwire. |
🔬 Full writeup: Copy Fail (CVE-2026-31431) on rfxn.com/research covers cf1 kernel mechanics; cf2 and Dirty Frag extend the same primitive to two more sinks.
The Copy Fail bug class is a deterministic page-cache-write
primitive: an unprivileged process uses splice() to plant a
read-only page-cache page (e.g., /etc/passwd or /usr/bin/su)
into a sender skb's frag slot, the receiver path performs in-place
crypto on top of that frag, and the resulting STORE writes
attacker-controlled bytes into the page cache. The on-disk file is
unchanged; the corruption lives in RAM until eviction.
| Kernel sink | Privilege needed | Module | |
|---|---|---|---|
| cf1 (CVE-2026-31431) | algif_aead AEAD scratch-write |
none | algif_aead (RHEL: builtin) |
| cf2 ("Electric Boogaloo") | esp_input skip_cow path |
CAP_NET_ADMIN via unshare(NEWUSER|NEWNET) |
esp4, xfrm_user (RHEL: modules) |
| Dirty Frag-ESP | same as cf2 | same as cf2 | same as cf2 |
| Dirty Frag-RxRPC | rxkad_verify_packet_1 in-place pcbc(fcrypt) |
none | rxrpc (Ubuntu: loaded; RHEL: not in core) |
The same primitive shape, three different kernel sinks. Every layer in this toolkit is independently useful; none is a silver bullet on its own.
Each rung defeats the bug by a different mechanism, so an attack that defeats one doesn't necessarily defeat the next:
| Rung | Where it fails | What the next rung covers |
|---|---|---|
| Kernel patch (vendor) | EL7 EOL; EL8/9/10 patch rollout lags disclosure days-to-weeks; production reboot may not be available; Dirty Frag-RxRPC has no upstream patch | Userspace cuts close the window without a reboot |
| modprobe blacklist | No-op when the relevant module is builtin (RHEL algif_aead is); no effect on already-resident modules |
Functional for esp4/esp6/xfrm_user/xfrm_algo/rxrpc on stock RHEL kernels (these are modules) |
systemd RestrictAddressFamilies/RestrictNamespaces |
Reaches only services systemd starts post-restriction. Misses cron-jobs running as root, sshd-pre-restriction, container payloads with their own pid 1 | LD_PRELOAD shim covers every dyn-linked process regardless of init |
| LD_PRELOAD shim | Static binaries; processes issuing syscall instruction directly; SUID binaries (kernel strips LD_PRELOAD for secure-exec) |
seccomp at unit/runtime level catches direct-syscall path |
| seccomp filter | Per-service. Operationally heavy: each unit/runtime needs explicit policy | This package's systemd subpackage ships a one-line filter for the highest-leverage tenant units |
Where the shim itself fails (static binaries, direct syscall
instruction, SUID stripping) is attacker engineering territory.
The other rungs fail under routine operator reality: vendors
haven't shipped yet, the kernel was built with builtin crypto, the
threat surface includes a cron job. That asymmetry is the case for
deploying every rung this package ships.
| Package | Arch | Contents |
|---|---|---|
copyfail-defense |
x86_64 | meta, pulls all four below |
copyfail-defense-shim |
x86_64 | /usr/lib64/no-afalg.so + copyfail-shim-{enable,disable} |
copyfail-defense-modprobe |
noarch | /etc/modprobe.d/99-copyfail-defense.conf (cf-class entry-point cuts) |
copyfail-defense-systemd |
noarch | drop-ins for user@/sshd/cron/crond/atd + container-runtime examples |
copyfail-defense-auditor |
noarch | /usr/sbin/copyfail-local-check (Python, stdlib-only, read-only) |
Per-EL binary RPMs are independently compiled against each
distribution's glibc (EL8: 2.28 with split libdl; EL9/EL10: 2.34+
with merged libdl). Do not cross-install across ELs. Direct
download links + sha256s:
rfxn.github.io/copyfail.
v2.0.1+ inspects the host at install time for workloads the default cuts would break, and suppresses the conflicting drop-in only while keeping every other layer active.
Three workload classes are detected:
| Workload | Detection signals (any) | Suppresses |
|---|---|---|
| IPsec (strongSwan, libreswan, openswan) | systemctl is-enabled returns enabled for strongswan/strongswan-starter/strongswan-swanctl/ipsec/libreswan/openswan/pluto; OR /etc/ipsec.conf has a conn stanza; OR non-empty /etc/swanctl/conf.d/, /etc/ipsec.d/, /etc/strongswan/conf.d/, /etc/strongswan.d/ |
99-copyfail-defense-cf2-xfrm.conf (esp4, esp6, xfrm_user, xfrm_algo blacklist) |
| AFS (openafs, kafs) | systemctl is-enabled for openafs-client/openafs-server/kafs/afsd; OR /etc/openafs/CellServDB or /etc/openafs/ThisCell exists; OR /etc/krb5.conf.d/openafs* exists; OR /proc/fs/afs/ registered |
99-copyfail-defense-rxrpc.conf (rxrpc modprobe blacklist) AND 12-copyfail-defense-rxrpc-af.conf (RestrictAddressFamilies=~AF_RXRPC on all 5 tenant units) - preserves AFS userspace tooling like aklog |
| Rootless containers (rootless podman/buildah) | /home/*/.local/share/containers/storage/overlay-containers/ present (rootless podman storage tree, recent mtime); OR /var/lib/containers/storage/ non-empty with mtime <90d; OR /run/user/<UID>/containers/ for any UID >= 1000; OR podman.socket enabled (system or per-user) |
15-copyfail-defense-userns.conf on user@.service.d ONLY (other tenant units stay protected) |
Note: /etc/subuid populated by useradd is NOT a rootless
detection signal in v2.0.1 rev 2 - shadow-utils auto-populates
subuid for every regular user regardless of container intent,
which produced near-100% false positives on cPanel-shaped fleets.
The detection now requires active rootless usage (storage tree,
runtime tmpfs, or enabled podman.socket).
Detection runs in %posttrans after every install/upgrade and writes
a structured report to /var/lib/copyfail-defense/auto-detect.json
(schema versioned). The auditor consumes this and surfaces the
decision under posture.auto_detect.
If you enable IPsec / AFS / rootless containers post-install:
sudo /usr/sbin/copyfail-redetect
sudo systemctl daemon-reload
sudo systemctl try-reload-or-restart sshd.serviceThe helper re-runs detection, refreshes auto-detect.json, and
copies/removes the conditional drop-in files in /etc/. It does
NOT auto-reload systemd - the operator decides when running
services pick up the change.
Drop a sentinel file before dnf install (or before
copyfail-redetect) to skip detection entirely:
sudo mkdir -p /etc/copyfail
sudo touch /etc/copyfail/force-full
sudo dnf install -y copyfail-defenseThe auditor reports force-full sentinel active when this is on.
Remove the sentinel and re-run copyfail-redetect to re-engage
detection.
systemd drop-ins use the standard layered-override pattern.
Within a <unit>.service.d/ directory, files merge in
lexicographic order, and lower numbers lose to higher numbers
for =value directives (later files override earlier ones).
copyfail-defense ships at 10-, 12-, 15-; the standard
operator escape hatches sit at 20- and 25-:
20-override.conf (empty-value to neutralize a directive):
drop a 20-override.conf next to our files with empty values for
any directive you want to relax. Survives package upgrade because
20-override.conf is operator-owned (RPM doesn't manage it).
sudo mkdir -p /etc/systemd/system/user@.service.d
sudo tee /etc/systemd/system/user@.service.d/20-override.conf >/dev/null <<'EOF'
[Service]
RestrictNamespaces=
RestrictAddressFamilies=
EOF
sudo systemctl daemon-reloadEmpty = clears the union for list-valued directives like
RestrictAddressFamilies and RestrictNamespaces.
25-additions.conf (add a new directive on top of ours):
drop a 25-additions.conf next to our files with directives you
want to add. Sorts after 20- so it can layer on top of an
empty-override. Use this for fleet-wide hardening that goes
beyond the cf-class scope.
sudo tee /etc/systemd/system/sshd.service.d/25-additions.conf >/dev/null <<'EOF'
[Service]
NoNewPrivileges=true
EOF
sudo systemctl daemon-reload
sudo systemctl try-reload-or-restart sshd.servicemodprobe override: the conditional 99-copyfail-defense-cf2-xfrm.conf
and 99-copyfail-defense-rxrpc.conf files are managed by detect.sh
(cmp-and-skip per [SPEC §12.10.2a / D-57]). If you hand-edit a
conditional file, detect.sh detects the divergence on next
%posttrans or copyfail-redetect, logs a WARN, and preserves
your edits (does not overwrite). For the always-on
99-copyfail-defense-cf1.conf file, edits survive package upgrade
via %config(noreplace).
The earlier (incorrect) recommendation to chattr +i a managed
file is removed - it broke dnf via EPERM on the next
install -m 0644 from %posttrans. Use the cmp-and-skip
behavior or force-full instead.
1.0.1+ and 2.0.0+ are signed by the Copyfail Project Signing Key.
The .repo file enforces both gpgcheck=1 (per-RPM) and
repo_gpgcheck=1 (detached repomd.xml.asc over the metadata), so a
stock dnf install does end-to-end verification automatically.
fingerprint: 6001 1CDC EA2F F52D 975A FDEE 6D30 F32C D5E8 0F80
uid: Copyfail Project Signing Key <proj@rfxn.com>
key file: https://rfxn.github.io/copyfail/RPM-GPG-KEY-copyfail
Out-of-band verification of a downloaded RPM:
curl -sSL https://rfxn.github.io/copyfail/RPM-GPG-KEY-copyfail \
| sudo rpm --import /dev/stdin
rpm -K copyfail-defense-2.0.1-1.el9.x86_64.rpm
# expect: digests signatures OK--json emits a structured object. The headline fields:
{
"schema_version": "2.0",
"covers": ["CVE-2026-31431", "cf2-xfrm-esp", "dirtyfrag-esp", "dirtyfrag-rxrpc"],
"posture": {
"verdict": "vulnerable_kernel_userspace_mitigated",
"bug_classes_covered": ["cf1", "cf2", "dirtyfrag-esp"],
"bug_classes": {
"cf1": { "applicable": true, "mitigated": true, "kernel_sink": "...", "layers": {...} },
"cf2": { "applicable": true, "mitigated": true, "kernel_sink": "...", "layers": {...} },
"dirtyfrag-esp": { "applicable": true, "mitigated": true, "kernel_sink": "...", "layers": {...} },
"dirtyfrag-rxrpc": { "applicable": true, "mitigated": false, "kernel_sink": "...", "layers": {...} }
},
"layers": { ... },
"auto_detect": {
"available": true,
"suppressed_modprobe": [],
"suppressed_systemd": []
}
}
}bug_classes_covered is the SIEM-ergonomic single filter ("is this
host hardened?"). bug_classes map exposes per-layer breakdown for
finer dashboards. verdict and layers from v1.0.x are preserved for
backwards compatibility.
--emit-remediation prints a bash script aggregating per-check
remediation hints. Output is fully commented by default; review
every block before pasting (chmod on suid binaries, modprobe blacklist,
unprivileged-userns sysctl are policy-dependent or require a reboot to
undo).
no-afalg.c is single-file, no build system. Tested on EL7
(gcc 4.8 / glibc 2.17), EL8 (gcc 8.5 / glibc 2.28), EL9 (gcc 11.5 /
glibc 2.34), and EL10 (gcc 14 / glibc 2.39). x86_64 only.
gcc -shared -fPIC -O2 -Wall -Wextra \
-o /usr/lib64/no-afalg.so no-afalg.c -ldlTo rebuild the RPMs from the published SRPM (under your own signing):
mock -r centos-stream+epel-9-x86_64 --rebuild \
https://github.com/rfxn/copyfail/releases/download/v2.0.1/copyfail-defense-2.0.1-1.el9.src.rpmThe spec lives at packaging/copyfail-defense.spec.
- x86_64 only. The shim has architecture asserts; the auditor's trigger probe struct layout is x86_64. Patches welcome for arm64.
- The userspace shim is irrelevant to static binaries and syscall-instruction issuers. Other rungs (modprobe, systemd RestrictNamespaces, kernel patch) cover those.
- Dirty Frag-RxRPC has no upstream patch as of v2.0.0 ship date.
Mitigation is the
rxrpcmodprobe blacklist + systemdRestrictAddressFamilies=~AF_RXRPCuntil upstream merges V4bel's proposed gate (skb_cloned(skb) || skb->data_len). - modprobe blacklists do not unload already-resident modules. The
package's
%post modprobedoes a best-effortrmmod; reboot to fully clear. - Auditor's trigger probe is destructive only against its own
sentinel; it will not corrupt anything you would notice. It will,
however, briefly load
algif_aeadand friends if they aren't already loaded (which is the point). auditdrules (cf_userns, cf_addkey) are emitted by--emit-remediation, not installed by the package. Auditd rules cause unnecessary alerting on hosts where auditd is not tuned for them; operator action required.
GPL v2. See LICENSE.
rfxn.com | forged in prod | Ryan MacDonald