文件和行号
同型一行出现在 14 个 IM panel 的 *BindingEntries() 渲染路径(每帧执行):
- internal/tui/discord_panel.go:418
- internal/tui/dingtalk_panel.go:423
- internal/tui/matrix_panel.go:483
- internal/tui/whatsapp_panel.go:517
- internal/tui/irc_panel.go:456
- internal/tui/feishu_panel.go:446
- internal/tui/slack_panel.go:493
- internal/tui/signal_panel.go:654
- internal/tui/tg_panel.go:400
- internal/tui/mattermost_panel.go:472
- internal/tui/nostr_panel.go:562
- internal/tui/twitch_panel.go:447
- internal/tui/wechat_panel.go:615
- internal/tui/qq_panel.go:449
问题描述
keys := make([]string, 0, len(m.config.IM.Adapters)) 是这些文件中唯一绕过 imAdaptersMu 的 map 读。#2152 建立的契约是所有 IM adapter map 访问走锁访问器(IMSnapshot/GetIMAdapter/IMAdapterEnabled 均为 RLock+深拷贝)。互斥锁对不持锁读者零保护:与任一持锁写(SetIMAdapterEnabled / AddIMAdapter / toggle 等,config_save.go:679-698)交叠时 Go runtime fatal "concurrent map read and map write",不可恢复,整个 TUI 崩溃。
触发场景
- 打开任一 IM panel(每帧渲染执行该行)
- 同时任一 Cmd goroutine 完成一次持锁写(如 im_panel.go toggleIMAdapterEnabled 的 enable/disable 链、或残留的 Cmd goroutine 直调 SetIMAdapterEnabled 的路径)
- 写入期间恰好帧渲染到该行 → fatal
预期行为 vs 实际行为
修复建议
每个文件改为先取一次快照复用:snap := m.config.IMSnapshot().Adapters; keys := make([]string, 0, len(snap))(下一行的遍历本来就用 IMSnapshot(),可复用同一变量)。并将 #2337 的源码扫描测试泛化到全部 IM panel 文件,防止再次复发。
严重程度
low~medium(触发窗口窄:需要渲染帧与持锁写精确交叠;但后果是进程 fatal 不可恢复,且面板打开期间渲染每帧都执行该行)
文件和行号
同型一行出现在 14 个 IM panel 的 *BindingEntries() 渲染路径(每帧执行):
问题描述
keys := make([]string, 0, len(m.config.IM.Adapters))是这些文件中唯一绕过imAdaptersMu的 map 读。#2152 建立的契约是所有 IM adapter map 访问走锁访问器(IMSnapshot/GetIMAdapter/IMAdapterEnabled 均为 RLock+深拷贝)。互斥锁对不持锁读者零保护:与任一持锁写(SetIMAdapterEnabled / AddIMAdapter / toggle 等,config_save.go:679-698)交叠时 Go runtime fatal "concurrent map read and map write",不可恢复,整个 TUI 崩溃。触发场景
预期行为 vs 实际行为
修复建议
每个文件改为先取一次快照复用:
snap := m.config.IMSnapshot().Adapters; keys := make([]string, 0, len(snap))(下一行的遍历本来就用 IMSnapshot(),可复用同一变量)。并将 #2337 的源码扫描测试泛化到全部 IM panel 文件,防止再次复发。严重程度
low~medium(触发窗口窄:需要渲染帧与持锁写精确交叠;但后果是进程 fatal 不可恢复,且面板打开期间渲染每帧都执行该行)