Copy Fail - Linux

Table of Contents

Table of Contents

TL;DR

CVE-2026-31431 (Copy Fail) is a local privilege escalation in the Linux kernel’s crypto subsystem that gives any unprivileged user deterministic root on every major Linux distribution shipped since 2017. A 732-byte Python script using only standard library calls can corrupt the page cache of any readable file — including setuid binaries like /usr/bin/su — without touching disk. The kernel loads the corrupted in-memory copy, executes attacker shellcode as UID 0, and the on-disk file stays pristine. File integrity tools that compare disk checksums see nothing wrong.

The CVSS is 7.8 (CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H). The bug is a straight-line logic flaw — no race conditions, no kernel memory corruption, no per-distro offsets. It crosses container boundaries because the page cache is shared across the host. Microsoft, CISA, and every major Linux vendor issued advisories within 48 hours of disclosure. If you run Linux on a kernel between 4.14 and 6.19.12, you are affected.

The Backstory

Taeyang Lee at Theori discovered Copy Fail while studying how the Linux crypto subsystem interacts with page-cache-backed data. He had previously mapped out the AF_ALG attack surface in his kernelCTF work and realized that splice() feeding page cache pages into crypto scatterlists might be an underexplored source of vulnerabilities. Using Xint Code, an AI-assisted security research tool, he scaled the analysis across the entire crypto subsystem. After roughly an hour of scanning, Copy Fail emerged as the highest-severity finding.

Lee reported the vulnerability to the Linux kernel security team on 2026-03-23. Patches were proposed within two days and committed to mainline on 2026-04-01. CVE-2026-31431 was assigned on 2026-04-22. Public disclosure followed on 2026-04-29 through the website copy.fail. CISA added it to the Known Exploited Vulnerabilities catalog on 2026-05-01.

The bug is not Android-specific — GrapheneOS confirmed Android is protected by SELinux policies that restrict AF_ALG socket access to the dumpstate process only. But every mainstream Linux distribution with kernel 4.14 or later was exposed: Ubuntu, Red Hat Enterprise Linux, SUSE, Amazon Linux, Debian, Fedora, Arch Linux. Siemens also issued advisories for industrial controllers running embedded Linux.

The Vulnerability

Root Cause

Three independent changes, each reasonable in isolation, created this bug at their intersection.

2011 — authencesn added. Commit a5079d084f8b introduced authencesn, an AEAD wrapper for IPsec Extended Sequence Number (ESN) support. IPsec uses 64-bit sequence numbers. The wire format carries only the low 32 bits (seqno_lo); the high half (seqno_hi) is implicit. For HMAC computation, authencesn rearranges these bytes using the caller’s destination buffer as scratch space. The third write in crypto_authenc_esn_decrypt() goes to dst[assoclen + cryptlen] — four bytes past the legitimate output boundary. No other AEAD algorithm in the kernel does this.

1scatterwalk_map_and_copy(tmp, dst, 0, 8, 0);                        // read AAD bytes 0–7
2scatterwalk_map_and_copy(tmp, dst, 4, 4, 1);                        // overwrite dst[4..7] with seqno_hi
3scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1);  // write seqno_lo past the tag

The original bytes at that position are permanently lost. Even when the operation fails, they are never restored.

2015 — AF_ALG gains AEAD and splice(). Commit 104880a6b470 converted authencesn to the new AEAD interface, introducing the assoclen + cryptlen offset write. Around the same time, algif_aead.c added a splice() path that could deliver page cache pages into the crypto scatterlist. But at this point, AF_ALG used out-of-place operation: req->src and req->dst were separate scatterlists. Page cache pages stayed in src (read-only). The scratch write went to dst (the user’s buffer). Not exploitable.

2017 — In-place optimization. Commit 72548b093ee3 optimized algif_aead.c to perform AEAD operations in-place. For decryption, the kernel copies AAD and ciphertext from the TX SGL into the RX buffer, but chains the authentication tag pages by reference using sg_chain(). It then sets req->src = req->dst, both pointing to the same combined scatterlist. Page cache pages from splice() — intended for read-only input — are now in the writable destination scatterlist.

1req->src ----+
2             |
3             v
4req->dst --> [ AAD  ||  CT  ] --> [ Tag (page cache pages) ]
5             |                |   |                        |
6             +-- RX buffer ---+   +-- chained from TX SGL -+
7             |   (user mem)       |   (file's page cache)

authencesn’s write at dst[assoclen + cryptlen] now walks straight into those chained page cache pages. scatterwalk_map_and_copy maps the page via kmap_local_page and writes seqno_lo directly into the kernel’s cached copy of the target file. The HMAC computation fails. recvmsg() returns an error. The 4-byte write persists.

The attacker controls three things: which file (any readable file), which offset (via splice offset, splice length, and assoclen), and which value (seqno_lo from AAD bytes 4–7).

Attack Class

Local privilege escalation through improper page cache write. CWE-669 (Incorrect Resource Transfer Between Spheres) per CISA-ADP. CWE-1288 (Improper Validation of Consistency within Input) per Red Hat.

Exploitation Walkthrough

The canonical exploit targets /usr/bin/su, a setuid-root binary present on essentially every Linux distribution. The full PoC is 732 bytes of Python using only os, socket, and zlib.

Step 1 — Socket setup. Open an AF_ALG socket and bind to authencesn(hmac(sha256),cbc(aes)). Set a key. Accept a request socket. No privileges required — AF_ALG is exposed to unprivileged users by default on every major distribution.

1a = socket.socket(38, 5, 0)  ### AF_ALG, SOCK_SEQPACKET
2a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))

Step 2 — Construct the write. For each 4-byte chunk of shellcode, the attacker sends a sendmsg() with the AAD (bytes 4–7 carry the target 4 bytes as seqno_lo), then splices the target file’s page cache pages in as ciphertext and tag.

1u.sendmsg([b"A"*4 + payload_chunk], [cmsg_headers], MSG_MORE)
2os.splice(target_fd, pipe_wr, offset)
3os.splice(pipe_rd, alg_fd, offset)

The AEAD parameters (assoclen, splice offset, splice length) are chosen so that dst[assoclen + cryptlen] lands on the target offset within su’s .text section.

Step 3 — Trigger the write. recv() kicks off decryption. Inside crypto_authenc_esn_decrypt(), the kernel reads ESN bytes from AAD and writes seqno_lo at dst[assoclen + cryptlen]. The scatterwalk crosses from the RX buffer into the chained page cache page. Four bytes are written into the kernel’s cached copy of /usr/bin/su. HMAC fails. recvmsg() returns an error. The corrupted page persists — the kernel never marks it dirty for writeback, so the on-disk file remains untouched.

Step 4 — Execute. After all chunks are written, call execve("/usr/bin/su"). The kernel loads the binary from the page cache. The page cache version contains injected shellcode. Because su is setuid-root, the shellcode runs as UID 0.

The elegance is that the exploit uses no custom kernel modules, no ptrace, no /proc or /sys manipulation, no ioctl abuse, no BPF, and no network sockets. Everything goes through three standard syscalls: socket(), splice(), and sendmsg().

Real-World Impact

CISA added CVE-2026-31431 to the Known Exploited Vulnerabilities catalog on 2026-05-01, confirming active exploitation. Microsoft Defender Security Research reported “preliminary testing activity” within days of public disclosure and assessed that threat actor exploitation would increase. As of this writing, no major ransomware group or APT has been publicly attributed with using Copy Fail in campaign — but the window is short.

The impact surface is extraordinary. Every Linux cloud workload on kernel 4.14 through 6.19.12 is affected. Microsoft estimated “millions of Kubernetes clusters” and “a significant portion of cloud Linux workloads.” The container escape vector is especially dangerous: because the page cache is shared between containers and the host, a compromise in one pod equals node compromise. Xint published a separate writeup (“From Pod to Host”) demonstrating Kubernetes container escape using the same primitive.

Industries most at risk: cloud providers, CI/CD pipelines, multi-tenant SaaS platforms, and any environment where unprivileged code execution can be achieved through web shells, malicious CI jobs, or compromised containers.

The Fix

The patch (commit a664bf3d603d) reverts algif_aead.c to out-of-place operation. The Fixes: tag points to 72548b093ee3, confirming the 2017 in-place optimization as the root cause.

1// Before: src and dst point to the same scatterlist (in-place)
2aead_request_set_crypt(&areq->cra_u.aead_req, rsgl_src,
3                       areq->first_rsgl.sgl.sgt.sgl, used, ctx->iv);
4// After: src is TX SGL, dst is RX SGL (out-of-place)
5aead_request_set_crypt(&areq->cra_u.aead_req, tsgl_src,
6                       areq->first_rsgl.sgl.sgt.sgl, used, ctx->iv);

req->src now points to the TX SGL (which may contain page cache pages from splice()). req->dst points to the RX SGL (the user’s recvmsg buffer). The sg_chain mechanism that linked page cache tag pages into the writable destination scatterlist is removed entirely. The commit message is refreshingly direct: “There is no benefit in operating in-place in algif_aead since the source and destination come from different mappings.”

Fixed versions: 6.19.12 (ce42ee423e58), 6.18.22 (fafe0fa2995a), and subsequent stable backports. For older longterm kernels (6.12, 6.6, 6.1, 5.15, 5.10), backports were posted by Greg Kroah-Hartman on 2026-04-30.

Interim mitigation for organizations unable to patch immediately: disable the algif_aead kernel module.

1echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif-aead.conf
2rmmod algif_aead 2>/dev/null

SELinux and AppArmor can block the attack if configured to deny AF_ALG socket creation for unconfined domains. In default configurations, though, they do not — any unconfined process can open AF_ALG sockets.

Lessons Learned

For Developers

The bug survived for nine years because each individual change was reasonable. The 2011 authencesn scratch write was harmless under the old API. The 2015 splice() path was safe with out-of-place crypto. The 2017 in-place optimization was a legitimate performance improvement. The vulnerability exists at the intersection — no single commit is obviously wrong.

This is a cautionary tale about implicit invariants across subsystem boundaries. The crypto API never documented that AEAD algorithms must confine writes to dst[0..assoclen + cryptlen - 1]. The algif_aead in-place code never checked. splice() never warned that its zero-copy page references would end up in a writable scatterlist. Each subsystem assumed the others would handle safety.

Fuzzing the AF_ALG + splice() path with a coverage-guided kernel fuzzer (like syzkaller) could have caught this. The syscall combination is standard — socket(), bind(), sendmsg(), splice(), recv(). That it was not caught suggests the crypto subsystem’s AF_ALG paths were underfuzzed relative to more common attack surfaces like VFS or network.

For Defenders

Sigma detection: Monitor for non-standard parent processes spawning su. The Copy Fail PoC calls execve("/usr/bin/su") from a Python process — not from a shell or sudo. A query matching su executions where the parent process is not bash, sh, zsh, ksh, or sudo can catch the exploit post-facto.

Kernel module monitoring: Alert on algif_aead module loads in environments where AF_ALG is not expected.

Seccomp profiles: Block AF_ALG socket creation (socket(AF_ALG, ...)) in container seccomp profiles. Kubernetes Pod Security Standards should deny AF_ALG by default.

File integrity awareness: Standard integrity monitoring (AIDE, Tripwire, OSSEC) compares on-disk checksums and will not detect page cache corruption. Runtime integrity monitoring (e.g., eBPF-based verification of inode page cache contents) would be required, but no mainstream tool supports this today.

Container hardening: Treat any container RCE as potential node compromise — the page cache is shared. Rapid node recycling after compromise is the pragmatic defense.

Timeline

  • 2011-12-13 — authencesn added to kernel (commit a5079d084f8b)
  • 2015-10-20 — authencesn converted to new AEAD interface (commit 104880a6b470)
  • 2017-11-02 — In-place optimization introduced in algif_aead.c (commit 72548b093ee3); bug becomes exploitable
  • 2026-03-23 — Vulnerability reported to Linux kernel security team by Taeyang Lee (Theori)
  • 2026-03-24 — Initial acknowledgment from kernel team
  • 2026-03-25 — Patches proposed and reviewed
  • 2026-04-01 — Patches committed to mainline kernel
  • 2026-04-22 — CVE-2026-31431 assigned
  • 2026-04-29 — Public disclosure at copy.fail
  • 2026-04-30 — Distributions begin releasing patches; backports posted for stable kernels
  • 2026-05-01 — CISA adds to Known Exploited Vulnerabilities catalog
That's all for now, folks!

Please let me know about any typos or mistakes that may have crept up in this article.