Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 

Repository files navigation

CVE-2026-31431 — Copy Fail

732 bytes. Any distro. Root.

A straight-line logic flaw in the Linux kernel's authencesn cryptographic 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


Tested Distributions

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.


What Makes This Different

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)

Root Cause

The Setup: Page Cache Pages in a Writable Scatterlist

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.

The Trigger: authencesn's Out-of-Bounds Scratch Write

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 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.

The Three Attacker-Controlled Variables

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)

How It Happened: A Nine-Year Chain

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.


Exploit

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

Remediation

Permanent Fix

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);

Immediate Mitigation

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.


Coordinated Disclosure Timeline

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

Discovery

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.

About

Copy Fail: 732 Bytes to Root on Every Major Linux Distribution.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages