
Copy Fail: 732 Bytes to Root on Every Major Linux Distribution.
732 bytes. Any distro. Root.
A straight-line logic flaw in the Linux kernel's
authencesncryptographic template enables an unprivileged local user to perform a precise, controlled 4-byte write into the page cache of any readable file — including setuid binaries. No races. No retries. No recompilation. Root on every major Linux distribution shipped since 2017.
📄 Technical Write-up · 🔗 Kernel Patch · 🛡️ CVSS: Critical
| Distro | Kernel Version |
|---|---|
| Ubuntu 24.04 LTS | 6.17.0-1007-aws |
| Amazon Linux 2023 | 6.18.8-9.213.amzn2023 |
| RHEL 10.1 | 6.12.0-124.45.1.el10_1 |
| SUSE 16 | 6.12.0-160000.9-default |
All four were rooted using the identical 732-byte Python script, without modification.
AF_ALG exposes the kernel's crypto subsystem to unprivileged userspace. splice() transfers file data into a pipe by reference — passing page cache pages directly, without copying. When a user splices a file into an AF_ALG AEAD socket, the socket's input scatterlist holds live references to the kernel's cached pages of that file.
In algif_aead.c, the 2017 in-place optimization copied AAD and ciphertext from the TX scatterlist into the RX buffer, but chained the authentication tag pages by reference using sg_chain(), then set req->src = req->dst:
Input SGL: [ AAD | CT | Tag ]
^
└─ sg_chain() → still points to page cache pages
Output SGL: [ AAD | CT ] ──→ [ Tag (page cache pages) ]
(RX buffer) (chained from TX SGL)
req->src ──┐
├──→ same combined scatterlist
req->dst ──┘
Page cache pages from splice() were now sitting inside a writable destination scatterlist, separated from the legitimate write region by only an offset boundary. Nothing in the API enforced that algorithms must stay within bounds.
authencesn's Out-of-Bounds Scratch Writeauthencesn is an AEAD wrapper used by IPsec for 64-bit Extended Sequence Number (ESN) support. To rearrange ESN bytes for HMAC computation, it uses the caller's destination buffer as scratch space — including a write at offset assoclen + cryptlen, which lies past the authentication tag boundary:
scatterwalk_map_and_copy(tmp, dst, 0, 8, 0); // read AAD[0..7]
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1); // overwrite dst[4..7]
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); // ← writes past the tag
The third call writes 4 bytes (seqno_lo) at dst[assoclen + cryptlen]. In the in-place AF_ALG path, the scatterwalk crosses from the RX buffer into the chained page cache tag pages. The kernel maps the page cache page via kmap_local_page and writes directly into the cached copy of the target file.
The HMAC then fails (the ciphertext is fabricated), recvmsg() returns an error — but the 4-byte write persists permanently.
No single change was individually wrong. The vulnerability lives at the intersection of all three.
The default target is /usr/bin/su, a setuid-root binary present on all tested distributions.
Step 1 — Socket setup
Open AF_ALG socket, bind to authencesn(hmac(sha256),cbc(aes))
Set key. Accept request socket. (No privileges required.)
Step 2 — Write loop (once per 4-byte shellcode chunk)
sendmsg() → AAD bytes [4:8] carry the 4 bytes to write (seqno_lo)
splice() → target file's page cache pages into the AF_ALG socket
recv() → triggers decrypt → authencesn writes seqno_lo into page cache
(recvmsg returns error; the write persists)
Step 3 — Execute
execve("/usr/bin/su")
Kernel loads binary from the (now-corrupted) page cache
Setuid-root binary executes injected shellcode → UID 0
a = socket.socket(38, 5, 0) # AF_ALG, SOCK_SEQPACKET
a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
# ... set key, accept request socket u ...
u.sendmsg([b"A"*4 + payload_chunk], [cmsg_headers], MSG_MORE)
os.splice(target_fd, pipe_wr, offset)
os.splice(pipe_rd, alg_fd, offset)
u.recv(...) # triggers page cache write
Update to a kernel containing patch a664bf3d603d. The fix reverts algif_aead.c to out-of-place operation: req->src points to the TX SGL; req->dst points to the RX buffer. Page cache pages from splice() remain read-only. The sg_chain() mechanism that linked them into the writable destination is removed.
// Before (vulnerable): src and dst share the same scatterlist
aead_request_set_crypt(&areq->cra_u.aead_req, rsgl_src, rsgl_src, used, ctx->iv);
// After (fixed): src is TX SGL, dst is RX buffer — fully separated
aead_request_set_crypt(&areq->cra_u.aead_req, tsgl_src, rsgl_dst, used, ctx->iv);
Disable the algif_aead kernel module:
echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif-aead.conf
rmmod algif_aead 2>/dev/null
Or block AF_ALG socket creation via a seccomp policy in your workload profiles.
Note for container environments: Because the page cache is shared across the host, this vulnerability crosses container boundaries. Apply mitigations at the node level, not just per-pod. See Part 2 for full Kubernetes escape details.
Theori researcher Taeyang Lee identified, through prior kernelCTF work, that AF_ALG + splice() creates a path where unprivileged userspace can feed page cache pages directly into the crypto subsystem — and that scatterlist page provenance was an underexplored vulnerability class.
The research team used Xint Code to scale this insight across the entire crypto/ subsystem with the following operator prompt:
"This is the linux crypto/ subsystem. Please examine all codepaths reachable from userspace syscalls. Note one key observation: splice() can deliver page-cache references of read-only files (including setuid binaries) to crypto TX scatterlists."
After approximately one hour of automated analysis, Copy Fail was the highest-severity output. Additional vulnerabilities discovered during the same scan remain under coordinated disclosure.
Part 2: From Pod to Host — how Copy Fail escapes every major cloud Kubernetes platform. Coming soon.
| Property | Detail |
|---|
| Deterministic | Straight-line logic flaw — no race conditions, no timing windows, no retries |
| Portable | Same script, same bytes, works across all tested distros and architectures |
| Tiny | 732-byte Python script using only the standard library (os, socket, zlib). Requires Python 3.10+ for os.splice |
| Stealthy | The corrupted page is never marked dirty. On-disk checksums are unchanged; only the in-memory page cache is modified |
| Cross-container | The page cache is shared system-wide across container boundaries — this is also a Kubernetes node escape primitive (see Part 2) |
| Variable | Controlled Via |
|---|
| Target file | Any file readable by the current user |
| Write offset | assoclen, splice offset, and splice length |
| Write value | Bytes 4–7 of the AAD supplied in sendmsg() (seqno_lo) |
| Year | Event |
|---|
| 2011 | authencesn added to the kernel (a5079d084f8b) for IPsec ESN support. The scratch write existed but was harmless — only the internal xfrm layer called it, and AAD lived in a separate scatterlist. |
| 2015 | AF_ALG gains AEAD support. authencesn converted to the new AEAD interface (104880a6b470), introducing the assoclen + cryptlen write offset. Still out-of-place: page cache pages were in src (read-only). Not yet exploitable. |
| 2017 | In-place optimization added to algif_aead.c (72548b093ee3). req->src = req->dst. Page cache tag pages chained into the writable destination. Vulnerability formed. |
| 2026-03-23 | Reported to the Linux kernel security team. |
| 2026-04-01 | Patch merged into mainline. |
| 2026-04-22 | CVE-2026-31431 assigned. |
| 2026-04-29 | Public disclosure. |
| Date | Event |
|---|
| 2026-03-23 | Vulnerability reported to Linux kernel security team |
| 2026-03-24 | Initial acknowledgment received |
| 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 |