Go 1.24 默认用 MPTCP 监听:Linux 内核 CVE-2026-98432 的排查与缓解笔记
漏洞概况
CVE-2026-98432 是 Linux 内核 IPv4 TCP options 路径上的 RCU use-after-free,CVSS 3.1 评分 9.8(Critical)。
- 引入版本:Linux 4.4,commit
079096f103fa("tcp/dccp: install syn_recv requests into ehash table")。 - 上游修复:2026-10-05 进入主线。
- 触发前提:目标暴露 MPTCP listener 时,存在已公开的零点击、未认证远程 root 利用。
机制
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。
由于 request socket 自 4.4 起在 ehash 中无 listener 锁处理,子 socket 可能在 SYNACK 还在构造时就被释放。TCP 子 socket 没有 SOCK_RCU_FREE,inet_sock_destruct() 用普通 kfree() 释放 inet_opt,于是 ip_options_build() 读到已释放内存。
迁移路径同理:设置 net.ipv4.tcp_migrate_req=1 或加载 BPF_SK_REUSEPORT_SELECT_OR_MIGRATE 程序时,reqsk_timer_handler() 用 inet_reqsk_clone() 克隆 request socket,原 socket 与克隆共享 ireq_opt,reqsk_migrate_reset() 清掉旧指针;tcp_v4_reqsk_destructor() 同样用普通 kfree()。
修复由 Eric Dumazet 完成,把两处 kfree() 换成 kfree_rcu():
net/ipv4/af_inet.c的inet_sock_destructnet/ipv4/tcp_ipv4.c的tcp_v4_reqsk_destructor
IPv6 不受第一条路径影响(tcp_v6_syn_recv_sock 会复制 options);tcp_v6_reqsk_destructor 存在同样的迁移问题(ipv6_opt,仅在 CALIPSO 设置时),由另一个补丁处理。
受影响与修复版本
受影响:4.4 起,维护分支上低于下列版本的内核。
| 分支 | 修复版本 |
|---|---|
| 5.10 | 5.10.272 |
| 5.15 | 5.15.223 |
| 6.1 | 6.1.190 |
| 6.6 | 6.6.159 |
| 6.12 | 6.12.113 |
| 6.18 | 6.18.56 |
| 7.2 | 7.2.10 |
| 7.3 | 7.3(7.3-rc7 之前受影响) |
发行版状态(截至 2026-10-30):Ubuntu 与 RHEL 尚未发布修复内核;Debian 12/13 已有 linux 6.1.190-1(DLA-4841-1)、6.12.113-1(DSA-6568-1 / DLA-4850-1)。
公开利用
2026-10-27,Anthropic Frontier Red Team(研究员 Xinyang Ge)披露零点击未认证远程 root 利用。前提是目标暴露 MPTCP listener。利用先泄内核堆指针破 KASLR(约 48 分钟),再溢出报文缓冲覆写回调指针执行命令(约 5 分钟);输掉竞态只会导致握手失败,可以无限重试。
Go 1.24+ 为什么相关
2026-10-29/30,Go Security Team 发布公告(golang/go#82717、oss-security 2026/10/30):Go 1.24 起(commit 3d4daa38a7,internal/godebugs/table.go)把 multipathtcp 默认值从 "0" 改为 "2"(listeners only)。
结果是 Go 1.24/1.25/1.26 编译的服务在 Linux 上通过 net.Listen("tcp") / http.ListenAndServe 创建的是 IPPROTO_MPTCP(262) 监听 socket,即使客户端从未协商 MPTCP。普通 TCP 客户端照样能连,所以很难察觉,需要 ss -Mltn 才能看到。
GODEBUG multipathtcp 取值:
0:拨号与监听都关1:都开2:只开监听(Go 1.24+ 默认)3:只开拨号
程序内可用 net.Dialer.SetMultipathTCP / net.ListenConfig.SetMultipathTCP 逐 socket 覆盖 GODEBUG。
排查
# 内核版本与分支
uname -r
# 系统是否启用 MPTCP
sysctl net.mptcp.enabled
# 直接看 MPTCP 监听 socket(普通 ss -ltn 看不出来)
ss -Mltnp
# 二进制是用哪个 Go 版本构建的
go version -m /path/to/service
# 代码里是否显式操作过 MPTCP 开关
rg SetMultipathTCP
临时缓解
按优先级:
- 升级内核到上表对应版本并重启。
- 关闭 Go 程序的 MPTCP:
GODEBUG=multipathtcp=0 ./service
或在 go.mod 里固定:
godebug multipathtcp=0
- 系统级关闭,并重启 Go 服务使其生效:
sysctl -w net.mptcp.enabled=0
- 在 nftables 丢弃带 IPv4 options 的入站 SYN:
nft add rule inet filter input ip hdrlength \> 20 tcp flags syn drop
逐程序关闭是脆弱缓解,且这个 bug 不只与 MPTCP 相关,仍必须打内核补丁。
Go 侧显式关闭示例
net.ListenConfig.SetMultipathTCP(false)(Go 1.24+)逐 listener 覆盖:
package main
import (
"context"
"log"
"net"
"net/http"
)
func main() {
lc := net.ListenConfig{}
lc.SetMultipathTCP(false)
ln, err := lc.Listen(context.Background(), "tcp", ":8080")
if err != nil {
log.Fatal(err)
}
log.Fatal(http.Serve(ln, mux))
}
gRPC 同理:把上面得到的 ln 交给 grpcServer.Serve(ln),而不是让 grpc 自己调 net.Listen。
只关 MPTCP 不算修复
这处 UAF 位于通用 IPv4 TCP 路径:tcp_v4_syn_recv_sock() 的指针转移、inet_sock_destruct() 的 kfree() 与 MPTCP 无关。关掉 MPTCP 只是移除了已公开利用所要求的监听 socket 前置条件,未打补丁的内核仍可能经重传 SYN、tcp_migrate_req / reuseport 迁移等其它路径命中同一段竞态。内核补丁是唯一根治手段。