copyfail / afalg-defense

Latest: v1.0.1 EL8 / EL9 / EL10 x86_64 CVE-2026-31431 GPL v2

Userspace defense against CVE-2026-31431 ("Copy Fail") - an AF_ALG / authencesn page-cache corruption local privilege-escalation primitive that leaves no on-disk artefacts. Ships an LD_PRELOAD shim that blocks AF_ALG socket creation in every dynamic-linked process, plus a comprehensive read-only host posture auditor.

Designed to be a viable primary defense for the windows where the other rungs of the ladder cannot reach: the vendor kernel patch is not yet available (or you cannot reboot in that window), modprobe blacklist is a no-op because algif_aead is builtin (the RHEL default), or systemd RestrictAddressFamilies does not cover the threat surface (cron, login shells, container payloads). One .so + one ld.so.preload line, system-wide.

🔬 Read the full writeup: Copy Fail (CVE-2026-31431) on rfxn.com/research — kernel-level mechanics of the AEAD over-read, the splice-into-page-cache path that turns it into a privesc primitive, and why the userspace shim closes the practical attacker windows.

Add the repo (EL8 / EL9 / EL10)

A single copyfail.repo file works for all three: dnf expands $releasever + $basearch per host, and v1.0.1+ RPMs are GPG-signed (full gpgcheck=1 + repo_gpgcheck=1).

sudo curl -sSL https://rfxn.github.io/copyfail/copyfail.repo \
  -o /etc/yum.repos.d/copyfail.repo
sudo dnf install -y afalg-defense

After install the LD_PRELOAD shim is on disk but not yet active. Wiring /etc/ld.so.preload from a package post-install is too dangerous - one broken upgrade locks every dynamic-linked binary out of dlopen. Activation is an explicit operator action:

sudo /usr/sbin/copyfail-shim-enable    # smoke-tests, then writes /etc/ld.so.preload
sudo /usr/sbin/copyfail-shim-disable   # reverses it

Also blacklist the AF_ALG modules (where loadable)

The package also ships an equally low-barrier mitigation: a modprobe drop-in that severs algif_aead, authenc, and authencesn at the kernel level. When the kernel exposes AF_ALG as a loadable module - most stock mainline kernels do - this stacks with the shim and deserves equal weight. The shim blocks every userspace caller at libc; the blacklist removes the kernel attack surface entirely. Different mechanisms, both one-line operator actions.

# Decide whether the blacklist will be effective on this kernel.
ls /sys/module/algif_aead 2>/dev/null && echo "modular - blacklist effective" \
    || echo "builtin or absent - shim is your primary defense"
grep -E 'ALG_USERMODE|CRYPTO_USER_API' /boot/config-$(uname -r) 2>/dev/null
# =m -> modular (blacklist is primary)   =y -> builtin (shim is primary)

# If modular, drop a blacklist into /etc/modprobe.d/ and unload
# anything already resident. The CVE-2026-31431 chain is the trio at
# the bottom; the algif_* family is added for general AF_ALG hygiene.
sudo tee /etc/modprobe.d/99-no-afalg.conf >/dev/null <<'EOF'
install af_alg          /bin/false
install algif_aead      /bin/false
install algif_skcipher  /bin/false
install algif_hash      /bin/false
install algif_rng       /bin/false
install authenc         /bin/false
install authencesn      /bin/false
EOF
sudo rmmod algif_aead authenc authencesn 2>/dev/null || true

(The package also ships this file under /usr/share/doc/afalg-defense/examples/no-afalg-modprobe.conf - same content, copy that into place if you prefer the audit trail.) Where your kernel allows it, deploy both the shim and the blacklist - belt-and-suspenders coverage for the price of two sudo commands.

Verify the block

python3 -c 'import socket; socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)'
# expect: PermissionError [Errno 1] Operation not permitted

Removal

Always disable before uninstalling so RPM does not erase the .so out from under /etc/ld.so.preload:

sudo /usr/sbin/copyfail-shim-disable
sudo dnf remove afalg-defense afalg-defense-shim afalg-defense-auditor

The package's %preun scriptlet also scrubs /etc/ld.so.preload on full erase as a safety net, but disabling first keeps the operation transparent.

Subpackages

PackageArchContents
afalg-defensex86_64meta - pulls shim + auditor
afalg-defense-shimx86_64 /usr/lib64/no-afalg.so + copyfail-shim-{enable,disable} helpers
afalg-defense-auditornoarch /usr/sbin/copyfail-local-check (Python, stdlib-only, read-only)

Auditor-only is a valid install pattern for hosts that do not yet trust an LD_PRELOAD on every dynamic-linked process:

sudo dnf install -y afalg-defense-auditor

Auditor quick-start

The auditor is read-only. It writes only to mkdtemp() sentinel files, never modifies /usr/bin or /etc, and runs unprivileged (some checks degrade gracefully without root). Five categories: ENV, KERNEL, MITIGATION, HARDENING, DETECTION.

copyfail-local-check                       # human-readable, only flags non-OK
copyfail-local-check --verbose             # show passing checks too
copyfail-local-check --json                # SIEM-friendly: posture.verdict + per-check details
copyfail-local-check --skip-trigger        # no live AF_ALG probe
copyfail-local-check --skip-hardening      # skip suid/page-cache audit
copyfail-local-check --emit-remediation    # print a bash script of suggested fixes

JSON output is structured for fleet ingest. The posture.verdict field is the headline; consume that, not the human report:

{
  "schema_version": "1.1",
  "tool": "copyfail_checker",
  "cve": "CVE-2026-31431",
  "checks": [ ...per-check details... ],
  "posture": {
    "verdict": "patched | vulnerable | vulnerable_kernel_userspace_mitigated |
                kernel_likely_safe | inconclusive",
    "layers": {
      "kernel_patched":      "ok | missing | skipped | not_evaluated",
      "af_alg_unreachable":  "...",
      "modprobe_blacklist":  "...",
      "ld_preload_shim":     "...",
      "systemd_restriction": "...",
      "user_service_dropin": "...",
      "seccomp_runtime":     "...",
      "auditd_running":      "...",
      "audit_rule_af_alg":   "..."
    }
  },
  "exit_code": 0
}

Exit codes: 0 clean, 2 VULN (no userspace mitigation), 3 VULN-but-mitigated, 4 hardening recommendations only.

Direct downloads

Each release on github.com/rfxn/copyfail/releases ships per-EL binary RPMs, an SRPM, the .repo file, and the public signing key as release assets. The same files are published in the dnf tree below.

The .repo file (works on EL8/EL9/EL10):

sudo curl -sSL https://rfxn.github.io/copyfail/copyfail.repo \
  -o /etc/yum.repos.d/copyfail.repo

And the public signing key (dnf imports this automatically on first install, but you can pre-import it):

sudo curl -sSL https://rfxn.github.io/copyfail/RPM-GPG-KEY-copyfail \
  | sudo rpm --import /dev/stdin

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.

EL8 (RHEL/Alma/Rocky/CentOS Stream 8)

Filesha256
afalg-defense-1.0.1-1.el8.x86_64.rpm 3c1129956b52ca451eb928d304f9b4bd2e99eb70e8f52c03e486d1c843cbd652
afalg-defense-shim-1.0.1-1.el8.x86_64.rpm c0613d5569536edeee9f172d8b370128c0eef0081032f0745695487a7a61015e
afalg-defense-auditor-1.0.1-1.el8.noarch.rpm 61e7ddefbfb0d5b03f012221cda4fdba3cf471f53c250825d7b43ea153810021
afalg-defense-1.0.1-1.el8.src.rpm 9a0aa3049508893ede159fd31a21480ccb9eb7cdede4c68bbe190a7f6721e7ad

EL9 (RHEL/Alma/Rocky/CentOS Stream 9)

Filesha256
afalg-defense-1.0.1-1.el9.x86_64.rpm 381fc32223e22abc6db63bf72bd2403e66ed2fe59759c0b3afe0281c37db3eab
afalg-defense-shim-1.0.1-1.el9.x86_64.rpm 833f891a30f596872c393e979475cc9563d18f941ce58e038610f24bcd431fff
afalg-defense-auditor-1.0.1-1.el9.noarch.rpm adf0addeddd188871fb98bbb761ac1b9bdfafc5a653cc33451f41f3dd0715181
afalg-defense-1.0.1-1.el9.src.rpm 0326696f9ee0aa7b9cb14f4ae8f2fa5774257906dc32ad7280cebd691fd8dbf7

EL10 (RHEL 10 / CentOS Stream 10 / AlmaLinux Kitten)

Filesha256
afalg-defense-1.0.1-1.el10.x86_64.rpm 046ec488b1be69f161f52b52a65ccf5bba0eccde6895eb9ec6eceebd59242b31
afalg-defense-shim-1.0.1-1.el10.x86_64.rpm 86caff2df7633da369c6e5e35c2a431c46d29d05ee13b0691f07fc3461a480eb
afalg-defense-auditor-1.0.1-1.el10.noarch.rpm 2476ad91466e7716a0940d5b5080c416607bac6ea77a9e819f5b8563e3b7de30
afalg-defense-1.0.1-1.el10.src.rpm fac98d3b442178ea13d3abd449e18ed83f2e3623cb6118bc5194e5fc4aa4997a

Signing

1.0.1 and later are signed by the Copyfail Project Signing Key:

fingerprint: 6001 1CDC EA2F F52D 975A  FDEE 6D30 F32C D5E8 0F80
uid:         Copyfail Project Signing Key <proj@rfxn.com>
key file:    /RPM-GPG-KEY-copyfail

The copyfail.repo sets both gpgcheck=1 (verifies each RPM) and repo_gpgcheck=1 (verifies repomd.xml via the detached repomd.xml.asc we publish alongside it). dnf imports the public key from gpgkey= on first use.

Verify out-of-band:

curl -sSL https://rfxn.github.io/copyfail/RPM-GPG-KEY-copyfail | gpg --import
gpg --fingerprint proj@rfxn.com
# expect: 6001 1CDC EA2F F52D 975A  FDEE 6D30 F32C D5E8 0F80
rpm --import https://rfxn.github.io/copyfail/RPM-GPG-KEY-copyfail
rpm -K /path/to/afalg-defense-1.0.1-1.el9.x86_64.rpm
# expect: digests signatures OK

Where this rung carries weight on its own

Every layer of defense for AF_ALG-class bugs has failure modes. The case for this package is that the conditions that defeat the rungs above it are not the same conditions that defeat the shim. That asymmetry is why the shim is a viable primary defense rather than just a backup.

RungWhere it failsWhat the shim does there
1. Kernel patch (vendor) EL7 EOL; EL8/9/10 rollout lags disclosure by days to weeks; can't always reboot in the hot window Closes the window without a reboot. Live install, no kernel touch
2. modprobe blacklist of algif_aead / authenc / authencesn Only when these are loaded as modules (not builtin) and not already resident from boot. On modular kernels (most stock mainline), this is an equally low-barrier primary defense that stacks with the shim. Becomes a no-op when algif_aead is builtin (RHEL default) Picks up the slack on builtin-crypto kernels - every userspace caller still goes through libc socket(2)
3. systemd RestrictAddressFamilies Misses cron, sshd login shells, containers with their own pid 1, pre-restriction units Global - /etc/ld.so.preload applies to every dyn-linked process regardless of init
4. LD_PRELOAD shim (this package) Static binaries; direct syscall instruction; SUID strip (see right column for coverage)
5. seccomp filter Per-unit; operationally heavy: explicit policy per service One .so, one preload line, whole host

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 lives in a cron job. Deploy this rung first; layer the others as they become available.

Full writeup is in the README.

Reporting issues

Bugs, packaging problems, false positives, missing distros - please file a GitHub issue. Include the auditor's --json output if the report is about detection, and the output of uname -a + cat /etc/os-release for any runtime question.