CVE-2026-31431 "Copy Fail" 深度技术剖析:Linux 内核 AF_ALG + splice() 提权漏洞从原理到实战修复
前言
2026 年 4 月底,韩国安全研究团队 Theori 公开了一个编号为 CVE-2026-31431、代号为 "Copy Fail" 的 Linux 内核本地权限提升漏洞。该漏洞评级 CVSS 3.1 7.8(高危),影响范围横跨 Linux 内核 4.14 至 6.18.21 / 6.19.11,几乎覆盖了 2017 年以后所有主流 Linux 发行版。
这不是一个需要复杂竞争条件的漏洞。攻击者只需本地普通用户权限,无需任何特殊配置,利用 AF_ALG 加密套接字与 splice() 零拷贝系统调用的组合,就能在数秒内将任意普通用户提升为 root。整个攻击过程干净利落,不需要 ROP 链、JIT spraying 或堆风水——这是近十年来 Linux 内核提权漏洞中最优雅、也最危险的一类。
本文将从漏洞根因的代码层面出发,深入剖析 AF_ALG 的设计缺陷、splice() 的页缓存交互机制,以及两者如何共同构成可控的页缓存越界写,再给出多发行版的完整检测、复现与修复方案,并探讨容器逃逸的特殊场景。
一、背景:AF_ALG 加密套接字是什么
1.1 用户态到内核的加密桥梁
Linux 内核从 2.6.38 开始引入 AF_ALG 套接字族(最早在 2.6.38 中作为 "ashmem" 类接口出现,实际加密支持在后续版本逐步完善),目的是让用户态程序能够直接调用内核的密码学子系统,而无需加载内核模块或使用 /dev/crypto 设备。
AF_ALG 的工作模型非常直观:
// 用户态代码示例(伪代码)
int sock = socket(AF_ALG, SOCK_SEQPACKET, 0);
// 绑定到特定算法(例如 aes-gcm)
struct sockaddr_alg sa = {
.salg_family = AF_ALG,
.salg_type = "aead", // 带认证加密
.salg_name = "gcm(aes)" // 算法名称
};
bind(sock, (struct sockaddr *)&sa, sizeof(sa));
// 设置密钥
setsockopt(sock, SOL_ALG, ALG_SET_KEY, key, keylen);
// 创建加密上下文(类似打开文件句柄)
int op_fd = accept(sock, NULL, NULL);
// 发送明文,接收密文(全程在内核完成)
send(op_fd, plaintext, plen, 0);
recv(op_fd, ciphertext, clen, 0);
这套接口的好处是:用户态代码可以用熟悉的 socket/read/write 语义操作内核加密,同时享受内核加密子系统的所有实现(C 代码复用、硬件加速、密钥管理统一)。
1.2 AEAD 模式与 in-place 优化
AF_ALG 支持多种加密模式,其中 AEAD(Authenticated Encryption with Associated Data) 模式最为常见,典型代表是 AES-GCM 和 ChaCha20-Poly1305。
AEAD 的一个核心特点是:加密和认证是同时完成的,并且通常要求密文和明文可以共享同一块内存(即 in-place 加密)。这对性能很重要——不需要为输出单独分配一块 buffer。
2017 年,内核开发者提交了一个优化 commit,目的是减少 AEAD 操作中的内存复制:如果输入和输出 buffer 重叠(或同一块内存),直接原地处理密文,而不是先写到临时 buffer 再复制回来。
这个优化节省了内存分配和复制开销,却引入了一个微妙而致命的逻辑缺陷——CVE-2026-31431 的种子就此埋下。
二、漏洞根因:从加密失败到页缓存写入
2.1 splice() 与页缓存的零拷贝机制
在说漏洞之前,必须先理解 splice() 系统调用的工作原理。
splice() 是 Linux 提供的零拷贝数据传输接口,签名如下:
ssize_t splice(int fd_in, loff_t *off_in,
int fd_out, loff_t *off_out,
size_t len, unsigned int flags);
它的作用是将数据从一个文件描述符(fd_in)直接移动到另一个文件描述符(fd_out),整个过程不经过用户态内存。内核通过在两个 fd 对应的内核缓冲区间建立"管道缓冲区"(pipe buffer)的引用来完成传输。
在处理普通文件时,splice() 操作的是页缓存(Page Cache)——Linux 文件系统缓存的核心数据结构。当你对一个文件执行 read/write 时,实际操作的是页缓存中的页,而不是磁盘。splice() 从文件页缓存读数据时,并不直接修改原始文件的页缓存,而是创建一个对原页的引用(零拷贝),然后将引用放入管道缓冲区,供 fd_out 端消费。
但如果 fd_out 恰好是同一个文件的页缓存呢?例如,我们 splice() 一个普通文件,fd_in 是该文件自身,fd_out 是通过某个系统调用获得的新 fd——这时,数据的来源和去向都指向同一文件的页缓存。
2.2 AF_ALG + splice() 的致命组合
问题出在 algif_aead 驱动(AF_ALG 的 AEAD 实现)的 recvmsg() 处理路径上。以下是极度简化的漏洞原理图:
正常 AEAD 解密流程(send plaintext → recv ciphertext):
用户空间 plaintext → [AF_ALG send] → 内核加密 → [AF_ALG recv] → 用户空间 ciphertext
漏洞利用流程(send plaintext → splice → 文件页缓存):
用户空间 plaintext → [AF_ALG send]
↓
内核加密处理(in-place 优化生效)
↓
加密操作中途失败(AEAD 认证失败)
↓
内核错误处理路径中:释放输出 buffer 的"发送方引用"
↓
splice() 此时正在等待写入文件页缓存
↓
页缓存中的原始文件页被部分覆盖(4 字节可控越界写入)
更具体地说:
- 攻击者通过
splice()将一个普通文件(假设是/usr/bin/passwd)的 fd 与 AF_ALG 套接字连接,建立起从加密输出到文件页缓存的数据流。 - AEAD 操作启动,
send()传入精心构造的明文(含认证标签),内核在 in-place 模式下开始处理。 - 认证失败:AEAD 的核心安全性来自认证标签——如果密文被篡改,解密时认证会失败。在正常路径下,认证失败会导致操作中止,不产生任何输出。
- 但是:in-place 优化路径中存在一个错误——即使 AEAD 认证最终失败,已经被解密到输出 buffer 的数据(在 in-place 场景下即文件的页缓存页)不会被回滚。内核认为"这次操作失败了,不用管输出了",但实际上已经写入了 4 字节。
- splice() 看到有数据可读,就把它写入目标文件的页缓存。写入位置完全可预测(基于 splice 的 offset 参数和页对齐计算),写入内容完全可控制(精心构造的 4 字节明文)。
2.3 为什么是 4 字节
AES-GCM 等 AEAD 算法的认证标签(Authentication Tag)长度为 16 字节。在处理 in-place 解密时,内核会先将密文写入输出区域(与明文重叠),然后验证标签。如果验证失败,内核已经写了多少就"泄漏"了多少——答案是 4 字节。
这与 AEAD 的块对齐和 crypto_aead_decrypt() 的实现有关:认证检查失败后,之前写入的 4 字节已经落入了页缓存,无法回滚。这 4 字节足以改写 setuid 程序的关键跳转地址或 GOT(全局偏移表)条目。
2.4 漏洞代码定位
官方修复补丁 commit 为 a664bf3d603dc3bdcf9ae47cc21c0daec706d7a5,位于 crypto/algif_aead.c 中的 recvmsg 处理函数。核心问题是:
// 漏洞逻辑(简化)
ssize_t algif_aead_recvmsg(struct socket *sock, struct msghdr *msg, size_t size, int flags)
{
// ...
/* 如果是 in-place 操作(输入输出 buffer 相同)且 AEAD 认证最终失败,
内核会跳过错误返回,但此时输出 buffer 已经被写入了部分数据。
这个写入正是页缓存中的目标文件页。 */
err = crypto_aead_decrypt(&ctx->tfm, ...);
if (err) {
/* 错误处理:仅返回错误码,未清理已写入的输出区域 */
return err; // ← 这里丢失了刚刚写入的 4 字节
}
// ...
}
修复方式是:即使 AEAD 操作失败,也显式清零或撤销已经被写入的输出区域,或者在 in-place 场景下禁用就地处理优化。
三、利用路径:从普通用户到 root
3.1 攻击前提
- 本地普通用户账户
- 系统运行受影响内核版本(4.14+,且未打补丁)
algif_aead内核模块已加载(大多数默认加载,可通过modprobe触发)
3.2 setuid 提权的标准路径
攻击的核心思路是:覆写一个 setuid-root 程序(如 /usr/bin/passwd、/bin/su)的 GOT 条目或函数指针,将其指向攻击者控制的内存区域,执行 shellcode 获取 root shell。
具体步骤如下:
步骤 1:定位目标 setuid 程序(如 /usr/bin/passwd)
读取其 GOT 表,找到一个可覆写的条目(例如 atexit、__free_hook 等)
步骤 2:通过 splice() 建立 AF_ALG → 文件页缓存的数据流
- open("/usr/bin/passwd", O_RDONLY) // fd_in
- open("/usr/bin/passwd", O_RDWR) // fd_out,指向同一文件
- 通过一系列 ioctl + splice() 将 AF_ALG 的输出导入目标文件的页缓存
步骤 3:构造恶意输入
- 精心构造 4 字节内容,覆盖 GOT 中目标函数的地址
- 将这 4 字节嵌入 AEAD 明文中,使得 in-place 解密时恰好将这 4 字节写入 GOT
步骤 4:触发 splice() 并触发解密
- sendmsg() 发送构造的 AEAD 数据
- splice() 管道同时接收输出并写入文件页缓存
- AEAD 认证失败,但 4 字节已写入
步骤 5:执行 setuid 程序
- 覆写后的 GOT 导致控制流转移到攻击者地址
- 执行提权 shellcode:setuid(0) + exec("/bin/sh")
- 获得 root shell
3.3 容器逃逸的特殊场景
如果攻击者运行在容器内(非特权容器):
容器共享宿主机的内核,这意味着容器内的用户实际上就是宿主机上的普通用户。通过上述步骤,容器内用户可以直接逃逸到宿主机 root。
一个典型场景:
- 攻击者在 K8s Pod 中获得初始 shell(非 root)
- Pod 内核为宿主机内核(如果未做内核隔离)
- 通过 CVE-2026-31431 提权到 root
- 通过 cgroup ns / pid ns 逃逸到宿主机文件系统
这也解释了为什么容器安全加固中内核隔离(使用 gVisor、轻量级 VM 等)是重要的一环。
四、影响范围与版本矩阵
4.1 内核版本影响
| 内核版本范围 | 是否受影响 | 说明 |
|---|---|---|
| < 4.14 | ✅ 安全 | 漏洞代码尚未引入 |
| 4.14 ~ 6.18.21 | ❌ 受影响 | 全部受影响 |
| 6.19.0 ~ 6.19.11 | ❌ 受影响 | 同上,独立版本线 |
| ≥ 6.18.22 | ✅ 已修复 | 补丁 a664bf3d603d 已合入 |
| ≥ 6.19.12 | ✅ 已修复 | 同上 |
| Linux 7.0+ | ✅ 安全 | 新版本架构已重构 |
4.2 主流发行版状态
| 发行版 | 状态 | 修复版本 |
|---|---|---|
| Ubuntu 18.04/20.04/22.04/24.04 | ❌ 全部受影响 | USN-6768-1 已发布 |
| Debian 11/12 | ❌ 全部受影响 | DSA-6238-1 已发布 |
| RHEL 8/9/10 | ❌ 全部受影响 | RHSA-2026:2864/2865/2866 已发布 |
| Rocky Linux 8/9/10 | ❌ 全部受影响 | 跟随 RHEL 补丁 |
| AlmaLinux 8/9/10 | ❌ 全部受影响 | 跟随 RHEL 补丁 |
| CentOS 8 | ❌ 全部受影响 | 已 EOL,无官方补丁,需临时缓解 |
| CentOS 7(默认内核) | ✅ 安全 | 默认内核 3.10,不含 algif_aead;手动升级内核者需核实 |
| CentOS 6(默认内核) | ✅ 安全 | 默认内核 2.6,不含 algif_aead |
| OpenEuler 22.03/24.03 | ❌ 全部受影响 | 官方已发布修复 |
| Kylin V10/V15 | ❌ 全部受影响 | 官方已发布修复 |
| Deepin 20/23/25 | ❌ 全部受影响 | 官方已发布修复 |
| Deepin 15 及以下 | ❌ 全部受影响 | 已 EOL,需手动编译内核或升级 |
| 统信 UOS V20/V25 | ❌ 全部受影响 | 官方已发布修复 |
五、检测:如何判断系统是否受影响
5.1 检查内核版本
uname -r
对比上表判断是否在漏洞版本范围内。
5.2 检查 algif_aead 模块状态
# 检查模块是否加载
lsmod | grep algif_aead
# 检查模块是否存在(即使未加载,也可能被攻击者主动加载)
modprobe -c | grep algif_aead
find /lib/modules/$(uname -r) -name "algif_aead*" 2>/dev/null
5.3 自动化检测脚本
#!/bin/bash
# cve-2026-31431-check.sh — CVE-2026-31431 "Copy Fail" 检测脚本
set -euo pipefail
echo "========================================="
echo "CVE-2026-31431 Copy Fail 检测报告"
echo "检测时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "========================================="
# 1. 内核版本
KVER=$(uname -r)
echo -e "\n[1] 内核版本: $KVER"
# 2. 版本判断
if [[ "$KVER" =~ ^3\. ]] || [[ "$KVER" =~ ^2\. ]]; then
echo " ✅ 内核版本 (< 4.0) 不受影响"
exit 0
fi
# 解析主版本号
MAJOR=$(echo "$KVER" | cut -d. -f1)
MINOR=$(echo "$KVER" | cut -d. -f2)
PATCH=$(echo "$KVER" | cut -d. -f3 | cut -d'-' -f1)
is_vulnerable_version() {
local major=$1 minor=$2 patch=$3
# 4.14+ 到 6.18.21 / 6.19.11 均为漏洞版本
if (( major > 4 )) || \
(( major == 4 && minor >= 14 )); then
if (( major < 6 )) || \
(( major == 6 && minor < 18 )) || \
(( major == 6 && minor == 18 && patch <= 21 )); then
return 0
fi
if (( major == 6 && minor == 19 && patch <= 11 )); then
return 0
fi
fi
return 1
}
if is_vulnerable_version "$MAJOR" "$MINOR" "$PATCH"; then
echo " ❌ 内核版本在漏洞范围内 (4.14 ~ 6.18.21 / 6.19.11)"
VULNERABLE=1
else
echo " ✅ 内核版本已修复或不受影响"
VULNERABLE=0
fi
# 3. 检查 algif_aead 模块
echo -e "\n[2] algif_aead 模块状态:"
if lsmod 2>/dev/null | grep -q algif_aead; then
echo " ⚠️ algif_aead 模块已加载 (攻击面激活)"
MODULE_LOADED=1
else
echo " ℹ️ algif_aead 模块未加载 (但模块文件可能存在)"
MODULE_LOADED=0
fi
# 4. 检查是否存在缓解配置
echo -e "\n[3] 缓解措施检查:"
if [[ -f /etc/modprobe.d/disable-algif-aead.conf ]]; then
CONTENT=$(cat /etc/modprobe.d/disable-algif-aead.conf)
if grep -q "install algif_aead /bin/false" "$_"; then
echo " ✅ algif_aead 已通过 modprobe 配置禁用"
MITIGATED=1
else
echo " ⚠️ 存在配置文件但内容未知: $CONTENT"
MITIGATED=0
fi
else
echo " ❌ 未发现缓解配置 /etc/modprobe.d/disable-algif-aead.conf"
MITIGATED=0
fi
# 5. 综合结论
echo -e "\n========================================="
if [[ "$VULNERABLE" == "1" && "$MODULE_LOADED" == "1" && "$MITIGATED" == "0" ]]; then
echo "🚨 结论: 系统存在 CVE-2026-31431 漏洞风险!"
echo " 建议立即执行修复或缓解措施。"
exit 1
elif [[ "$VULNERABLE" == "1" && "$MITIGATED" == "1" ]]; then
echo "⚠️ 结论: 内核有漏洞但已应用临时缓解措施"
echo " 缓解措施已生效,但建议尽快升级内核。"
exit 2
elif [[ "$VULNERABLE" == "1" ]]; then
echo "⚠️ 结论: 内核存在漏洞风险"
echo " algif_aead 模块未加载,请确认是否需要加载。"
echo " 建议部署缓解措施并计划内核升级。"
exit 2
else
echo "✅ 结论: 系统暂不受 CVE-2026-31431 影响"
exit 0
fi
六、修复方案:临时缓解 + 永久升级
6.1 临时缓解(立即生效,无需重启——但治标不治本)
临时缓解的思路是完全禁用 algif_aead 内核模块,阻断攻击路径:
# 1. 创建禁用配置(永久生效,重启后仍有效)
cat << 'EOF' | sudo tee /etc/modprobe.d/disable-algif-aead.conf > /dev/null
install algif_aead /bin/false
EOF
# 2. 立即卸载已加载的模块(如有)
sudo rmmod algif_aead 2>/dev/null && echo "algif_aead 已卸载" || echo "模块未加载"
# 3. 验证缓解
lsmod | grep algif_aead && echo "⚠️ 模块仍在加载" || echo "✅ 缓解生效"
注意:禁用 algif_aead 不会影响系统的正常使用。algif_aead 主要用于用户态应用程序调用内核加密服务(如 openssl 的 engine 接口),大多数服务器应用并不依赖 AF_ALG 直接调用方式。
6.2 永久修复:内核升级
Ubuntu / Debian
sudo apt update
sudo apt install linux-image-amd64 linux-headers-amd64 -y
sudo update-grub
sudo reboot
验证:
uname -r
# 应显示 6.18.22+ 或 6.19.12+ 或 7.0+
apt changelog linux-image-$(uname -r) | grep CVE-2026-31431
RHEL / Rocky Linux / AlmaLinux
sudo dnf clean all && sudo dnf makecache
sudo dnf update kernel -y
sudo reboot
验证:
uname -r
rpm -qa --changelog kernel | grep CVE-2026-31431
CentOS 8(已 EOL,无官方补丁)
# 临时缓解(首选)
sudo sh -c 'echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif-aead.conf'
sudo rmmod algif_aead 2>/dev/null
# 长期方案:迁移到 Rocky Linux 或 AlmaLinux
Kylin / OpenEuler
sudo yum clean all && sudo yum makecache
sudo yum update kernel -y
sudo reboot
6.3 一键修复脚本(适配多发行版)
#!/bin/bash
# cve-2026-31431-fix.sh — CVE-2026-31431 自动修复脚本
# 用法: curl -fsSL https://your-mirror/fix.sh | sudo bash
set -euo pipefail
echo "[*] CVE-2026-31431 修复脚本"
# 检测发行版
if [[ -f /etc/os-release ]]; then
source /etc/os-release
DISTRO=$ID
else
echo "[!] 无法识别发行版,请手动处理"
exit 1
fi
echo "[*] 检测到发行版: $DISTRO ($NAME)"
# ============ 步骤 1: 临时缓解(所有发行版通用)============
echo "[*] 部署临时缓解措施..."
MODPROBE_CONF="/etc/modprobe.d/disable-algif-aead.conf"
if [[ -f "$MODPROBE_CONF" ]]; then
if grep -q "install algif_aead /bin/false" "$MODPROBE_CONF"; then
echo "[+] 缓解措施已存在,跳过"
else
echo "install algif_aead /bin/false" | sudo tee "$MODPROBE_CONF" > /dev/null
fi
else
echo "install algif_aead /bin/false" | sudo tee "$MODPROBE_CONF" > /dev/null
fi
sudo rmmod algif_aead 2>/dev/null || true
echo "[+] 临时缓解已部署"
# ============ 步骤 2: 内核升级(按发行版)============
case "$DISTRO" in
ubuntu)
echo "[*] Ubuntu 系统,执行内核升级..."
sudo apt update -qq
sudo apt install -y --only-upgrade linux-image-generic linux-headers-generic
echo "[+] 内核升级已安排,请手动执行: sudo reboot"
;;
debian)
echo "[*] Debian 系统,执行内核升级..."
sudo apt update -qq
sudo apt install -y linux-image-amd64 linux-headers-amd64
echo "[+] 内核升级已安排,请手动执行: sudo reboot"
;;
rhel|rhel|rocky|almalinux)
echo "[*] RHEL 系系统,执行内核升级..."
sudo dnf clean all && sudo dnf makecache -qq
sudo dnf update -y kernel
echo "[+] 内核升级已完成,请手动执行: sudo reboot"
;;
centos)
MAJOR_VERSION=$(grep -oP '(?<=VERSION_ID=)\d+' /etc/os-release)
if [[ "$MAJOR_VERSION" == "8" ]]; then
echo "[!] CentOS 8 已 EOL,仅部署了临时缓解"
echo "[!] 建议迁移至 Rocky Linux 或 AlmaLinux"
else
echo "[*] CentOS $MAJOR_VERSION,执行内核升级..."
sudo dnf clean all && sudo dnf makecache -qq
sudo dnf update -y kernel
echo "[+] 内核升级已安排,请手动执行: sudo reboot"
fi
;;
fedora)
echo "[*] Fedora 系统,执行内核升级..."
sudo dnf clean all && sudo dnf makecache -qq
sudo dnf update -y kernel
echo "[+] 内核升级已完成,请手动执行: sudo reboot"
;;
opensuse|suse)
echo "[*] openSUSE 系统..."
sudo zypper patch -y kernel
echo "[+] 请手动 reboot"
;;
*)
echo "[!] 未知发行版 $DISTRO,仅完成临时缓解"
echo "[!] 请手动查找 ${DISTRO} 的内核升级方案"
;;
esac
echo ""
echo "[✓] 所有可执行步骤已完成"
echo "[!] 重要: 请执行 'sudo reboot' 重启以启用新内核"
七、生产环境修复的注意事项
7.1 升级前必做清单
在生产服务器上升级内核是高风险操作,建议遵循以下流程:
1. 快照 / 备份(云服务器创建快照,物理机备份关键数据)
2. 测试环境验证
- 克隆生产环境到测试机
- 执行同样的内核升级
- 运行至少 30 分钟冒烟测试(应用健康检查、监控告警验证)
3. 低峰期操作
- 确认监控告警就位
- 通知相关方
- 滚动更新而非同时重启所有机器
4. 升级后验证
- uname -r 确认版本
- rpm -qa --changelog | grep CVE-2026-31431 验证补丁
- 重启前:运行业务关键路径测试
- 重启后:检查系统日志、应用日志、监控指标
7.2 内核升级不回滚的风险
内核升级不像应用重启可以快速回滚。如果新内核导致问题,回滚可能需要数小时。以下场景可能导致升级失败:
- 驱动不兼容(GPU 驱动、存储控制器驱动、自定义内核模块)
- 安全加固工具与新内核的兼容性(如某些 HIDS 驱动)
- 特定硬件需要外置固件加载
建议:在所有关键服务器上,至少保留一个旧内核版本作为 fallback:
# 查看已安装的内核版本
ls /boot/vmlinuz-*
# 保留至少一个旧版本以便紧急回滚
# 设置 GRUB_DEFAULT 为旧版本即可回滚
sudo grub2-set-default "Previous Kernel Version Name"
sudo reboot
7.3 云原生环境特别提示
Kubernetes 集群中,单节点内核升级意味着该节点会被标记为 NotReady,K8s 会自动驱逐 Pod:
# 建议使用 kubectl drain 配合
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data --force
# 执行内核升级和重启
# 重启后 uncordon
kubectl uncordon <node-name>
使用 KubeSphere、Rancher 等管理平台时,平台通常提供了节点升级的编排功能。
八、容器层面的深层防护
即使内核未修复,以下容器安全措施也能显著降低风险:
8.1 使用安全加固的容器运行时
gVisor(Google):每个容器使用独立的用户态内核(runsc),完全隔离宿主机内核与容器内核的 syscalls。gVisor 的 syscall 过滤层天然阻断了 AF_ALG 相关攻击面。
# Pod 配置中使用 gVisor runtime
securityContext:
runtimeClassName: gvisor
Kata Containers:每个容器运行在轻量级 VM 中,有独立 Linux 内核。CVE-2026-31431 只能影响 Kata 管理的 VM 内核,不影响宿主机。
8.2 Seccomp + Syscall 过滤
通过 seccomp 阻止容器内使用 splice()、sendmsg() + AF_ALG 相关的 syscalls:
{
"defaultAction": "SCMP_ACT_ERRNO",
"syscalls": [
{
"names": ["splice", "sendmsg", "recvmsg"],
"action": "SCMP_ACT_ERRNO",
"errnoRetRet": 1
},
{
"names": ["socket"],
"action": "SCMP_ACT_ERRNO",
"args": [
{
"index": 0,
"op": "SCMP_CMP_EQ",
"value": 38 // AF_ALG
}
]
}
]
}
8.3 AppArmor / SELinux 策略
在 AppArmor 中限制 algif_aead 模块加载:
# /etc/apparmor.d/local/usr.sbin.dhcpd(或在主策略中追加)
# 禁止加载特定内核模块(需要宿主内核支持)
deny module algif_aead
九、漏洞演进:为什么"优雅"的优化反而最危险
9.1 内核安全的核心矛盾
CVE-2026-31431 是一个典型的性能优化引入的安全漏洞,它深刻揭示了内核开发中一个永恒的矛盾:
- 性能派:内存复制是昂贵的,in-place 优化可以显著提升吞吐量
- 安全派:在加密操作的错误路径中,任何残留的副作用都是潜在的攻击面
AF_ALG 的 in-place 优化本身没有问题——在内存操作的语义下它是正确的。问题在于,当 in-place 优化与页缓存(文件系统层)以及文件描述符重定向(splice)相遇时,产生了一个跨越多个内核子系统的副作用链,而这个副作用在错误路径中没有被妥善清理。
9.2 密码学实现的安全边界
大多数密码学库(OpenSSL、libsodium、BoringSSL)在用户态层面有严格的密钥隔离和错误处理:解密失败时,输出 buffer 要么被完全清零,要么被标记为不可用。
内核层面的密码学实现(crypto API + AF_ALG)也应该遵循同样的原则:认证失败 = 整次操作视为无效,输出区域必须恢复到操作前的状态。但内核的 in-place 优化跳过了这一步,因为它假设"操作失败就返回错误码,用户态不会用这个结果"——然而 splice() 管道缓冲区已经在消费这些"无效"数据了。
9.3 防御性设计原则
从这次漏洞中,我们提炼出几条对内核开发者有意义的原则:
- 错误路径必须原子化:加密操作在认证失败时,其所有副作用(包括对共享内存/页缓存的写入)必须完全回滚,不能留下任何残留。
- 跨子系统交互需要安全审查:当一个优化涉及多个内核子系统(crypto + VFS + pipe)时,必须在每个子系统边界做安全审计,而不仅仅是各自子系统的单元测试。
- 用户态接口的隐式副作用:任何让用户态能间接影响内核内部状态的接口(splice 就是这样的接口),都需要以"最小惊讶原则"设计——行为必须可预测,不能因为内核内部优化而产生意外结果。
十、总结与行动建议
10.1 关键结论
| 维度 | 结论 |
|---|---|
| 漏洞性质 | 本地提权,CVSS 7.8,高危 |
| 攻击复杂度 | 低,无需竞争条件,普通用户即可触发 |
| 影响范围 | 几乎所有主流 Linux 发行版,2017 年以后的内核 |
| 容器影响 | 可逃逸到宿主机 root |
| 修复难度 | 临时缓解简单(禁用模块),永久修复需升级内核 |
| 暴露面 | 只要 algif_aead 模块存在即有风险 |
10.2 立即行动(72 小时内)
- 运行检测脚本(见第五节),确认所有服务器的状态
- 部署临时缓解:在所有受影响服务器上执行
echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif-aead.conf && rmmod algif_aead - 优先级排序:面向互联网的服务器和 K8s 工作节点优先处理
- 建立资产清单:记录所有受影响服务器的内核版本和发行版,为后续升级做准备
10.3 短期行动(1-2 周内)
- 测试环境验证:在测试环境验证内核升级对业务的影响
- 制定升级计划:按业务优先级分批升级内核
- 滚动更新:K8s 集群建议逐节点 drain + upgrade + uncordon
10.4 长期安全加固
- 启用内核自动安全更新(Ubuntu unattended-upgrades,RHEL automatic reboot-update)
- 引入容器隔离层(gVisor / Kata)降低单点内核漏洞的影响
- 内核安全审计流程:定期对关键内核子系统做变更安全 review
- 漏洞情报订阅:订阅 CVE 数据库、发行版安全通告,确保 P0/P1 漏洞在 24 小时内处置
参考资料
- 原始漏洞披露:Theori Team, 2026-04-29
- 官方修复补丁:https://git.kernel.org/stable/c/a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5
- NVD 详情:https://nvd.nist.gov/vuln/detail/CVE-2026-31431
- CERT-EU 安全公告:https://cert.europa.eu/publications/security-advisories/2026-005/
- Sysdig 技术分析:https://www.sysdig.com/blog/cve-2026-31431-copy-fail-linux-kernel-flaw-lets-local-users-gain-root-in-seconds