
Proof-of-concept exploit for CVE-2026-31431, a Linux kernel local privilege escalation via splice() and crafted socket options, enabling arbitrary kernel memory write to overwrite setuid binaries.
copyFail.py is a proof-of-concept (PoC) exploit that demonstrates a local privilege escalation vulnerability in the Linux kernel. By abusing the splice() system call in combination with crafted socket options, the exploit gains the ability to corrupt arbitrary kernel memory. It uses this capability to overwrite any setuid binary on disk with a custom ELF executable, then runs the tampered binary to obtain elevated privileges.
Attack type: Local privilege escalation (LPE) Impact: Full root shell access (when targeting setuid-root binaries) Complexity: Low (single script, no external dependencies)
Usage:
./copyFail_cleaned.py [target_binary] # default: /usr/bin/su
The original PoC hardcoded /usr/bin/su, but the underlying kernel vulnerability applies to any file — the attacker can overwrite any setuid binary they choose. The cleaned version accepts an optional command-line argument to specify the target, making this behavior explicit.
The Linux kernel provides a splice() system call that copies data between two file descriptors entirely inside the kernel — without copying data into userspace first. This is called "zero-copy I/O" and is used for performance in high-throughput applications like web servers and reverse proxies.
The vulnerability arises because the kernel does not properly validate the internal state of certain socket structures when splice() is used on PACKET sockets that have been configured with unsupported protocol-level socket options. By carefully crafting these options, an attacker can corrupt kernel pointers, and then use splice() to redirect kernel memory writes to arbitrary locations.
In simple terms: the kernel has a shortcut for moving data around internally, and this exploit tricks that shortcut into writing data in the wrong place — specifically, into the in-memory copy of a system binary.
The exploit follows a clear sequence of steps:
The exploit creates a raw PACKET socket (AF_PACKET / SOCK_RAW) and binds it to intentionally malformed addresses. It then calls setsockopt() using SOL_PNIO (protocol level 279), which is a Solaris-level constant that Linux does not recognize.
Why this matters: When the kernel encounters an unsupported protocol level in setsockopt(), it falls through to a generic handler that doesn't validate the data properly. This leaves kernel memory in an inconsistent, exploitable state.
After establishing a socket connection via accept(), the exploit sends crafted ancillary (control) messages using sendmsg(). These messages have deliberately mismatched length headers — some claim to be shorter than they are, others claim to be longer.
Why this matters: The kernel's control message parser uses these lengths for pointer arithmetic. Mismatched lengths cause the parser to read or write beyond intended boundaries, corrupting adjacent kernel structures — specifically, pointers that splice() will later follow.
The exploit creates a pipe and calls splice() twice:
/usr/bin/su into the pipe.Why this matters: The splice() call follows the corrupted kernel pointers planted in Step 2, causing it to write data to an attacker-chosen kernel memory address instead of the socket buffer. This gives the exploit an arbitrary kernel memory write primitive.
The exploit's payload is a 160-byte ELF executable (see below). Each call to the exploit function writes exactly 4 bytes. So the main loop runs 40 times (160 / 4 = 40), each time:
After all 40 iterations, the first 160 bytes of /usr/bin/su have been overwritten.
Finally, the exploit runs os.system("su"). The kernel loads the modified /usr/bin/su from its page cache (which now contains the attacker's shellcode instead of the real su binary), and the shellcode executes.
The embedded payload decompresses to a 160-byte x86-64 ELF executable containing this shellcode:
; Attempt syscall 105 (execveat) — may not be available on older kernels
xor eax, eax
xor edi, edi
mov al, 0x69 ; syscall 105
syscall
; Fallback: syscall 59 (execve) — the reliable path
lea rdi, [rip+0xf] ; RDI = pointer to "/bin/sh"
xor esi, esi ; RSI = NULL (envp)
push 0x3b ; syscall 59 number
pop eax
cdq ; RDX = NULL (argv)
syscall
; Exit cleanly
xor edi, edi
push 0x3c ; syscall 60 (exit)
pop eax
syscall
; Data section: "/bin/sh\0\0\0"
What it does: Spawns /bin/sh with no arguments, inheriting the privileges of the process that ran su. If the exploit runs as root (or with capabilities that grant su root access), the resulting shell is a root shell.
What it does NOT do:
This is a straightforward one-shot privilege escalation, consistent with a research PoC.
copyFail.py
├── hex_to_bytes() — hex string decoder
├── exploit_splice() — core exploit (socket setup + corruption + splice)
│ ├── Phase 1: Create PACKET socket, bind, setsockopt (SOL_PNIO)
│ ├── Phase 2: accept() connection
│ ├── Phase 3: sendmsg() with crafted ancillary messages
│ ├── Phase 4: pipe() + splice() to corrupt kernel memory
│ └── Phase 5: recv() attempt (solidifies corruption)
└── Main loop:
├── Open /usr/bin/su (read-only)
├── Decompress embedded payload (zlib → 160-byte ELF)
├── Loop: inject 4 bytes per iteration (40 iterations total)
└── Execute tampered su → root shell
The exploit opens /usr/bin/su with O_RDONLY (read-only). It doesn't need write access because the corruption happens through the kernel page cache — the in-memory copy of the file that the kernel uses before flushing changes to disk. The splice()-based write bypasses normal file permissions entirely, going straight to kernel memory.
/usr/bin/su binary (compare hash against package manager)setsockopt() calls with unknown protocol levelssplice() syscalls on non-regular file descriptorssysctl to restrict AF_PACKET socket creation by unprivileged userscopyFail.py (as distributed)copyFail_cleaned.py (same directory)net/packet/, fs/splice.c, net/core/sock.cThis report is for security research and defensive analysis purposes only.
| Constant | Value | Purpose in Exploit |
|---|
AF_PACKET | 17 | Raw packet socket family |
SOCK_RAW | 3 | Raw socket type |
SOL_PNIO | 279 | Unsupported protocol level (Solaris) |
MSG_DONTWAIT | 0x400 | Non-blocking sendmsg flag |