CVE-2026-98432 是今年十月最值得认真对待的内核漏洞。CVSS 3.1 评分 9.8,影响 Linux 4.4 之后的所有版本,攻击者不需要任何凭证,只要目标机器上有一个监听套接字。十月这批修复里它最不显眼:上游补丁只改了两行,把两个 kfree() 换成 kfree_rcu()。

两行改动说明这个 bug 极难被静态审查发现,修起来便宜但部署麻烦:内核更新不重启不生效,而发行版补丁比上游慢了两周。

bug 在哪:request socket 把 option 的所有权让渡出去

Linux 处理 TCP 握手分两个阶段:半开连接记在一个叫 request socket 的轻量结构里,握手完成后才创建完整的 child socket。如果对端的 IPv4 头里带了选项(Record Route、Timestamp 之类),内核会把它们存下来,好在 SYNACK 回复里原样带回。

tcp_v4_syn_recv_sock() 把 ireq->ireq_opt 直接转移给子 socket 的 newinet->inet_opt,指针赋值,没有拷贝。问题出在并发:另一个 CPU 可能正在处理同一 request socket 上重传的 SYN,并通过 tcp_check_req() 发 SYNACK。tcp_v4_send_synack() 和 inet_csk_route_req() 只拿 rcu_read_lock() 就读 ireq->ireq_opt,接着 ip_build_and_send_pkt() 和 ip_options_build() 还会把 opt->optlen 读两次。

按提交说明,SYNACK 定时器本身不是问题:它持有 request socket 的引用,inet_csk_reqsk_queue_drop() 又会在 child 释放前调用 timer_delete_sync()。缺口来自 2015 年的 079096f103fa(tcp/dccp: install syn_recv requests into ehash table):request socket 进了 ehash 表之后,处理时不再持有 listener 锁,没有任何东西能阻止子 socket 在 SYNACK 还没拼完时被释放。

TCP 子 socket 没有 SOCK_RCU_FREE 标记,inet_sock_destruct() 释放 inet_opt 用的是裸 kfree()。UAF 的成因就是这么简单:读者在 RCU 保护下访问,写者却在没有 RCU 延迟的情况下直接归还内存。

平行的场景走 request socket 迁移。开了 net.ipv4.tcp_migrate_req=1,或挂了 BPF_SK_REUSEPORT_SELECT_OR_MIGRATE 程序时,reqsk_timer_handler() 用 inet_reqsk_clone() 克隆 request socket,克隆体和原件共享同一个 ireq_opt,然后 reqsk_migrate_reset() 清掉原件里的指针。克隆体释放时 tcp_v4_reqsk_destructor() 同样是裸 kfree(),而另一个 CPU 还握着原件的引用在读这批选项。

补丁为什么只有两行

内核里其他替换 inet_opt 的路径早就用了 kfree_rcu():do_ip_setsockopt()、cipso_v4_sock_setattr() 都是。RCU 约定在这个子系统里存在很久,只是这两个析构函数漏在约定之外。Eric Dumazet 的修法就是补齐漏项:

/* net/ipv4/af_inet.c — inet_sock_destruct() */
-	kfree(rcu_dereference_protected(inet->inet_opt, 1));
+	kfree_rcu(rcu_dereference_protected(inet->inet_opt, 1), rcu);/* net/ipv4/tcp_ipv4.c — tcp_v4_reqsk_destructor() */
-	kfree(rcu_dereference_protected(inet_rsk(req)->ireq_opt, 1));
+	kfree_rcu(rcu_dereference_protected(inet_rsk(req)->ireq_opt, 1), rcu);

提交是 2 files changed, 2 insertions(+), 2 deletions(-)。kfree_rcu() 等所有 RCU 读者退出临界区才释放,还在拼 SYNACK 的那个 CPU 手上的指针继续有效。

IPv6 不受第一个问题影响,因为 tcp_v6_syn_recv_sock() 复制了选项,没有做所有权转移。但 tcp_v6_reqsk_destructor() 存在同样的迁移问题,只是 ipv6_opt 只在 CALIPSO 场景下被设置,提交里明说这部分"will be addressed in a separate patch"。盯 IPv6 补丁时别漏这条尾巴。

攻击链:远程打穿 KASLR,再劫持回调

Anthropic 的 Frontier Red Team 在披露文章里给了两阶段思路,漏洞由 Xinyang Ge 报告。

第一阶段破 KASLR。利用 race 让内核按攻击者控制的长度字段拷贝数据、把多余字节回送给对端。只要某次越界读里恰好包含内核指针,地址空间布局就到手了。

第二阶段劫持控制流。同一个 UAF 溢出缓冲区,覆盖一个回调指针,指向攻击者事先用某个常规协议特性放进内核内存的 payload。内核调用该回调时,payload 串起一串已有内核函数,最后执行一条指定命令,权限是 root。

这个 race 有对攻击者极度有利的性质:抢输了只是握手失败,机器不崩,可以无限重试。PoC 实测泄露一次内核指针要 48 分钟,再花 5 分钟走到 shell 命令执行。听起来慢,但它不需要交互和凭证,可以挂着跑一晚上。Anthropic 没有公开利用代码,按披露政策厂商有 90 天修补期,之后还有 45 天才谈细节。

为什么 MPTCP 成了关键前置条件

Anthropic 说他们的可用利用需要"Linux server with a multipath TCP listener"。CVE 记录完全没提 MPTCP,Anthropic 也没解释这个前置条件为什么成立。线索在 Go 安全团队 10 月 29 日的通告里:从 Go 1.24 开始,Go 程序在 Linux 上默认给 listen socket 开 MPTCP。go.dev/doc/godebug 写明 multipathtcp 有四个取值,Go 1.24 默认 "2":

"0": disable MPTCP on dialers and listeners by default
"1": enable MPTCP on dialers and listeners by default
"2": enable MPTCP on listeners only by default
"3": enable MPTCP on dialers only by default

任何 go.mod 里写着 go 1.24 或更高的 Go 服务,只要内核允许 MPTCP(net.mptcp.enabled = 1,Debian 12 的内核就是开的,AlmaLinux 默认关),监听套接字就是 MPTCP 套接字。国内开发者社区用 Go 写网关和 k8s operator 的团队很多,波及面比想象的大。

Go 团队自己的措辞很克制,但也承认了边界:"the underlying bug is not specific to MPTCP. It is possible that systems with no MPTCP listeners are vulnerable to similar attacks." 并且明确说逐个程序关 MPTCP 是"a fragile mitigation",因为漏掉一个监听就前功尽弃。

先确认你在不在射程内

排查分两步:看内核版本,再看有没有 MPTCP 监听。

uname -r                                        # 正在运行的内核
uname -v                                        # Debian: 实际运行的包版本
dpkg -l 'linux-image*' | grep ^ii               # Debian/Ubuntu 已装内核包
rpm -q kernel                                   # RHEL 系已装内核
needs-restarting -r                             # RHEL: 是否需要重启

第二步:

ss -Mlnp                       # 只列 MPTCP 监听套接字及其进程
sysctl net.mptcp.enabled       # 1 表示允许应用开 MPTCP socket

ss -M 只显示 MPTCP 套接字,返回空就没有 MPTCP 监听。net.mptcp.enabled = 1 单独出现不构成风险,必须由应用真正打开了 MPTCP socket 才算。这些命令要在容器宿主机和 k8s 节点上跑,不是在容器里跑:容器看到的是宿主内核,换镜像对这件事毫无意义。

补丁状态:上游快,发行版慢

上游修复从 10 月 11 日开始铺开,到 10 月 18 日全部落地:

分支 修复日期
7.2.10 / 6.18.56 10 月 11 日
6.12.113 / 6.6.159 / 6.1.190 / 5.15.223 / 5.10.272 10 月 15 日
7.3 10 月 18 日
4.4 ~ 5.4 上游已 EOL,仅靠发行版回移

按 10 月 31 日的核对,发行版这边参差不齐:

  • Debian:12 bookworm 是 6.1.190-1(DLA-4841-1)和 linux-6.12 6.12.113-1~deb12u1(DLA-4850-1),13 trixie 是 6.12.113-1(DSA-6568-1),已修复。
  • Amazon Linux:全线修复。AL2023 的默认内核、kernel6.12、kernel6.18 都有对应的 ALAS 公告,AL2 的 5.4/5.10/5.15 Extras 内核也跟上了。
  • Oracle Linux:UEK 内核 10 月 16 日出 errata,ELSA-2026-500397/500398/500399,Oracle Linux 7 到 10 覆盖。
  • SUSE:SLES 16.0 和 15 的 SP4/SP5/SP6 LTSS 内核已修复,15 SP7 还在做。
  • AlmaLinux:8/9/10 的修复内核 10 月 30 日进了 testing 仓库,比 RHEL 还早。
  • Ubuntu:截至 10 月 31 日,所有发行版的内核在 tracker 上仍标"Vulnerable",26.10/26.04/24.04/22.04 写着"work in progress",20.04 及更早只写 vulnerable,没有对应的 USN。
  • RHEL:8、9、10 和 OpenShift 4、5 都受影响且无修复。RHEL 10 月 30 日把评级从 Important(8.1)提到了 Critical(9.8),公告编号 RHSB-2026-016,措辞是"Red Hat is expediting the release of fixes"。

同一个漏洞,内核 CNA 和 Red Hat 评 9.8,Oracle 和 Amazon 评 8.1,SUSE 说 important,Ubuntu 说 high。差异在评分假设:两家云厂商大概不认为默认配置下能达成 RCE,内核 CNA 按最坏情况算。横向比较不同厂商的 CVSS 数字,很容易被这个差异糊弄。

打不了补丁时能做什么

Red Hat 和 AlmaLinux 都给了一条 nftables 规则,丢掉带 IPv4 选项的 TCP 包。Record Route 和 Timestamp 这类选项在正常 TCP 流量里几乎不存在,丢掉的代价接近零:

nft add table ip cve_2026_98432
nft add chain ip cve_2026_98432 pre '{ type filter hook prerouting priority -300; }'
nft add rule ip cve_2026_98432 pre meta l4proto tcp ip hdrlength gt 5 counter drop

ip hdrlength gt 5 匹配 IPv4 头部长度超过 20 字节最小值的情况,超过 20 字节只可能是带了选项。规则挂在 prerouting,比 Red Hat 那条挂 input 的覆盖更广,主机转发给容器的包也会被拦。iptables 体系的等价写法挂在 raw 表,同样在 conntrack 之前:

iptables -t raw -I PREROUTING -p tcp -m u32 --u32 "0>>24&0xF=6:15" -j DROP

u32 读头部长度字段,值域 6 到 15 即代表带选项。有个坑:这条规则依赖内核的 xt_u32 模块,Debian 12、Ubuntu 22.04/24.04、RHEL 8 都带,RHEL 9 和 10 的内核及其衍生版去掉了,只能走 nft 那条。

规则有三处局限,别当成补丁用。Red Hat 自己的说法是"reduces exposure to this vulnerability but cannot fully prevent it"。AlmaLinux 提醒同宿主上容器之间的桥接流量可能绕过 prerouting,依赖 CIPSO 安全标签的网络也不能用,因为 CIPSO 标签本身就走 IPv4 选项。

如果确认不用 MPTCP,直接关掉。消除的是利用的前置条件,不是漏洞:

echo 'net.mptcp.enabled = 0' | sudo tee /etc/sysctl.d/90-disable-mptcp.conf
sudo sysctl -w net.mptcp.enabled = 0
sudo ss -Mlnp          # 之前已开的监听仍是 MPTCP,要重启对应服务

开关按 network namespace 生效,每个新建的 namespace 默认又是开的,Docker 里要显式传 --sysctl net.mptcp.enabled=0。对单个 Go 程序可以用 GODEBUG=multipathtcp=0,或在 go.mod 里写 godebug multipathtcp=0 编译期关掉。

时间线里的两个信号

10 月 29 日 18:34,Go 安全团队发出 MPTCP 默认开启的通告。同一天 19:22,第一个描述 race trigger 的 GitHub 仓库出现。10 月 31 日,引用这个 CVE 的公开仓库已有 36 个,至少 6 个自称包含 race trigger 或 PoC。从默认值被公开到 PoC 落地,中间隔了不到一小时。

另一个信号是 KEV。截至 10 月 31 日,这个 CVE 还没进 CISA 的已知在野利用目录。这不代表安全,只代表还没观察到。Anthropic 的 PoC 拿到 root 需要 53 分钟且有失败率,对勒索团伙的吸引力不如即插即用的 RCE。但等 Ubuntu 和 RHEL 的补丁出来、等有人把 PoC 优化到分钟级,情况会变。

一个开发者的视角

我手上有几台 Debian 12 构建机和一台跑 Go 服务的测试节点。测试节点大概率命中 multipathtcp="2" 那一类,服务是 Go 1.24 编的。处理顺序是这样:

  1. 先跑 ss -Mlnp 把有 MPTCP 监听的机器列出来。这才是真正的高危集合,优先级排在其他所有主机前面。
  2. uname -r 对照发行版 tracker,不看上游版本号。回移的补丁带自己的版本号,拿上游 6.12.113 比对 Debian 的 6.12.113-1~deb12u1 只会让人困惑。
  3. 确认装上的内核比正在跑的新,那就排 reboot。内核更新不重启等于没装,这条在 k8s 节点上尤其容易漏。
  4. 一时不能重启的,先上 nft 规则,再考虑关 MPTCP。规则不是补丁,只能降低概率。

最后一句,这个漏洞不需要应用层配合,攻击面和"有没有暴露服务"关系不大,只和"有没有监听 TCP"有关。只开着 SSH 的机器也算。

⚠️ 网络安全免责声明

本文内容仅供技术研究与安全学习之用。文中描述的漏洞分析、攻击链拆解和代码示例均基于公开披露的安全研究,旨在帮助开发者理解攻击原理、提升安全意识和防御能力。

请勿将本文所述技术用于任何未经授权的安全测试、渗透攻击或非法用途。任何因滥用本文信息而造成的法律后果,由使用者自行承担,与本文作者无关。

如发现系统安全漏洞,请通过合法渠道向相关厂商或平台报告,共同维护网络安全生态。


参考来源:内核修复提交 e3f33b0 · Anthropic 漏洞研究披露 · Go 安全团队通告 · Red Hat RHSB-2026-016 · Atlas CVE-2026-98432 分析