CVE-2026-98432 叠加 Go 1.24 的 MPTCP 默认:先打内核补丁,GODEBUG 只能当临时挡板
结论先说:升级内核是唯一完整的修复。Go 侧的 GODEBUG=multipathtcp=0 只是缓解手段,而且很脆弱——漏掉一个 MPTCP listener,机器依旧暴露;而且它默认赌「所有可行的利用都依赖 MPTCP」,而这个前提本身并不成立。
项目/信息入口:
- NVD:CVE-2026-98432
- Go GODEBUG 文档:go.dev/doc/godebug#go-124
- Go 讨论 issue:go.dev/issue/82717
net包文档:pkg.go.dev/net- LWN 报道:lwn.net
内核侧:一个不限于 MPTCP 的 use-after-free
CVE-2026-98432 的标题是 ipv4: free inet_opt and ireq_opt after an RCU grace period,CVSS v3.1 评分 9.8。问题出在 IPv4 TCP 连接处理路径上,与 MPTCP 协议本身无关。
具体机制: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。
自 commit 079096f103fa("tcp/dccp: install syn_recv requests into ehash table")起,request socket 的处理不再持有 listener 锁,因此子 socket 可能在 SYNACK 还在构建时就被释放。TCP 子 socket 没有 SOCK_RCU_FREE,inet_sock_destruct() 用普通的 kfree() 释放 inet_opt,结果就是 ip_options_build() 里的 use-after-free。
请求 socket 迁移路径(net.ipv4.tcp_migrate_req=1,或挂着 BPF_SK_REUSEPORT_SELECT_OR_MIGRATE 程序)有同样的问题:reqsk_timer_handler() 通过 inet_reqsk_clone() 克隆 request socket,旧 socket 和克隆共享同一个 ireq_opt,而 tcp_v4_reqsk_destructor() 同样用裸 kfree()。
修复方式是在 inet_sock_destruct() 和 tcp_v4_reqsk_destructor() 中改用 kfree_rcu(),等一个 RCU grace period 之后再释放。
- 引入版本:4.4(commit
079096f103fa) - 受影响文件:
net/ipv4/af_inet.c、net/ipv4/tcp_ipv4.c - 修复版本:5.10.272、5.15.223、6.1.190、6.6.159、6.12.113、6.18.56、7.2.10、7.3-rc7
- Debian 侧:DSA-6568-1,
linux包6.12.113-1
LWN 在 2026-10-30 的报道里补充了触发条件的时序:对重传 SYN 的响应,正好撞上另一线程完成 TCP 连接建立;第一个线程随后访问已被第二个线程释放的 options 结构,可导致内核内存泄露,或用攻击者控制的 payload 触发缓冲区溢出。
Go 侧:1.24 起 listener 默认开 MPTCP
Go security team 在 2026-10-29 的 oss-security 邮件里指出,从 Go 1.24 开始,Linux 上的 listen socket 默认启用 MPTCP。于是大量用 Go 写的服务在不知情的情况下带着 MPTCP listener 暴露在外。
net/dial.go 中的默认值:
defaultMPTCPEnabledListen = true
defaultMPTCPEnabledDial = false
// 0 == MPTCP disabled, 1 == MPTCP enabled, 2 == MPTCP enabled on listeners only, 3 == MPTCP enabled on dialers only
var multipathtcp = godebug.New("multipathtcp")
对应的 commit 是 3d4daa38("net: enable multipath TCP by default for listeners",2024 年 8 月,Go 1.24)。只有 listener 默认开启,dialer 仍关闭。服务端影响相对可控的原因是:客户端不请求 MPTCP 时,内核在 accept 阶段创建的是普通 TCP socket。
GODEBUG 的取值语义:
multipathtcp=0:禁用 MPTCPmultipathtcp=1:启用 MPTCPmultipathtcp=2:仅在 listener 上启用multipathtcp=3:仅在 dialer 上启用
两种关闭方式:
- 运行时:程序环境变量里设
GODEBUG=multipathtcp=0 - 构建时:在主模块
go.mod里写godebug multipathtcp=0
也可以按 socket 粒度控制,用 Dialer.MultipathTCP() / Dialer.SetMultipathTCP(use bool) 和 ListenConfig.MultipathTCP() / ListenConfig.SetMultipathTCP(use bool)。这两个接口会覆盖系统默认值和 GODEBUG 设置。主机不支持 MPTCP,或对端不支持时,会回退到普通 TCP。
GODEBUG 为什么只是挡板
Go 团队自己在邮件里写明:按程序逐个禁用 MPTCP 是一种 fragile mitigation。只要有一个 MPTCP listener 被漏掉,系统就仍然可被利用;而且这种做法假设所有可行的 exploit 都依赖 MPTCP,但底层 bug 并非 MPTCP 特有,没有 MPTCP listener 的系统也可能遭到类似攻击。
所以顺序应该是:先打内核补丁,再用 GODEBUG 作为过渡期兜底。Go 团队同时表示,目前不宜因为这个 issue 改动 Go 的默认行为(例如改掉 MPTCP 默认值),后续讨论放在 go.dev/issue/82717。
另一个需要注意的历史问题是 Go issue #70500:GODEBUG=multipathtcp= 对直接调用 ListenTCP / DialTCP 的应用不生效,只覆盖 Listen / Dial 路径。该问题由 CL 630715 修复,并 backport 到 1.22 / 1.23。
自查与处置
确认内核是否已修复,先看版本号是否落在上面的修复列表里,再看 socket 状态:
uname -r
# 列出 MPTCP socket
ss -M
# 确认监听端口
ss -tlnp
# MPTCP 端点与限制(需 iproute2 支持)
ip mptcp endpoint show
ip mptcp limits show
如果暂时无法升级内核,可以全局关掉 MPTCP(LWN 评论区提到的做法):
sysctl -w net.mptcp.enabled=0
这属于系统级开关,会同时影响所有使用 MPTCP 的程序,包括非 Go 程序。部署前要确认没有业务依赖多路径传输。
按照 Go 的方式临时收紧:
GODEBUG=multipathtcp=0 ./your-service
或在 go.mod 中固定:
godebug multipathtcp=0
最后再强调一遍:这两条都只是过渡措施。真正把 CVE-2026-98432 关掉的,是升级到 5.10.272 / 5.15.223 / 6.1.190 / 6.6.159 / 6.12.113 / 6.18.56 / 7.2.10 / 7.3-rc7 及以上内核。