## https://sploitus.com/exploit?id=BFDF0769-E209-5815-82DD-727C5F47C101
# CVE-2026-31431 โ Copy Fail (Linux Kernel LPE)
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.
---
## Disclaimer
**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.
---
## Vulnerability Summary
The Linux kernel has exposed an in-kernel cryptography API to userspace since
kernel 3.2 via the `AF_ALG` socket family (``). 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:
1. Open any readable file (e.g. `/usr/bin/su`) with `O_RDONLY`.
2. Splice its pages into an `AF_ALG` operation socket.
3. Trigger an `authencesn` decryption; the ESN scratch write corrupts the
page at a controlled offset with attacker-controlled bytes.
4. The *disk* is never modified; only the in-memory page cache is patched.
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.
---
## Affected Kernels
| 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 |
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.
---
## Affected Distributions (Non-Exhaustive)
| 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)|
*Check your distribution's security tracker for the exact advisory status.*
---
## Requirements
- Linux kernel in a vulnerable range (see table above)
- Python 3.10+ (`os.splice` was added in Python 3.10)
- Read access to the target binary (default: `/usr/bin/su`, world-readable
as it is setuid)
- No special privileges required โ this is a **local** privilege escalation
---
## Usage
```bash
# 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.
---
## How It Works
**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.
---
## Detection
### Falco rule
```yaml
- 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]
```
### YARA (memory scan for the exploit script)
```yara
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
}
```
### auditd โ detect AF_ALG socket creation
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:
```bash
ausearch -k cve_2026_31431_afalg --interpret
```
---
## Mitigation
**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, ...)`:
```c
// 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.
---
## Cleanup After Testing
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:
```bash
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.
---
## References
- [Original PoC โ Theori](https://github.com/theori-io/copy-fail-CVE-2026-31431)
- [Technical writeup โ Xint](https://xint.io/blog/copy-fail-linux-distributions)
- [Sysdig threat analysis](https://www.sysdig.com/blog/cve-2026-31431-copy-fail-linux-kernel-flaw-lets-local-users-gain-root-in-seconds)
- [Microsoft Security Blog](https://www.microsoft.com/en-us/security/blog/2026/05/01/cve-2026-31431-copy-fail-vulnerability-enables-linux-root-privilege-escalation/)
- [NVD entry](https://nvd.nist.gov/vuln/detail/CVE-2026-31431)
- [Debian security tracker](https://security-tracker.debian.org/tracker/CVE-2026-31431)
---
## Credits
- **Original discovery and disclosure**: Theori (theori.io) and Xint (xint.io)
- **Educational rewrite**: n0kk
---
## License
MIT โ see [LICENSE](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.