CVE-2026-31431 Copy.Fail 在容器逃逸上的想法
Created At: Apr 30, 2026今天有一个奇怪的洞 Copy Fail (CVE-2026-31431)。看了一圈都在讲主机上 732 字节脚本拿 root 的事,官方 Part 2 才会写容器逃逸。这篇就先聊聊我自己的想法——这玩意儿在容器里到底怎么打。
TL;DR
能逃,但是需要一些特殊的权限和配置。
背景
先讲三个概念。
Page Cache
Linux 不会每次读文件都跑磁盘。进程读文件,内核把内容缓存在内存里,后续读直接先找缓存,找不到才是缺页 + reload。
这里有个背景知识 page cache 以 inode 为粒度。不管同一个文件被多少进程打开,内核里只有一份 Page Cache。
splice
splice() 是 Linux 的 Zero Copy 机制,通过 pipe 在两个 fd 之间搬数据,不走用户态,内核里直接传页面引用:
文件 ──splice──→ pipe ──splice──→ socket
pipe 持有的就是源文件 page cache 页面的引用。
AF_ALG
AF_ALG 是内核暴露给用户态的密码学接口。一个普通用户开个 socket,bind 到 AEAD,就能直接调内核的加解密。
漏洞根因
这块照着 Theori 的 writeup 复述一遍,他们写得已经很清楚了。
先看 in-place 操作
用户通过 splice 把文件喂进 AF_ALG socket 后,socket 的输入 scatterlist 直接持有 page cache 的引用。
AEAD 解密的输入格式是 AAD || ciphertext || tag。2017 年 algif_aead 加了个 in-place 优化:解密时,AAD 和 ciphertext 通过 memcpy 拷到用户的 RX buffer,但 tag 那段是用 sg_chain() 直接挂到输出 scatterlist 末尾的,也就是说 page cache 的页面被链到了"可写"的目的 scatterlist 上。
Input SGL: AAD || CT || Tag
| | ^
| copy | | sg_chain (还指向 page cache 页面)
v v |
Output SGL: AAD || CT -----+
然后 req->src = req->dst,都指向这个组合链。
然后是 authencesn 的 scratch write
authencesn 是 IPsec 用的 AEAD wrapper,处理 64 位 ESN(扩展序列号)。ESN 高 32 位 seqno_hi 和低 32 位 seqno_lo 在 wire 上和 HMAC 输入的位置不一样,所以解密时需要重排。
crypto_authenc_esn_decrypt() 里:
scatterwalk_map_and_copy(tmp, dst, 0, 8, 0); // 读 AAD 0-7 字节
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1); // 把 dst[4..7] 覆盖成 seqno_hi
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); // 在 tag 之后写 seqno_lo
第三行,在 assoclen + cryptlen 写 4 字节,越界到 tag 那段了。
两个独立的"合理"决策叠在一起
- 2011:authencesn 加进内核,scratch write 设计就是这样,当时 AAD 在独立的 scatterlist 里,xfrm 内部用,对其他人没影响
- 2015:AF_ALG 支持 AEAD,但当时是 out-of-place,page cache 页面只在 src 里(只读),scratch write 落在 dst(用户 buffer),没事
- 2017:algif_aead 改成 in-place,page cache 页面被 sg_chain 挂到了可写的目的 scatterlist 上
到这里,authencesn 那个写出合法输出区的 scratch write,正好就落到了 page cache 页面上。
攻击者控制三件事:
- **写哪个文件: **任何当前用户能读的文件
- **写哪个偏移: **通过 splice 起始偏移、长度、assoclen 三个参数算出来
- **写什么值: **就是 sendmsg 里 AAD 的 4-7 字节
那么这样就可以每次4字节,想写什么写什么了。而且页面不会被标 dirty,在文件不修改的基础上,下次有人读这个文件,内核就会优先从 page cache 里拿,所以会读到被改的版本。
Exploit
PoC 目标默认是 /usr/bin/su:
- AF_ALG socket bind 到
authencesn(hmac(sha256),cbc(aes)),setkey、accept 拿操作 fd - 每个 4 字节 chunk:
sendmsg(AAD 里塞要写的 4 字节, MSG_MORE),然后splice(目标文件 → pipe → AF_ALG socket),参数算好让dst[assoclen + cryptlen]落在 su 里要改的偏移 - recv() 触发解密,authencesn 把 seqno_lo 写进 page cache,HMAC 失败 recvmsg 报错,但那 4 字节留下了
- 全部写完,execve("/usr/bin/su"),内核从 page cache 加载被注入 shellcode 的版本,SUID 生效
容器场景下的 Page Cache 污染
容器里个人感觉关键在 OverlayFS 和文件共享。
OverlayFS 和 Page Cache
容器的文件系统是 OverlayFS:
容器视图
├─ upper layer (可写,容器跑起来后改的东西)
└─ lower layer (只读,从镜像来的)
│
└─ 实际 inode → Page Cache
↑
共享! 同一镜像 = 同一 inode = 同一份 page cache
容器读 lower layer 的文件,page cache 缓存的是底层的 inode 的内容。多个容器跑同一镜像,共享同一份 page cache。

那思路就很直接:
- 找节点上有哪些镜像,相同 layer 在 overlayfs 下其实是同一个 inode
- 改底层layer可能被触发的二进制、so 文件
- 其他容器一旦碰到对应的 page cache,就执行 payload
- 突破隔离
场景 1:打 DaemonSet
集群里 DaemonSet 满天飞。复制一份 DaemonSet 用的镜像起个 pod,攻击者在自己 Container A 里跑 exploit,污染的 page cache 被同节点的 DaemonSet Container B、C 同时使用。
很多 DaemonSet 会周期性跑任务或者拉起子进程,改一下它频繁调用的二进制就行——监控、日志守护进程里这种调用一抓一大把。下次执行就触发 payload。
可以直接用execsnoop,看下 ppid,和执行的什么内容,那么可以当场搞事。
而且 DaemonSet 普遍权限较高,或者挂了宿主目录,剩下的就是常规集群渗透。
场景 2:hostPath 也跑不掉?
AI GPU 业务里,宿主机的 nvidia 驱动一般都 bind mount 进容器。
容器里和宿主的 inode 是同一个,按这次漏洞,容器里有人用 splice 把 nvidia-smi 这种 bin 或者 .so 改了,宿主一旦用到这些东西就直接 RCE。


场景 3:PaaS
再看 PaaS 这种多租户场景。每个用户启动的镜像都一样,攻击者摸清楚厂商的镜像启动会调哪些东西,改 page cache,后面其他人起的容器全都可以带这你的 payload。
横向移动
把这些能力组合起来:
低权限 Pod (没特殊 capability)
│
├─ 1. 污染 lower layer 的文件,比如 /usr/bin/python3
│ 注入:启动时反弹 shell
│
├─ 2. 等同节点其他 Pod 调 python3
│ 触发点:health check、cron、应用启动
│ (大部分 Python 服务几秒钟就会触发一次)
│
└─ 3. 在目标 Pod 上下文里拿到执行权
能用的东西:
├─ ServiceAccount token → K8s API 横向
├─ 挂的 Secret / ConfigMap
├─ 网络策略允许的内网访问
└─ 更高的 Linux capability
总结
OverlayFS 这条思路只是抛砖引玉,证明这个 CVE 在容器场景下的实际影响。官方的 Part 2 还没出,但是他们既然提到"From Pod to Host",那就感觉应该会有更优雅的姿势,等出了对照看。