
Educational rewrite of the Copy Fail PoC (CVE-2026-31431) — Linux kernel LPE via algif_aead in-place crypto + splice() page-cache write
Educational rewrite of the Theori/Xint proof-of-concept for CVE-2026-31431,
a local privilege escalation vulnerability in the Linux kernel's algif_aead
crypto socket interface. Affects every mainline kernel from 4.14 (July 2017)
through 6.18.21, and the 6.19 stable branch through 6.19.11.
For educational and authorized security research only. Do NOT run on systems you do not own or do not have explicit written permission to test. Running this on a vulnerable host gives any unprivileged local user immediate root access. The authors take no responsibility for misuse.
The Linux kernel has exposed an in-kernel cryptography API to userspace since
kernel 3.2 via the AF_ALG socket family (<linux/if_alg.h>). Userspace
opens a SOCK_SEQPACKET socket, binds it to an algorithm template (e.g.
"aead" / "authencesn(hmac(sha256),cbc(aes))"), and sends data for
encryption or decryption.
In 2017, a performance optimisation was merged that allows AEAD algorithms to encrypt or decrypt data in-place when the kernel detects that the source and destination share the same underlying pages. This avoids a redundant memory copy on each operation.
The authencesn template (which adds Extended Sequence Number support to
authenticated encryption) contains a scratch-write during decryption: it
writes ESN bytes into a specific offset within the operation's data buffer
before the authentication check runs. When the operation's source pages were
obtained from the page cache of a regular file via splice(), and the
in-place optimisation is active, this scratch write goes directly back into
those cached pages.
Because the kernel's page cache is a shared mapping — any process that opens the same file sees the same physical pages — an unprivileged user can:
/usr/bin/su) with O_RDONLY.AF_ALG operation socket.authencesn decryption; the ESN scratch write corrupts the
page at a controlled offset with attacker-controlled bytes.Off-host integrity scanners (AIDE, Tripwire, IMA/EVM) that compare checksums of on-disk files are completely blind to this attack. The write vanishes after a page-cache eviction or reboot, leaving no forensic trace on disk.
By targeting /usr/bin/su (a setuid root binary) and overwriting a branch
that guards the PAM authentication check, the attacker obtains a passwordless
root shell.
The optimisation commit that introduced the bug landed in the 4.14 development
cycle (July 2017). Any kernel compiled with CONFIG_CRYPTO_USER_API_AEAD=y
and CONFIG_CRYPTO_AUTHENC=y (both common defaults) in the above ranges is
vulnerable.
Check your distribution's security tracker for the exact advisory status.
os.splice was added in Python 3.10)/usr/bin/su, world-readable
as it is setuid)# Show what would be written without touching the kernel (safe):
python3 copy_fail_exploit.py --dry-run
# Patch /usr/bin/su page cache (vulnerable kernel required):
# WARNING: vulnerable kernel required -- use only on systems you own
python3 copy_fail_exploit.py
# Patch and immediately escalate to root:
python3 copy_fail_exploit.py --spawn-shell
# Use a custom target binary and payload:
python3 copy_fail_exploit.py --target /usr/bin/sudo --payload-file ./custom_patch.bin
# Verbose output (print each 4-byte write):
python3 copy_fail_exploit.py --verbose --dry-run
By default --spawn-shell is off. After patching, the script prints:
[+] Patch applied to page cache of '/usr/bin/su' (disk unchanged).
Run `su` to escalate, or re-run with --spawn-shell.
To restore: echo 3 | sudo tee /proc/sys/vm/drop_caches
You must opt in to the shell spawn with --spawn-shell. This is intentional:
it forces you to understand what the exploit did before executing the result.
Setting up the primitive.
The exploit opens an AF_ALG socket and binds it to the
authencesn(hmac(sha256),cbc(aes)) template with a dummy all-zero key. It
then configures the authentication tag size to 4 bytes via ALG_SET_AEAD_AUTHSIZE.
accept() on the control socket yields an operation socket that can submit
individual decrypt requests.
Triggering the page-cache write.
For each 4-byte chunk of the patch payload, the exploit calls sendmsg() on
the operation socket with MSG_MORE set, passing 8 bytes of associated data
(AAD): 4 bytes of filler followed by the 4 payload bytes. MSG_MORE tells the
kernel to hold the operation until more data arrives. It then creates a pipe
and issues two splice() calls — file_fd → pipe → op_sock — donating the
file's own page-cache pages as the input to the decryption operation. When
recv() is finally called, the kernel runs authencesn decryption. The
in-place optimisation fires (source and destination are the same pages), and
the ESN scratch write copies our payload bytes into the cache page at the
target offset. recv() returns EBADMSG because the authentication tag
cannot verify — that is expected and harmless. The write has already happened.
Patching the binary.
The embedded payload (taken verbatim from the Theori PoC) overwrites a branch
instruction in /usr/bin/su from a stock util-linux build, turning a
conditional jump that enforces PAM authentication into an unconditional
fall-through. After patching, running su without a password succeeds and
drops a root shell. The on-disk binary is untouched; dropping page-cache
entries (echo 3 > /proc/sys/vm/drop_caches) reverts the change instantly.
For deeper technical analysis see the references below, in particular the Xint blog post and the Theori PoC repository.
- rule: AF_ALG socket opened by unprivileged process
desc: >
CVE-2026-31431 — an unprivileged process opened an AF_ALG (family 38)
socket, which is the first step of the Copy Fail exploit.
condition: >
evt.type = socket and
evt.arg.domain = 38 and
not user.uid = 0
output: >
AF_ALG socket opened (user=%user.name uid=%user.uid
pid=%proc.pid comm=%proc.name)
priority: WARNING
tags: [host, network, privilege_escalation, CVE-2026-31431]
rule CopyFail_CVE_2026_31431 {
meta:
description = "Detects Copy Fail exploit script in memory or on disk"
cve = "CVE-2026-31431"
strings:
$template = "authencesn(hmac(sha256),cbc(aes))" ascii
$splice = "os.splice" ascii
$aflag = "AF_ALG" ascii
condition:
all of them
}
Add to /etc/audit/rules.d/cve-2026-31431.rules:
-a always,exit -F arch=b64 -S socket -F a0=38 -k cve_2026_31431_afalg
Then watch logs with:
ausearch -k cve_2026_31431_afalg --interpret
1. Apply the kernel patch (recommended) Upgrade to kernel 6.18.22, 6.19.12, 7.0-rc7 or later, or apply your distribution's security update.
2. Disable CONFIG_CRYPTO_USER_API_AEAD at build time If you build your own kernel, set:
# CONFIG_CRYPTO_USER_API_AEAD is not set
This removes the entire AF_ALG AEAD interface and eliminates the attack
surface. Most embedded/hardened deployments do not need it.
3. seccomp — deny AF_ALG socket family
Add a seccomp filter to sensitive processes (or system-wide via a LSM policy)
that rejects socket(AF_ALG, ...):
// Deny socket() when domain == AF_ALG (38)
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[0])),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 38, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EACCES),
4. Drop page-cache privileges via kernel hardening
vm.unprivileged_userfaultfd=0 and similar tunables reduce the attack surface
of user-accessible kernel memory interfaces, though they do not directly block
this CVE.
The exploit modifies only the in-memory page cache; the on-disk binary is never touched. To restore the original file content in memory, drop the kernel's page cache:
echo 3 | sudo tee /proc/sys/vm/drop_caches
This evicts all clean pages from the cache. The next read of /usr/bin/su
(or whichever binary was patched) will reload the original bytes from disk.
Rebooting also clears the page cache completely.
MIT — see LICENSE.
MIT was chosen over a custom "educational use only" license because no such
license exists as a recognized OSI/SPDX identifier. MIT is permissive, legally
well-understood, and compatible with responsible disclosure repositories. The
educational intent is expressed in the module docstring, this README's
Disclaimer section, and the --help output — not in a legally dubious
custom license clause.
| Branch | First Vulnerable | Last Vulnerable | Fixed In |
|---|
| mainline | 4.14 | 6.18.21 | 6.18.22 |
| 6.19 stable | 6.19.0 | 6.19.11 | 6.19.12 |
| 7.0-rc | 7.0-rc1 | 7.0-rc6 | 7.0-rc7 |
| Distribution | Shipped Kernel (approx.) | Patched? |
|---|
| Debian 12 (Bookworm) | 6.1.x | Yes (backport) |
| Debian 13 (Trixie) | 6.12.x / 6.14.x | Yes (backport) |
| Ubuntu 24.04 LTS | 6.8.x | Yes (USN-7xxx) |
| Ubuntu 24.10 | 6.11.x | Yes |
| RHEL 9.x | 5.14.x (rebased) | Yes (RHSA) |
| Alpine 3.20 | 6.6.x | Yes (backport) |
| Arch Linux | rolling (≥ 6.18.22 now) | Yes |
| Kali Linux 2026.1 | 6.18.12 | Vulnerable |
| Kali Linux 2026.2+ | 6.18.22+ | Fixed |
| Linux Mint 22 | 6.8.x (Ubuntu base) | Yes (Ubuntu USN) |