编号:B-05 严重级:critical 工作线:broker(internal/broker) 来源:审查 I-03、P-03 依赖:无 被依赖:B-09 (#16)(同改 hooks.go 的 OnDisconnect,本条先合)
两份审查结论一致:OnConnect 在每个分支都调用 rememberPending 把连接放进 byClient,唯一删除点在 OnDisconnect;而 mochi 在 OnConnect 返回错误、OnConnectAuthenticate 返回 false、回 CONNACK 失败时都不调用 OnDisconnect。用不存在的编号反复连接,每次泄漏一个 *mqtt.Client(含 1024 槽发送通道与读缓冲),内存持续上涨直到 OOM;lookupConn、connStateOf 的线性扫描也随之变慢。
OnConnect
rememberPending
byClient
OnDisconnect
OnConnectAuthenticate
*mqtt.Client
lookupConn
connStateOf
统一方案:
connState
established
OnSessionEstablished
!established && cl.Closed()
byConnID map[port.ConnID]*connState
严重级取 critical(两份审查分别判 critical 与 high):无需凭据、成本极低、结果是进程 OOM。
internal/broker/hooks.go、internal/broker/broker.go,broker 测试。
internal/broker/hooks.go
internal/broker/broker.go
len(byClient)==0
以下是本次复审各区审查报告的原文段落。A、M、I、P、S 开头的是原始发现编号(A 管理后台与网页、M 消息核心、I 身份认证群在线、P 传输平台部署、S SDK)。解决方案以本 issue 上方的"结论与统一方案"为准;原文里的方案与之不一致时,按上方执行。
internal/broker/hooks.go:50-55
70-79
82-86
88-97
hooks.go:196-205
broker/broker.go:294-306
412-421
server.go:460-463
server.go:486
res, err := h.b.auth.Authenticate(context.Background(), endpointID, pk.Connect.Password, remoteIP) if err != nil { st.authErr = err h.rememberPending(cl, st) return err // mochi 不回 CONNACK,直接断开 } st.authOK = res.OK st.sessionToken = res.SessionToken h.rememberPending(cl, st) return nil
err = s.hooks.OnConnect(cl, pk) if err != nil { return err } cl.refreshDeadline(cl.State.Keepalive) if !s.hooks.OnConnectAuthenticate(cl, pk) { // [MQTT-3.1.4-2] err := s.SendConnack(cl, packets.ErrBadUsernameOrPassword, false, nil) if err != nil { return fmt.Errorf("invalid connection send ack: %w", err) } return packets.ErrBadUsernameOrPassword }
!authOK
delete(byClient, cl)
cl.Closed()
byConn map[ConnID]*connState
broker/hooks.go
broker/broker.go
func (s *Server) attachClient(cl *Client, listener string) error { // ... pk, err := s.readConnectionPacket(cl) // ... err = s.hooks.OnConnect(cl, pk) if err != nil { return err } cl.refreshDeadline(cl.State.Keepalive) if !s.hooks.OnConnectAuthenticate(cl, pk) { // [MQTT-3.1.4-2] // ... return packets.ErrBadUsernameOrPassword }
其余位置:hooks.go:50-55、70-79、82-86、196-199(唯一删除点);broker.go:294-306、412-421(全表扫描);mochi server.go:460-463(CONNACK 失败直接返回)。
hooks.go:50-55
196-199
broker.go:294-306
res.OK
broker.go
len(b.byClient)==0
复审基线:main 4059a15(2026-09-30)。编号说明、各工作线的合并顺序、共享文件归属见总览 #7。
4059a15
已合入 origin/main 0c9b459。落地提交 0b9ce03 fix: 认证失败不泄漏连接表并脱敏 mochi 整包日志 (#12)。
0c9b459
0b9ce03
No dependencies set.
The note is not visible to the blocked user.
编号:B-05 严重级:critical 工作线:broker(internal/broker) 来源:审查 I-03、P-03
依赖:无 被依赖:B-09 (#16)(同改 hooks.go 的 OnDisconnect,本条先合)
结论与统一方案
两份审查结论一致:
OnConnect在每个分支都调用rememberPending把连接放进byClient,唯一删除点在OnDisconnect;而 mochi 在OnConnect返回错误、OnConnectAuthenticate返回 false、回 CONNACK 失败时都不调用OnDisconnect。用不存在的编号反复连接,每次泄漏一个*mqtt.Client(含 1024 槽发送通道与读缓冲),内存持续上涨直到 OOM;lookupConn、connStateOf的线性扫描也随之变慢。统一方案:
OnConnect只在认证通过时写入byClient;拒绝与内部错误都不登记。OnConnectAuthenticate查不到即返回 false。对客户端的行为不变:拒绝仍回 0x86,内部故障仍不回 CONNACK。connState增加established(在OnSessionEstablished置位)与创建时间;broker 每分钟清扫一次!established && cl.Closed()且存在超过 1 分钟的条目,覆盖"认证通过但 CONNACK 失败"等路径。已建立的连接一定会走OnDisconnect,不在清扫范围。byConnID map[port.ConnID]*connState索引,lookupConn、connStateOf改为 O(1)。严重级取 critical(两份审查分别判 critical 与 high):无需凭据、成本极低、结果是进程 OOM。
改动文件
internal/broker/hooks.go、internal/broker/broker.go,broker 测试。与其他问题的交互 / 冲突说明
OnConnect)同属 broker 线,按 broker 线顺序合入。OnDisconnect增加 closed 标记,需在本 issue 合入后 rebase。验收与测试
len(byClient)==0。问题明细(各区审查原文,证据含文件与行号)
[I-03] 认证失败的连接永久留在 broker 连接表,不用凭证就能耗尽内存
OnConnect在所有分支都执行rememberPending,把连接放进byClient,而唯一的删除点是OnDisconnect。lookupConn(带连接代号时)和connStateOf对byClient做线性扫描,每次下行发布和上行处理都要遍历,泄漏越多 CPU 越高。internal/broker/hooks.go:50-55、70-79、82-86、88-97。hooks.go:196-205。broker/broker.go:294-306、412-421。server.go:460-463;OnDisconnect 只在会话建立、读循环结束后调用:server.go:486。!authOK时,先delete(byClient, cl)再返回 false。established标记,在 OnSessionEstablished 时置位;在握手超时计时器或每分钟一次的清扫里,删除「未建立且cl.Closed()」的条目,覆盖 CONNACK 失败这类路径。byConn map[ConnID]*connState,让查找变成 O(1)。broker/hooks.go、broker/broker.go。byClient为空;成功连接后断开,也为空。[P-03] 认证失败的连接永久留在 byClient 表里:不需要凭据即可触发的内存泄漏,还会拖慢所有按 connID 的查找
OnConnect对每个 CONNECT 都会rememberPending,唯一删除的地方在OnDisconnect。OnConnect返回 error、或OnConnectAuthenticate返回 false 时直接返回,不会调OnDisconnect。*mqtt.Client永久挂在表里,里面有已关闭的连接、2 KiB 读缓冲、1024 槽的发送通道、最长 64 KiB 的用户名:lookupConn和connStateOf每次都要全表扫描,每条 PublishDown、每条上行、每个握手计时器都会随泄漏条目线性变慢。其余位置:
hooks.go:50-55、70-79、82-86、196-199(唯一删除点);broker.go:294-306、412-421(全表扫描);mochiserver.go:460-463(CONNACK 失败直接返回)。OnConnect只在res.OK时写入byClient;拒绝和内部错误都不登记。OnConnectAuthenticate查不到就返回 false,对客户端的行为不变。connState增加established标志(在OnSessionEstablished置位)和创建时间。broker 起一个每分钟一次的清扫,删除!established && cl.Closed()且存在超过 1 分钟的条目。已建立的连接一定会走 OnDisconnect,不能清。byConnID map[port.ConnID]*connState索引,把lookupConn和connStateOf改成 O(1)。internal/broker/hooks.go、broker.golen(b.byClient)==0。另测"认证成功后客户端立刻断开"的条目在清扫后消失。复审基线:main
4059a15(2026-09-30)。编号说明、各工作线的合并顺序、共享文件归属见总览 #7。已合入 origin/main
0c9b459。落地提交0b9ce03fix: 认证失败不泄漏连接表并脱敏 mochi 整包日志 (#12)。