Copy Fail (CVE-2026-31431): 732 bytes of Python — and any user becomes root
Taeyang Lee at Theori was studying what happens when the Linux cryptographic subsystem receives data from the page cache via the splice() system call. The hunch was simple: if an unprivileged user can feed a cached kernel page into an encryption operation, something has to give. He pointed the AI-assisted tool Xint Code at the entire crypto subsystem reachable from userspace — and about an hour later, CVE-2026-31431 was on the screen.
Copy Fail is a logic bug in the Linux kernel’s authencesn cryptographic template (CWE-787, out-of-bounds write). It lets an unprivileged local user write exactly 4 bytes into the page cache of any file readable by that user. Point it at a setuid binary — say, /usr/bin/su — and you have root. CVSS 7.8, High. The proof-of-concept exploit was published on April 29, 2026 and fits in 732 bytes of Python. The patch is already in the mainline kernel; distribution updates went out through the normal kernel package mechanism.
What sets Copy Fail apart from the other LPE vulnerabilities this year is determinism. Dirty Cow required winning a race condition and sometimes crashed the system on a failed attempt. Dirty Pipe was version-specific and needed precise pipe buffer manipulation. Copy Fail triggers without races, without retries, without crash-prone timing windows. The same 732-byte script works unchanged on Ubuntu, Amazon Linux, RHEL, and SUSE — no per-distro offsets, no recompilation, no version checks. 100% reliability.
WHAT AF_ALG AND PAGE CACHE ARE
AF_ALG is a socket type that exposes the kernel’s cryptographic subsystem to unprivileged userspace. You open a socket, bind to any AEAD template (Authenticated Encryption with Associated Data), and pass data in for encryption or decryption. No privileges required — it’s standard functionality available to any user.
The page cache is the layer in kernel memory where file contents live. When you read a file, the kernel loads its pages into the page cache. Every subsequent read(), mmap(), and execve() works from that cache, not from disk. The page cache is shared across the entire system — including all containers running on a host.
The splice() system call transfers data between file descriptors and pipes without copying — it passes a reference to a page in memory, not the content itself. When a user splices a file into an AF_ALG socket, the crypto subsystem receives direct references to the page cache pages of that file. Not copies — the exact same physical pages the kernel will read from when the file is next executed via execve().
HOW THE BUG WORKS
In AEAD decryption through AF_ALG, the input looks like this: AAD (associated authenticated data) || ciphertext || authentication tag. The code in algif_aead.c sets up the operation in-place: the same scatterlist serves as both input and output. AAD and ciphertext are copied from the TX buffer into the RX buffer via memcpy_sglist. But the tag — the last few bytes of the input — is not copied. Instead, the page cache pages holding the tag are chained onto the output scatterlist via sg_chain(). The result: the output scatterlist is the user’s RX buffer followed by the page cache pages of the target file.
This is where authencesn enters the picture. It’s an AEAD wrapper used by IPsec for Extended Sequence Number support. IPsec splits the 64-bit sequence number into a high half (seqno_hi, bytes 0–3 of the AAD) and a low half (seqno_lo, bytes 4–7). To compute the HMAC, authencesn needs to rearrange those bytes — and it uses the destination scatterlist as scratch space to do it. Specifically, crypto_authenc_esn_decrypt() calls scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1) — a 4-byte write at offset assoclen + cryptlen, immediately past the authentication tag.
Under normal conditions, that position would be inside the user’s buffer. But when the tag pages are chained into the output scatterlist via sg_chain(), scatterwalk walks right out of the RX buffer and into the page cache of the target file, writing 4 attacker-controlled bytes. The HMAC verification then fails and recvmsg() returns an error — but the 4-byte write has already landed. The kernel never marks the page dirty for writeback, so the file on disk stays untouched. The modification is in memory only — but memory is exactly what the kernel reads from on the next execve().
HOW IT IS EXPLOITED
The attacker controls three parameters of the write: which file (anything readable by the current user), which offset within the file (via the splice offset, length, and assoclen), and which value to write (bytes 4–7 of the AAD in sendmsg). This isn’t a random overwrite — it’s a precise, repeatable, programmable write of an arbitrary value to an arbitrary position in the page cache of any file on the system.
The exploit targets /usr/bin/su, a setuid-root binary present on all tested distributions. First, an AF_ALG socket is opened and bound to the authencesn(hmac(sha256),cbc(aes)) template. A key is set. A request socket is accepted. No privileges needed — AF_ALG is available to any user by default.
Then, for each 4-byte chunk of the shellcode payload, a sendmsg() + splice() pair is constructed. The AAD carries the 4 bytes to write (seqno_lo). The splice descriptor delivers the page cache pages of /usr/bin/su at the right offset. The parameters — assoclen, splice length, and splice offset — are chosen so the scratch write lands on exactly the right position in the binary’s .text section.
Calling recv() triggers the decryption. Inside authencesn, the scratch write goes to dst[assoclen + cryptlen] — scatterwalk crosses from the RX buffer into the page cache and writes 4 bytes. The HMAC check immediately fails and recvmsg() returns an error — but the write is done. Once all shellcode chunks are in place, the attacker calls execve("/usr/bin/su"). The kernel loads the binary from the page cache — where the shellcode now lives. Because su is setuid-root, the code runs as UID 0.
HOW THE BUG SURVIVED NINE YEARS
The history of this vulnerability is a case study in how three independently safe changes produce a lethal combination. In 2011, authencesn was added to the kernel to support IPsec ESP with 64-bit Extended Sequence Numbers (RFC 4303). The scratch write into the destination scatterlist was safe: the associated data lived in a separate scatterlist, and the only caller was the kernel’s internal xfrm layer.
In 2015, AF_ALG gained AEAD support, and authencesn was converted to the new AEAD interface. But algif_aead.c operated out-of-place: req->src and req->dst were separate scatterlists. Page cache pages landed in src (read-only); the scratch write went to dst (the user’s buffer). Still safe.
In 2017, an in-place optimization was added to algif_aead.c (commit 72548b093ee3). AAD and ciphertext were copied into the RX buffer, but the tag pages were chained in by reference via sg_chain(), and then req->src = req->dst. Page cache pages were now in the writable destination scatterlist. The authencesn scratch write crossed the buffer boundary and landed in them. Nobody connected the 2017 optimization to the authencesn scratch write from 2015 or to the splice path from the same year. The bug existed at the intersection of all three changes — and stayed invisible for nearly a decade.
WHY IT’S BEING FOUND NOW
Copy Fail wasn’t found by classic fuzzing or manual audit. Taeyang Lee formulated a hypothesis — that splice() can deliver page cache references into a crypto TX scatterlist — and handed it to Xint Code with the task of checking the entire crypto subsystem for that pattern. An hour later, Copy Fail came back as the highest-severity result.
This is part of a broader 2026 trend: AI-assisted vulnerability research is beginning to systematically cover attack surfaces that went unchecked for years. The intersection of multiple subsystems — crypto, VFS, socket API — is exactly the kind of place where a human auditor could easily miss the connection between changes spread across different files and years. A model can see the full call graph at once and verify a specific hypothesis in a fraction of the time.
TIMELINE
On March 23, 2026, Taeyang Lee reported the vulnerability to the Linux kernel security team. Acknowledgment came the next day; two days after that, the patch was proposed and reviewed. On April 1, 2026, the patch entered the mainline kernel (commit a664bf3d603d). CVE-2026-31431 was assigned on April 22. Public disclosure with a full writeup and PoC exploit followed on April 29 at copy.fail.
The coordinated disclosure took 37 days from report to publication — fast by Linux kernel standards. The security team confirmed the severity within hours and pushed the patch through quickly. Distribution kernel updates followed within days of the public disclosure through normal kernel package update channels.
WHY IT MATTERS
Copy Fail closes an important mental trap: “the file on disk hasn’t changed, so the system is clean.” File integrity tools that compare on-disk hashes — AIDE in disk-comparison mode, Tripwire — won’t catch this, because the on-disk file genuinely hasn’t changed. What changed is the page cache in memory, and that’s what the kernel actually executes from. FIM needs to be complemented by anomalous process behavior monitoring, not just file hashes.
The second dimension is containers. The page cache is shared between the host and all containers running on it. Copy Fail is not just an LPE — it’s a container escape primitive. A single compromised pod running as an unprivileged user can gain root on the entire Kubernetes node by modifying a setuid binary in the shared host page cache. Xint announced a second writeup — “From Pod to Host” — covering exactly this vector.
Finally, a nine-year-old bug in an actively developed kernel is a reminder that the intersection of multiple subsystems is the blindest spot in any security audit. authencesn, AF_ALG, and splice() were each reviewed in isolation. Nobody reviewed them together.
UPDATE
The patch reverts the 2017 optimization: req->src now points to the TX SGL (which may contain page cache pages from splice), while req->dst points to the RX SGL (the user’s buffer). Page cache pages no longer end up in the writable destination scatterlist. The fix consists of three upstream commits; the primary one is a664bf3d603d, which removes the in-place operation from algif_aead.c.
On Debian and Ubuntu, start by checking the current kernel version — note it so you can confirm it changes after the update:
uname -r
Then update and reboot:
sudo apt update && sudo apt upgrade -y
After rebooting, run uname -r again — the version should be different. An installed package without a reboot offers no protection: the system keeps running the vulnerable kernel in memory.
The modprobe.d workaround that spread widely after the public disclosure works only on Debian and Ubuntu. On RHEL, AlmaLinux, CloudLinux, and other RHEL-family distributions, the algif_aead module is compiled directly into the kernel and is not a loadable module. The commands run without errors but do nothing — and create a false sense of security. To check quickly:
modinfo algif_aead | grep filename
Output of (builtin) means the module is built in and the modprobe.d workaround won’t work. A path to a .ko file means it’s a loadable module and the workaround will take effect.
For RHEL-family systems where an immediate update isn’t possible, CloudLinux’s advisory describes a working temporary workaround using initcall_blacklist. It requires a reboot but closes the attack surface without replacing the kernel:
sudo grubby --update-kernel=ALL --args="initcall_blacklist=algif_aead_init"
sudo reboot
After rebooting, confirm the parameter is active:
sudo grubby --info=ALL | grep initcall_blacklist
Once the patched kernel is installed, remove the workaround and reboot again:
sudo grubby --update-kernel=ALL --remove-args="initcall_blacklist=algif_aead_init"
sudo reboot
Note: dm-crypt/LUKS, kTLS, IPsec, SSH, and standard OpenSSL/GnuTLS builds don’t depend on AF_ALG and won’t be affected by this workaround. Only applications that explicitly use AF_ALG for AEAD operations will be impacted — which is rare on a typical web server.
CONCLUSIONS
Copy Fail is a rare vulnerability where the technical mechanism is both elegant and devastating. Three lines of code in a single function, added as an optimization nine years ago, turned a standard encryption API into a tool for writing to the page cache of any file on the system. The result: deterministic root in 732 bytes of Python across every major Linux distribution.
For sysadmins, the action is straightforward: update the kernel and reboot. Skip the modprobe.d workaround if you’re on RHEL-family. For anyone running Kubernetes: the second Xint writeup on container escape via this primitive is out — read it.
Affected kernel versions:
All Linux kernels released between 2017 and March 2026 — versions from approximately 4.14 through 6.18 inclusive.
Not affected:
Kernels older than 2017 (prior to ~4.14) — the in-place optimization that introduced the bug didn’t exist yet. CloudLinux 7 (classic) is also not affected.
Fixed starting from:
| Distribution | First safe kernel version |
|---|---|
| Debian / Ubuntu | any kernel updated after April 1, 2026 — via apt upgrade |
| RHEL 8 / AlmaLinux 8 / CL8 | 4.18.0-553.121.1.el8 |
| RHEL 9 / AlmaLinux 9 / CL9 | 5.14.0-611.49.2.el9_7 |
| RHEL 10 / AlmaLinux 10 / CL10 | 6.12.0-124.52.2.el10_1 |
| Amazon Linux 2023 | via dnf upgrade after April 2026 |
Check your version:
uname -r
