这是迄今遇到的最迷惑的一次故障:快速切歌或切源,整机就"哑"了——不出声、面板失联,但所有进程都活着:ps 全在、心跳正常、没有任何 crash 和重启。软重启无效,断电是唯一恢复。最后定位到内核里的 sunxi rpmsg 竞态 Oops,而且 Oops 本身还死在打印路径里——这篇完整复盘。

一、症状:进程全活,整机不响应

触发条件很具体:快速切歌/切源,在 3 次通道循环以内必现。之后的现场:

  • MPD 无声:播放命令发进去没有反应;
  • 面板侧 USB URB 挂起:提交出去的请求永远不返回;
  • 业务服务全部存活,看门狗毫无动作——因为心跳都是好的;
  • reboot 命令都能执行,但系统回不到可用状态。

「进程全活但业务全死」——这个组合第一次见时会本能地怀疑应用层,绕一大圈。之后的直觉应该反过来:应用层全员正常而不工作,问题在它们共同依赖的底下那一层。

二、根因:竞态 UAF + Oops 死在 printk 里

音频链路里,ARM 与 DSP 之间用 rpmsg 通道传命令。快速切歌意味着通道被高频打开/关闭,撞上 sunxi rpmsg 驱动的释放竞态(UAF)→ 内核 Oops。

更糟的是第二层:Oops 发生在持锁上下文,panic 打印路径(printk)又撞上已被破坏的锁——Oops 自己死在打印里。结果是"半死"系统:调度器还活着(进程能跑、心跳能跳),出问题的子系统全卡死(rpmsg、以及等它的人),而且 dmesg 里连 Oops 现场都看不到——dmesg 干净不等于没出过 Oops。

用户态看到的三层连坐由此而来:MPD 等内核锁(哑)、面板 URB 挂起(USB 层不返回)、看门狗看的是进程存活与心跳(全正常)——每个组件的行为都"合理",合起来是假活。

三、修复:内核暂不动,用户态五层加固

内核根治(给 sunxi rpmsg 补 UAF 修复/升级)需要完整的内核回归验证,量产节奏上先走用户态防御——把"整机永久卡死"降级为"短暂中断后自愈":

  1. RX 看门狗:USB 传输层加接收看门狗——分发链路永久卡死时主动 abort,进程退出交给 procd 无限 respawn(配合另一篇的三层自愈 init),服务重启后面板自动重连(配合 Bulk 自愈环);
  2. I2C 音源芯片全部公开接口加互斥 + open 时设 I2C_TIMEOUT——多线程访问音频切换芯片的路径全部串行化,永不无限等;
  3. 拆除命令分发器里两处锁纪律隐患——历史代码里两处"持锁调用外部回调"的埋雷点清掉;
  4. 面板侧切源改"拧定":快速按 N 次音源键只发最终选中的一帧——从源头把竞态触发频率压下去,治不了内核就先别去捅它;
  5. OTG 芯片状态查询全局节流(3 秒一次):那颗芯片高频访问会自己卡死,顺带再降链路压力。

四、方法论沉淀

  1. “进程全活但整机不响应”→ 优先查内核层。用户态全员正常而业务全死,公共依赖(内核子系统)是唯一解释。
  2. dmesg 空不等于没事:Oops 可能死在打印路径里。判断依据改用行为指纹(哪类操作必死、哪些还活着)。
  3. 看门狗要盯业务心跳,不是进程存活。进程活着和业务在干活是两回事——这次所有基于"进程存活"的监控全部漏报。
  4. 修不起内核就釜底抽薪:降触发频率(拧定/节流)+ 缩爆炸半径(看门狗 abort + 无限重生 + 链路自愈),三层下来用户感知从"变砖"降到"闪断"。
  5. 内核根治项登记在案不装死——UAF 还在,只是门口加了三道闸。

尾注

这次故障的触发手法(快速切歌三连)后来成了回归测试的固定用例:任何动过音频链路的版本,上板先来十轮快速切歌。能稳定复现的故障是资产,别浪费。