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.
| 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) |
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 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 tagThe 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.
| 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. |
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 writeUpdate 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/nullOr 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.
| 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 |
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.