一句话定调 :这是 2026 年 5 月最尴尬的剧本------安全研究员修好了 Dirty Frag(CVE-2026-43284) ,结果修复改动把
skb_try_coalesce()里一个潜伏多年的 flag-propagation 缺陷 推到了可达路径上。修一个 page-cache 写漏洞,引入另一个 page-cache 写漏洞。 安全圈管这叫 regression-induced vulnerability ,黑客管这叫 Christmas。
0 · 档案卡
| 项目 | 内容 |
|---|---|
| CVE | CVE-2026-46300 |
| 代号 | Fragnesia ("frag" = 分片/"amnesia" = 遗忘------skb 把 SHARED_FRAG标记忘掉了) |
| 发现者 | William Bowling(V12 Security / Zellic),借助 AI 辅助审计工具发现 |
| 公开日期 | 2026 年 5 月 13 日(PoC + 补丁同日发布到 netdev 列表) |
| CVSS 3.1 | **7.8(High)** --- AV:L / AC:L / PR:L / UI:N |
| 漏洞类 | 本地权限提升(LPE)------ page-cache 写入原语 |
| **确定性?** | ✅ 无竞态、无偏移泄露、无崩溃------单次 syscall 序列,一发入魂 |
| PoC 状态 | ✅ 已公开(GitHub: theori-io/fragnesia/ V12 的 PoC) |
| 最讽刺的事实 | Dirty Frag 的修复激活了它 ------ Hyunwoo Kim 的 CVE-2026-43284 补丁改了 coalescing 路径的控制流,让一个之前不可达的 SKBFL_SHARED_FRAG丢失路径变得可达 |
1 · 先搞懂:SKBFL_SHARED_FRAG 到底是干嘛的?
1.1 sk_buff 的碎片(frags)不是拷贝,是引用
Linux 网络栈的核心数据结构 sk_buff(简称 skb)承载数据包。一个 skb 的数据可以分为:
-
线性区 (
skb->head ~ skb->tail):实际分配的内存 -
非线性 paged frags (
skb_shinfo(skb)->frags[]):每个skb_frag_t就是一个(struct page *, offset, size)三元组------指向某个页面的引用,不一定有自己的内存
当你用 splice()把一个文件(比如 /usr/bin/su)喂进 socket 时,内核走的是零拷贝路径 :文件的 page cache 页 被直接挂到 frags[]里作为一个引用。磁盘内容没被拷贝,只是 page 指针被链进去了。
1.2 问题来了:后续代码想"写"怎么办?
如果后面有人(比如 ESP 解密)想对这个 skb 的数据做 in-place 修改(AES-GCM 解密就是 XOR 到原地),它必须先搞清楚:
"这个 frag 指向的 page......是不是别人也在用?"
如果是 page-cache 页,别人当然在用(文件系统本身就在用)。所以内核有一套机制来标记这个事实------
SKBFL_SHARED_FRAG ← 标记位,意思是"这个 skb 的 frags 里有外部共享页(尤其是 page cache 页),别原地写!"
带了这个标记的路径,应该走:
skb_cow_data() → 做 COW(Copy-On-Write)私有拷贝 → 再写私有副本
如果不带这个标记,下游就以为 "这是我的私有非线性 skb,可以原地写" → 直接往 page cache 页上 XOR。
1.3 那么标记怎么会丢?
这就是 Fragnesia 的 bug:当 skb_try_coalesce()把 @from的 paged frags 挂到 @to上时,如果 @from带着 SKBFL_SHARED_FRAG,合并后的 @to应该 继承这个标记------但它没有。
skb_try_coalesce(to, from):
to.frags += from.frags ← 把 page 引用搬过去了 ✓
to.flags |= SKBFL_SHARED_FRAG (if from had it) ← ❌ 丢了!
下游 ESP input 路径看到的 skb_has_shared_frag(skb)== false → 跳过 skb_cow_data()→ 原地解密写到 page cache 页。
2 · 攻击链:怎么把这块零件拧成一把 root 扳手
2.1 前置条件(不苛刻)
| 条件 | 为什么 |
|---|---|
| 本地低权限 shell | 永远是 "already-in" 场景 |
unshare(CLONE_NEWUSER | CLONE_NEWNET)可用 |
在 user namespace 里你能拿到伪 CAP_NET_ADMIN,足以创建 XFRM SA、attach ULP |
CONFIG_INET_ESPINTCP=y/m |
ESP-in-TCP(XFRM ESP over TCP)编译进内核 |
| esp4/esp6 模块可用 | 大多数发行版默认满足 |
Ubuntu 的默认 AppArmor 会限制非特权 user namespace(
kernel.apparmor_restrict_unprivileged_userns=1),这构成了部分缓解 ------但需要sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0就能撤掉。
2.2 六步攻击链(概念级完整版)
① 创建 user netns → 获得伪 CAP_NET_ADMIN
───────────────────────────────────────────
unshare(CLONE_NEWUSER | CLONE_NEWNET)
② 打开目标只读文件 /usr/bin/su(O_RDONLY)
用 splice() 把它的一部分页缓存页喂入一个 TCP socket 的接收队列
───────────────────────────────────────────
此刻:su 的 page cache 页被挂到 skb->frags[] 里
内核标记了 SKBFL_SHARED_FRAG ✓ (初始标记是对的)
③ 在这个 TCP socket 上 attach ESP-in-TCP ULP
(Upper Layer Protocol 切换------把该 TCP 连接的接收路径交给 ESP 解码器)
───────────────────────────────────────────
setsockopt(sock, SOL_TCP, TCP_ULP, "espintcp", ...)
④ TCP 接收路径做 skb_try_coalesce() 合并 frags
→ ★ BUG:SKBFL_SHARED_FRAG 在此丢失 ★
→ ESP input 路径检查 skb_has_shared_frag() → false → 不 COW
⑤ ESP 原地 AES-GCM 解密
→ XOR keystream 直接写到 su 的 page cache 页上
→ 攻击者通过控制 IV/nonce(明文已知、ciphertext 可控)
逐块(192 字节/trigger)覆写 su 的关键字节为 shellcode stub
───────────────────────────────────────────
要改的字节数不多:只需把 su 的前几条指令覆盖为
setuid(0) → execve("/bin/sh") 的 machine code stub
⑥ 执行 /usr/bin/su(仍是磁盘上的 setuid-root 二进制)
→ 内核映射已被污染的页缓存页 → 以 root 身份跑你的代码 → 🎉
关键美学 :磁盘上的
/usr/bin/su完全没有被修改 。sha256sum /usr/bin/su结果不变。改的只是 RAM 里的 page cache------echo 3 > /proc/sys/vm/drop_caches一刷就恢复。
3 · 为什么 Fragnesia 比 Dirty Frag 更"优雅"(对攻击者而言)
| 维度 | Dirty Frag (CVE-2026-43284) | Fragnesia (CVE-2026-46300) |
|---|---|---|
| 入口 | UDP datagram splice()路径漏设 SKBFL_SHARED_FRAG |
TCP skb_try_coalesce()丢掉 已有的 SKBFL_SHARED_FRAG |
| 写原语 | ESP 原地解密 → 4 字节受控 STORE / 轮 | ESP 原地解密 → 192 字节 XOR/轮(更快、更灵活) |
| 需要哪个模块 | esp4/esp6 或 rxrpc | 仅 esp4/esp6(espintcp 路径) |
| 与 Dirty Frag 修复关系 | 原始 bug | 被修复改动激活的 latent bug |
| 本质段位 | "忘了锁门" | "门其实锁了,但搬家具时把'已锁'牌子碰掉了" |
最值得品味的句子来自 Hyunwoo Kim(Dirty Frag 原作者)本人:
"Fragnesia was activated by the Dirty Frag fix --- not a missed variant, but a regression introduced by the remediation."
这在安全工程史上有一个经典名字:Incomplete Fix / Regression-Induced Vulnerability。修 A 的时候改变了控制流形状,让原本不可达的 B 变成可达,而 B 也有同样的 root cause(flag 不传播)但藏得更深。
4 · 上游修复:两行补丁,四两拨千斤
上游修复提交信息(kernel.org / netdev 列表,2026-05-13)核心内容
c
cpp
// net/core/skbuff.c --- skb_try_coalesce()
bool skb_try_coalesce(struct sk_buff *to, struct sk_buff *from, ...)
{
// ... 原有逻辑把 from->frags 挂到 to->frags ...
// ★ 新增:传播共享标记 ★
+ if (skb_shinfo(from)->flags & SKBFL_SHARED_FRAG)
+ skb_shinfo(to)->flags |= SKBFL_SHARED_FRAG;
return true;
}
以及更完整的修复还覆盖了兄弟路径(__pskb_copy_fclone()、skb_shift()、skb_gro_receive()等同样漏传播的 helper),因为 TLCTC 分析和 NVD 变更记录表明这是一类模式bug而非单点:
c
cpp
// __pskb_copy_fclone() 和 skb_shift() 也需要:
+ if (skb_shinfo(from)->flags & SKBFL_SHARED_FRAG)
+ skb_shinfo(new)->flags |= SKBFL_SHARED_FRAG;
这就是全部------让 invariant 恢复 :只要 frags 里有外部共享页,合并后的 skb 必须继续保持 shared-frag 标记,下游才能正确走 COW。
5 · 影响范围速判
版本维度
| 范围 | 状态 |
|---|---|
| < 4.11 | 大概率不受影响(espintcp ULP 还没存在 / coalescing 路径形态不同) |
| **4.11 ~ 修复日(2026-05-13)** | ⚠️ 受影响,尤其 5.x / 6.x 全系 LTS |
| 已修复的 stable | 5.10.206+、5.15.206+、6.1.172+、6.6.138+、6.12.87+、6.18.28+、≥ 7.0.5 等(各发行版 pkg 名不同) |
发行版快照(公开状态)
| 发行版 | 状态(截至 5月中) |
|---|---|
| AlmaLinux | ✅ 已发修复内核(8/9/10 均有 errata) |
| CloudLinux | ✅ 测试中 + KernelCare livepatch 验证中 |
| Fedora | ✅ 7.0.6 含修复 |
| Amazon Linux | ❌ 不受影响(CONFIG_INET_ESPINTCP未编译) |
| Alibaba Cloud Linux | ❌ 不受影响(同上,ESPINTCP未编译) |
| Ubuntu / Debian / RHEL / CentOS Stream / openSUSE | ⚠️ 多数在 "needs evaluation / pending" 状态------需要手动确认你的内核是否含 5月13日后的 patch |
一键自查
bash
# 1. 你在哪个内核?
uname -r
# 2. espintcp 编译进去了吗?
grep -E "CONFIG_INET_ESPINTCP|CONFIG_INET6_ESPINTCP" /boot/config-$(uname -r) 2>/dev/null
# =y 或 =m → 攻击面存在
# 3. 模块活着吗?
lsmod | grep -E "esp4|esp6"
# 4. 非特权 userns 开了吗?(攻击前置条件)
cat /proc/sys/kernel/unprivileged_userns_clone 2>/dev/null
sysctl kernel.unprivileged_userns_clone 2>/dev/null
# 或 AppArmor 限制:
cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns 2>/dev/null
6 · 修复 & 缓解(行动清单)
✅ 最终解:升内核(必须重启)
bash
# Ubuntu / Debian
sudo apt update && sudo apt full-upgrade
sudo reboot
# RHEL / Alma / Rocky
sudo dnf update kernel
sudo reboot
验证新内核起来后 uname -r确认版本跳进安全区间。
🛡️ 临时缓解(不能立刻重启时的标准操作)
bash
# 1. 黑名单 + 尝试卸载 ------ 斩断 esp4/esp6/rxrpc 的加载路径
sudo sh -c 'printf "install esp4 /bin/false\ninstall esp6 /bin/false\ninstall rxrpc /bin/false\n" > /etc/modprobe.d/fragnesia.conf'
sudo rmmod esp4 esp6 rxrpc 2>/dev/null || true
# 2. 刷掉可能已被污染的页缓存(不能"治愈"已污染的,但能强制从磁盘重新读)
sync && echo 3 | sudo tee /proc/sys/vm/drop_caches > /dev/null
⚠️ 如果你这台机真跑 IPsec VPN(strongSwan/libreswan 的 kernel-mode ESP),禁用 esp4/esp6 会断隧道------先评估业务。
🐳 容器场景
页缓存在宿主机全局共享,所以容器内的 exploit 也能污染宿主机的 su。你需要的不是容器内的缓解,而是:
-
宿主机内核升级------唯一根治
-
容器 runtime 层限制
CAP_NET_ADMIN(但 unprivileged userns 可以伪造它,所以不够) -
Seccomp 拦
socket(AF_ALG, ...)和 XFRM netlink msg(辅助缩小面)
7 · 这个故事的真正教训
Fragnesia 不是某个程序员"粗心少写一行"那么简单。它暴露的是一个系统性难题:
零拷贝的承诺 = "我用引用,不拷贝,所以快"
安全的前提 = "每个下游消费者都必须尊重引用所有者语义"
现实 = 下游太多、路径太曲折、invariant 靠人的纪律维护 → 必漏
Dirty Pipe、Copy Fail、Dirty Frag、Fragnesia------这四个的名字不同,根因是同一个:
内核用引用传递 page-cache 页到能做 in-place-write 的路径,然后 ownership tracking 在某个拐弯处脱落。
修一次不难,难的是让这个 invariant 机器可验证(Rust-for-Linux 的类型系统、kCFI/Clang 的 stricter analysis、或者形式化验证级别的 skb API redesign)。在那之前,这个家族还会出续集。
⚠️ 收尾声明
CVE-2026-46300 的 PoC 已公开且极低门槛 (不需要编译、不需要竞态调参、不需要内核地址泄露)。本文仅限合法授权环境(自有设备 / 授权渗透测试 / 企业红队 / CTF)。在未授权系统上跑它 = 刑事入侵行为。