Tools: CopyFail (CVE-2026-31431): How a 732-Byte Python Script Gets Root on Almost Every Linux Machine

Tools: CopyFail (CVE-2026-31431): How a 732-Byte Python Script Gets Root on Almost Every Linux Machine

Introduction

Non-Technical Explanation: What is actually happening?

Technical Explanation

The Four Pieces You Need to Understand

The Bug — How These Four Pieces Combine

The Full Attack Chain

The Shellcode (160 bytes, decompressed)

Why Forensics Miss It

Live Demo

System Info (before exploit)

Running the Exploit

After Exploit

Mitigation

Check if you are vulnerable

Layer 1 — Disable algif_aead immediately (no reboot needed)

Layer 2 — Patch your kernel (permanent fix)

After Mitigation — Exploit Fails A beginner-friendly breakdown of one of the most dangerous Linux kernel vulnerabilities since Dirty COW — with a live demo and mitigation guide. On April 29, 2026, researchers at Xint disclosed a vulnerability in the Linux kernel that sent shockwaves through the security community. It is called CopyFail, tracked as CVE-2026-31431, and it has a CVSS score of 7.8 (HIGH). Here is what makes it terrifying: I ran this on my own Ubuntu 24.04 VM to understand it, demo it, and show you how to defend against it. This is that story. Imagine your computer is a library. Books on the shelves = files on your hard disk (permanent) When someone wants to read a book, the librarian makes a photocopy and puts it on the reading table = RAM (temporary, fast) Next person who wants the same book? Librarian skips the shelf and hands them the copy on the table directly. Faster. Now imagine someone sneaks into the library at night and scribbles over the photocopy on the reading table — but never touches the original book on the shelf. Next morning, every reader gets the scribbled version. The original book looks fine. The librarian has no idea anything happened. The "photocopy on the reading table" is the Linux page cache — RAM copies of files your system is using. CopyFail scribbles shellcode into the RAM copy of /usr/bin/su (a program that lets you switch users). When you run su, Linux executes the scribbled version — and gives the attacker a root shell. The file on disk? Untouched. sha256sum /usr/bin/su returns the same hash before and after. Your file integrity tools see nothing. 3. AF_ALG — The Kernel Crypto Socket 4. splice() — Zero Copy The authencesn algorithm needs 4 bytes of scratch space during processing. After the 2017 optimization, those 4 bytes land inside su's page cache instead of a safe kernel buffer. And you control exactly where those 4 bytes land by choosing assoclen + cryptlen. Inside the 732-byte script is a zlib-compressed mini ELF binary. Decompressed, it does exactly three things: The string /bin/sh\0 is appended at the end. That is the entire payload. No persistence. No disk writes. No logs. Rebooting removes all traces — including all evidence. ⚠️ This was performed on my isolated VirtualBox VM running Ubuntu 24.04 LTS, kernel 6.17.0-20-generic. Never run this on a production system or any machine you do not own. A completely normal, unprivileged user. No sudo. No special groups. The script ran silently. Then su got triggered. The prompt changed to #. 732 bytes. One command. Root. Disk untouched. This closes the attack surface immediately. It does not affect dm-crypt, LUKS, SSH, OpenSSL, or GnuTLS. ⚠️ Note: On some distros like (AlmaLinux, Rocky, CentOS), algif_aead may be built into the kernel (CONFIG_CRYPTO_USER_API_AEAD=y). The rmmod approach will not work there. AlmaLinux / Rocky / RHEL: Same script. Same machine. One mitigation command = exploit dead. This blog was written for educational purposes. The demo was performed in my isolated VM environment. Always patch your systems and never run exploit code on machines you do not own. If you found this useful, share it — especially with anyone running Ubuntu 24.04 or any Linux server built since 2017. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

User runs /usr/bin/su ↓ First time → kernel reads from disk → stores in RAM (page cache) ↓ Next time → kernel serves from RAM directly (faster) ↓ All processes share the same cached pages User runs /usr/bin/su ↓ First time → kernel reads from disk → stores in RAM (page cache) ↓ Next time → kernel serves from RAM directly (faster) ↓ All processes share the same cached pages User runs /usr/bin/su ↓ First time → kernel reads from disk → stores in RAM (page cache) ↓ Next time → kernel serves from RAM directly (faster) ↓ All processes share the same cached pages /usr/bin/su has permissions: -rwsr-xr-x ↑ 's' = setuid bit Normal binary → runs as YOU (the caller) Setuid binary → runs as the FILE OWNER (root) If you inject code into su and run it → your code executes as root /usr/bin/su has permissions: -rwsr-xr-x ↑ 's' = setuid bit Normal binary → runs as YOU (the caller) Setuid binary → runs as the FILE OWNER (root) If you inject code into su and run it → your code executes as root /usr/bin/su has permissions: -rwsr-xr-x ↑ 's' = setuid bit Normal binary → runs as YOU (the caller) Setuid binary → runs as the FILE OWNER (root) If you inject code into su and run it → your code executes as root Normal socket → sends data over network AF_ALG socket → sends data through kernel's crypto engine socket(AF_ALG, SOCK_SEQPACKET, 0) setsockopt → "use authencesn(hmac(sha256),cbc(aes))" Now feed data in → kernel encrypts it for you Normal socket → sends data over network AF_ALG socket → sends data through kernel's crypto engine socket(AF_ALG, SOCK_SEQPACKET, 0) setsockopt → "use authencesn(hmac(sha256),cbc(aes))" Now feed data in → kernel encrypts it for you Normal socket → sends data over network AF_ALG socket → sends data through kernel's crypto engine socket(AF_ALG, SOCK_SEQPACKET, 0) setsockopt → "use authencesn(hmac(sha256),cbc(aes))" Now feed data in → kernel encrypts it for you Normal read(): Disk → page cache → YOUR program's memory → destination ↑ data passes through you splice(): Disk → page cache → destination (kernel handles it directly) ↑ your program never touches the data ↑ the ACTUAL page cache pages get wired in Normal read(): Disk → page cache → YOUR program's memory → destination ↑ data passes through you splice(): Disk → page cache → destination (kernel handles it directly) ↑ your program never touches the data ↑ the ACTUAL page cache pages get wired in Normal read(): Disk → page cache → YOUR program's memory → destination ↑ data passes through you splice(): Disk → page cache → destination (kernel handles it directly) ↑ your program never touches the data ↑ the ACTUAL page cache pages get wired in BEFORE 2017 (safe): splice(su → AF_ALG socket) ↓ Input pages: [su's page cache] Output pages: [separate kernel buffer] ← scratch write goes HERE (safe) AFTER 2017 optimization (buggy): splice(su → AF_ALG socket) ↓ Input pages: [su's page cache] ↕ ← src = dst (in-place optimization) Output pages: [su's page cache] ← scratch write goes HERE (su's RAM!!!) BEFORE 2017 (safe): splice(su → AF_ALG socket) ↓ Input pages: [su's page cache] Output pages: [separate kernel buffer] ← scratch write goes HERE (safe) AFTER 2017 optimization (buggy): splice(su → AF_ALG socket) ↓ Input pages: [su's page cache] ↕ ← src = dst (in-place optimization) Output pages: [su's page cache] ← scratch write goes HERE (su's RAM!!!) BEFORE 2017 (safe): splice(su → AF_ALG socket) ↓ Input pages: [su's page cache] Output pages: [separate kernel buffer] ← scratch write goes HERE (safe) AFTER 2017 optimization (buggy): splice(su → AF_ALG socket) ↓ Input pages: [su's page cache] ↕ ← src = dst (in-place optimization) Output pages: [su's page cache] ← scratch write goes HERE (su's RAM!!!) ATTACKER (normaluser) KERNEL ───────────────────── ────── 1. open("/usr/bin/su") → file descriptor to su 2. socket(AF_ALG) → crypto socket opened setsockopt(authencesn...) → algorithm configured 3. splice(su_fd → socket) → su's page cache pages wired directly into crypto engine I/O 4. sendmsg() → crypto runs carefully chosen assoclen 4-byte scratch write + cryptlen lands at target offset inside su's page cache 5. repeat for each → shellcode injected 4 bytes of shellcode byte by byte into RAM 6. os.system("su") → kernel executes RAM version (not disk version) ↓ setuid(0) → root execve("/bin/sh") → shell ATTACKER (normaluser) KERNEL ───────────────────── ────── 1. open("/usr/bin/su") → file descriptor to su 2. socket(AF_ALG) → crypto socket opened setsockopt(authencesn...) → algorithm configured 3. splice(su_fd → socket) → su's page cache pages wired directly into crypto engine I/O 4. sendmsg() → crypto runs carefully chosen assoclen 4-byte scratch write + cryptlen lands at target offset inside su's page cache 5. repeat for each → shellcode injected 4 bytes of shellcode byte by byte into RAM 6. os.system("su") → kernel executes RAM version (not disk version) ↓ setuid(0) → root execve("/bin/sh") → shell ATTACKER (normaluser) KERNEL ───────────────────── ────── 1. open("/usr/bin/su") → file descriptor to su 2. socket(AF_ALG) → crypto socket opened setsockopt(authencesn...) → algorithm configured 3. splice(su_fd → socket) → su's page cache pages wired directly into crypto engine I/O 4. sendmsg() → crypto runs carefully chosen assoclen 4-byte scratch write + cryptlen lands at target offset inside su's page cache 5. repeat for each → shellcode injected 4 bytes of shellcode byte by byte into RAM 6. os.system("su") → kernel executes RAM version (not disk version) ↓ setuid(0) → root execve("/bin/sh") → shell setuid(0) → lock in root privileges execve("/bin/sh") → replace process with a shell exit(0) → cleanup if execve fails setuid(0) → lock in root privileges execve("/bin/sh") → replace process with a shell exit(0) → cleanup if execve fails setuid(0) → lock in root privileges execve("/bin/sh") → replace process with a shell exit(0) → cleanup if execve fails Disk: sha256(/usr/bin/su) = a1b2c3d4... ✅ clean RAM: su's page cache = [shellcode] ☠️ compromised File integrity tools (AIDE, Tripwire) → read from disk → report clean auditd file watches → watch disk writes → see nothing Rebooting → RAM wiped → su reloads clean from disk Disk: sha256(/usr/bin/su) = a1b2c3d4... ✅ clean RAM: su's page cache = [shellcode] ☠️ compromised File integrity tools (AIDE, Tripwire) → read from disk → report clean auditd file watches → watch disk writes → see nothing Rebooting → RAM wiped → su reloads clean from disk Disk: sha256(/usr/bin/su) = a1b2c3d4... ✅ clean RAM: su's page cache = [shellcode] ☠️ compromised File integrity tools (AIDE, Tripwire) → read from disk → report clean auditd file watches → watch disk writes → see nothing Rebooting → RAM wiped → su reloads clean from disk $ uname -r 6.17.0-20-generic $ id uid=1001(normaluser) gid=1001(normaluser) groups=1001(normaluser) $ sha256sum /usr/bin/su 44900c631391f0d60eb6d271b8374a08dc1d9be....... /usr/bin/su $ uname -r 6.17.0-20-generic $ id uid=1001(normaluser) gid=1001(normaluser) groups=1001(normaluser) $ sha256sum /usr/bin/su 44900c631391f0d60eb6d271b8374a08dc1d9be....... /usr/bin/su $ uname -r 6.17.0-20-generic $ id uid=1001(normaluser) gid=1001(normaluser) groups=1001(normaluser) $ sha256sum /usr/bin/su 44900c631391f0d60eb6d271b8374a08dc1d9be....... /usr/bin/su $ python3 copy_fail_exp.py # $ python3 copy_fail_exp.py # $ python3 copy_fail_exp.py # # id uid=0(root) gid=0(root) groups=0(root) # whoami root ![SHA remains the same](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6r2s0sx7q1ankvmhnsih.png) # sha256sum /usr/bin/su 44900c631391f0d60eb6d271b8374a08dc1d9be....... /usr/bin/su ↑ exact same hash — disk was never touched # id uid=0(root) gid=0(root) groups=0(root) # whoami root ![SHA remains the same](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6r2s0sx7q1ankvmhnsih.png) # sha256sum /usr/bin/su 44900c631391f0d60eb6d271b8374a08dc1d9be....... /usr/bin/su ↑ exact same hash — disk was never touched # id uid=0(root) gid=0(root) groups=0(root) # whoami root ![SHA remains the same](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6r2s0sx7q1ankvmhnsih.png) # sha256sum /usr/bin/su 44900c631391f0d60eb6d271b8374a08dc1d9be....... /usr/bin/su ↑ exact same hash — disk was never touched uname -r # Vulnerable if kernel is between 4.14 and 6.18.21 # or 6.19.x below 6.19.12 uname -r # Vulnerable if kernel is between 4.14 and 6.18.21 # or 6.19.x below 6.19.12 uname -r # Vulnerable if kernel is between 4.14 and 6.18.21 # or 6.19.x below 6.19.12 # Block the module echo "install algif_aead /bin/false" | sudo tee /etc/modprobe.d/disable-algif.conf # Remove from memory if loaded sudo rmmod algif_aead 2>/dev/null || true # Verify it is blocked sudo modprobe algif_aead # Expected: modprobe: ERROR: could not insert 'algif_aead' # Block the module echo "install algif_aead /bin/false" | sudo tee /etc/modprobe.d/disable-algif.conf # Remove from memory if loaded sudo rmmod algif_aead 2>/dev/null || true # Verify it is blocked sudo modprobe algif_aead # Expected: modprobe: ERROR: could not insert 'algif_aead' # Block the module echo "install algif_aead /bin/false" | sudo tee /etc/modprobe.d/disable-algif.conf # Remove from memory if loaded sudo rmmod algif_aead 2>/dev/null || true # Verify it is blocked sudo modprobe algif_aead # Expected: modprobe: ERROR: could not insert 'algif_aead' sudo apt update && sudo apt full-upgrade -y sudo reboot uname -r # confirm new kernel version sudo apt update && sudo apt full-upgrade -y sudo reboot uname -r # confirm new kernel version sudo apt update && sudo apt full-upgrade -y sudo reboot uname -r # confirm new kernel version sudo dnf clean metadata && sudo dnf upgrade -y sudo reboot sudo dnf clean metadata && sudo dnf upgrade -y sudo reboot sudo dnf clean metadata && sudo dnf upgrade -y sudo reboot $ python3 copy_fail_exp.py Traceback (most recent call last): ... OSError: [Errno 19] No such device $ id uid=1001(normaluser) gid=1001(normaluser) groups=1001(normaluser) $ python3 copy_fail_exp.py Traceback (most recent call last): ... OSError: [Errno 19] No such device $ id uid=1001(normaluser) gid=1001(normaluser) groups=1001(normaluser) $ python3 copy_fail_exp.py Traceback (most recent call last): ... OSError: [Errno 19] No such device $ id uid=1001(normaluser) gid=1001(normaluser) groups=1001(normaluser) - It affects every mainstream Linux distribution built since 2017 - It requires zero special privileges — any normal user can trigger it - The entire exploit is 732 bytes of Python - It leaves no trace on disk — traditional security tools see nothing - It works on Ubuntu, Amazon Linux, RHEL, SUSE — unmodified, out of the box - Discovery & Technical Writeup: Brian Pak and the team at Xint — xint.io/blog/copy-fail-linux-distributions - Official PoC: github.com/theori-io/copy-fail-CVE-2026-31431 - Sysdig Analysis & Detection Rules: sysdig.com/blog/cve-2026-31431