Files
lucky-cat/docs/knowledge vault/bugfix-disable-tls-renegotiation-crash.zh-CN.md

11 KiB
Raw Permalink Blame History

关闭 TLS Renegotiation:语音上行崩溃原因、证据与修改说明

1. 文档目的

本文说明设备在本地唤醒后进行语音上行时发生的一次 LoadProhibited 崩溃,包括:

  • 崩溃发生在哪条调用链;
  • 日志能够直接证明什么;
  • 为什么判断为 TLS 动态 RX buffer 与写操作之间的时序问题;
  • 为什么选择关闭 TLS renegotiation;
  • 修改方法;
  • 对 HTTPS、WSS、鉴权、重连、内存和兼容性的影响;
  • 后续真机验证方法。

对应现场日志:

monitor-20260812-170239

对应固件版本:

1c32d9f feat(runtime): add diagnostic and network recovery controls
ESP-IDF v5.5.4

2. 崩溃现象

设备已经完成以下步骤:

  1. 本地唤醒词“喵掌柜”检测成功;
  2. WebSocket 已连接;
  3. 服务端已经进入 Listening;
  4. 本地唤醒提示音播放完成;
  5. ASR 开始;
  6. 麦克风上行启动;
  7. 第一包 Opus 音频成功发送;
  8. 约 2.5 秒后 Core 1 发生 LoadProhibited 并重启。

崩溃前关键日志:

I (29077) cp_mm_session: ASR started
I (29077) cp_rt_workers: PTT state WAIT_SPEECH_STARTED -> STREAMING
I (29137) multimodal_audio: Mic uplink activated: mode=network
I (29167) multimodal_audio: Mic uplink first packet sent: activation_to_send_ms=60 bytes=180

Guru Meditation Error: Core 1 panic'ed (LoadProhibited).

这说明崩溃发生在设备已经进入正常语音上行以后,不是在启动、鉴权、WebSocket 握手或本地提示音解码阶段。

3. 直接证据

3.1 异常类型是空地址读取

寄存器信息:

PC      : 0x40056ece  memcmp
A2      : 0x00000000
EXCVADDR: 0x00000000
EXCCAUSE: 0x0000001c

ESP32-S3 Xtensa 调用约定中,A2 是函数的第一个参数。崩溃 PC 位于 memcmp,且 A2 与 EXCVADDR 都是零,因此可以直接确定:

某次 memcmp() 的第一个输入地址为 NULL,CPU 尝试从地址 0 读取数据,从而触发 LoadProhibited。

3.2 完整回溯位于音频上行 TLS write

日志解析出的回溯:

uplink_send_task
  -> control_plane_audio_uplink_send_cb
  -> control_plane_multimodal_send_audio_realtime
  -> domain_runtime_multimodal_transport_send_binary_realtime
  -> platform_multimodal_transport_send_binary_realtime
  -> multimodal_ws_send_binary_realtime
  -> websocket_send_binary_internal
  -> esp_websocket_client_send_bin
  -> esp_transport_ws_send_raw
  -> esp_transport_write
  -> esp_tls_conn_write
  -> mbedtls_ssl_write
  -> ssl_check_ctr_renegotiate
  -> memcmp(NULL, ...)

回溯没有经过 Opus Decoder、LCD、WakeNet、MultiNet 或下行播放器。因此这些模块不是本次崩溃的直接位置。

3.3 崩溃行访问的是 TLS 输入计数器

ESP-IDF v5.5.4 所用 mbedTLS 的 ssl_check_ctr_renegotiate() 中存在:

in_ctr_cmp = memcmp(ssl->in_ctr + ep_len,
                    &ssl->conf->renego_period[ep_len],
                    MBEDTLS_SSL_SEQUENCE_NUMBER_LEN - ep_len);

结合 A2=0,发生异常时 ssl->in_ctr 没有指向有效的 TLS RX record counter。

3.4 不是堆内存申请失败

崩溃前最近一次内存上下文:

internal free=21059
internal largest=7680
dma free=13271
dma largest=6400
alloc_failures=0

内部内存已经比较紧张,但仍有可用空间,且 allocation failure hook 没有记录失败。本次异常也不是 ESP_ERR_NO_MEM、heap assertion 或 heap corruption。

因此不能把这次崩溃归类为“内存耗尽”。

4. 触发条件

当前固件同时启用了:

CONFIG_MBEDTLS_DYNAMIC_BUFFER=y
CONFIG_MBEDTLS_SSL_RENEGOTIATION=y
CONFIG_ESP_WS_CLIENT_SEPARATE_TX_LOCK=y

三项配置分别代表:

配置 作用
CONFIG_MBEDTLS_DYNAMIC_BUFFER TLS 收发缓冲按需分配和释放,降低常驻内存。
CONFIG_MBEDTLS_SSL_RENEGOTIATION 编译并启用 TLS 1.2 连接内重新协商的支持。
CONFIG_ESP_WS_CLIENT_SEPARATE_TX_LOCK WebSocket 发送使用独立 TX 锁,允许接收循环和音频发送并行。

动态 TLS buffer 的 RX 路径会释放、缩小或重新建立输入 buffer,同时更新 ssl->in_buf、ssl->in_ctr 等内部指针。

WebSocket 独立 TX 锁则允许出现:

WebSocket RX 任务
    └─ TLS read
        └─ 动态释放或重建 RX buffer / in_ctr

音频上行任务
    └─ TLS write
        └─ renegotiation counter check 读取 in_ctr

如果两个动作刚好落入同一个很小的时序窗口,write 侧的 renegotiation 检查可能看到暂时为空的 ssl->in_ctr,随后在 memcmp 中访问空地址。

5. 根因结论及置信度

日志可以直接证明:

  • 崩溃位于音频上行 TLS write;
  • 崩溃函数是 renegotiation counter check;
  • 被传给 memcmp 的 TLS 输入计数器地址为 NULL;
  • 当时没有堆申请失败;
  • 当时没有 WebSocket stop、destroy 或 Wi-Fi 重关联。

结合动态 buffer 会改动 RX 内部指针、独立 TX 锁允许 RX/TX 并行,可以高置信度判断:

本次崩溃是 mbedTLS renegotiation 写侧检查访问了无效的动态 RX counter 指针;最可能的触发方式是 WebSocket 并行收发期间的短暂时序竞态。

由于现场只保存了 panic 日志,没有保存完整 coredump 和两个任务在崩溃瞬间的同步状态,所以“具体是哪一次 RX buffer 释放与哪一包 TX 完全重叠”属于基于源码和回溯的推断,而不是日志能够逐指令直接证明的部分。

6. 为什么采用关闭 TLS renegotiation

设备当前使用 TLS 的场景主要是:

  • HTTPS 请求设备 token;
  • WSS 连接 DashScope 多模态服务;
  • 其他普通 HTTPS API 请求。

这些连接依赖的是首次 TLS handshake、证书校验和连接断开后的重新建连,不依赖在同一条 TLS 1.2 连接中进行 renegotiation。

关闭 renegotiation 后,mbedtls_ssl_write() 不再编译或执行:

ssl_check_ctr_renegotiate()
    -> memcmp(ssl->in_ctr, ...)

因此直接移除了本次崩溃所在的访问路径。

相比其他方案,这个修改更小:

方案 效果与代价
关闭 TLS renegotiation 直接移除崩溃路径;不增加常驻内存;不改变正常首次握手。当前采用。
关闭动态 TLS buffer 也能减少 RX 指针动态变化,但会明显增加内部 RAM/PSRAM 常驻占用。暂不采用。
关闭 WebSocket 独立 TX 锁 强制收发争用同一把锁,可能增加实时音频发送延迟,并改变 WebSocket 调度行为。暂不采用。
修改 ESP-IDF/mbedTLS 内部源码 能增加空指针保护或同步,但会形成需要长期维护的 SDK 补丁。暂不采用。

7. 修改方式

7.1 项目默认配置

在 sdkconfig.defaults 中加入:

# CONFIG_MBEDTLS_SSL_RENEGOTIATION is not set

这样新建 build 目录、清理配置或其他开发者重新生成 sdkconfig 时,默认都关闭 renegotiation。

7.2 当前本地构建配置

当前 sdkconfig 同步改为:

# CONFIG_MBEDTLS_SSL_RENEGOTIATION is not set

因此当前工作区下一次直接运行 idf.py build 就会使用关闭后的配置,不需要先删除 build 目录。

也可以在菜单中搜索配置符号:

idf.py menuconfig
按 / 搜索 MBEDTLS_SSL_RENEGOTIATION

搜索结果会显示该选项所在菜单位置。

8. 修改后的完整运行链

修改前:

麦克风 PCM
  -> Opus 编码
  -> uplink_send_task
  -> WebSocket send_bin
  -> TLS write
  -> 检查 renegotiation record counter
  -> 读取动态 RX 的 in_ctr
  -> 极端时序下 in_ctr=NULL
  -> memcmp(NULL)
  -> LoadProhibited
  -> 设备重启

修改后:

麦克风 PCM
  -> Opus 编码
  -> uplink_send_task
  -> WebSocket send_bin
  -> TLS write
  -> 不编译/不执行 renegotiation counter check
  -> 正常写入 TLS record
  -> 服务端接收音频

连接真的失效时,仍走项目已有的恢复链:

TLS/WebSocket 发送失败
  -> transport error
  -> 停止音频上行
  -> 回收旧 WebSocket
  -> control-plane 重新检查鉴权
  -> 创建新 WebSocket

关闭 renegotiation 不会关闭这套断线重连能力。

9. 可能造成的影响

9.1 正常 HTTPS 和 WSS 首次握手不受影响

以下能力仍然保留:

  • TLS 1.2/1.3 首次握手;
  • 服务端证书校验;
  • ALPN;
  • HTTPS 请求;
  • WSS 建连;
  • TLS session ticket;
  • WebSocket 心跳;
  • WebSocket 断开后的新连接;
  • token 获取和刷新。

9.2 不再支持 TLS 1.2 同连接 renegotiation

如果某个服务端要求在一条已经建立的 TLS 1.2 连接内重新协商密钥或客户端证书,设备将无法完成该动作。服务端可能关闭连接,设备随后按现有逻辑重新建立一条完整的新连接。

当前使用的 token HTTPS 接口和 DashScope WSS 流程没有表现出必须依赖 renegotiation 的行为,因此该兼容性风险较低。

TLS 1.3 本身不使用 TLS 1.2 风格的 renegotiation,因此关闭该选项不会移除 TLS 1.3 的正常密钥更新机制。

9.3 可能略微减少固件和运行时开销

关闭后,renegotiation 相关代码和状态不再参与构建或运行,理论上会略微减少固件体积和 TLS 状态开销。实际差值需要以构建产物为准,预计不会很大。

9.4 不解决其他内存或网络问题

本修改只针对这次 ssl_check_ctr_renegotiate -> memcmp(NULL) 崩溃,不会自动解决:

  • DMA/internal heap 水位下降;
  • DNS EAI_FAIL;
  • HTTPS ESP_ERR_HTTP_CONNECT;
  • 服务端 TTS stall;
  • Wi-Fi 关联异常;
  • Opus 编解码资源问题。

10. 与近期修复的关系

与 fresh-auth WebSocket 重连的关系

没有冲突。关闭 renegotiation 处理的是一条已建立 TLS 连接内部的 renegotiation 检查;fresh-auth 修复处理的是 WebSocket 已经失败后,如何回收旧连接并创建新连接。

与下行 stall 恢复的关系

没有直接关系。下行 stall 恢复处理云端 TTS 长时间没有播放进展;本次崩溃发生在麦克风音频上行的 TLS write。

与 Wi-Fi 主动重关联的关系

没有直接关系。崩溃日志中没有主动 Wi-Fi reassociation,且该功能默认关闭。

11. 建议验证方法

11.1 基础回归

  1. 烧录关闭 renegotiation 的固件;
  2. 确认设备能正常获取 token;
  3. 确认 WSS 能连接;
  4. 分别测试本地唤醒和按键对话;
  5. 确认 ASR、TTS 和多轮对话正常。

11.2 并行收发压力测试

  1. 连续进行至少 100 轮语音对话;
  2. 每轮保持 5~15 秒语音上行;
  3. 在上行期间让服务端持续发送 Listening、ASR 或其他控制事件;
  4. 观察是否再次出现 ssl_check_ctr_renegotiate、memcmp 或 LoadProhibited;
  5. 同时记录 alloc_failures 和内存水位,区分本修复与其他资源问题。

11.3 网络异常回归

  1. 在语音上行期间短暂断网;
  2. 确认设备报告 WebSocket/TLS 发送失败,而不是 panic;
  3. 恢复网络;
  4. 确认旧 transport 被回收;
  5. 确认重新鉴权建连后可以继续对话。

12. 验收标准

满足以下条件后,可以认为本修复通过真机验证:

  • 构建配置中 CONFIG_MBEDTLS_SSL_RENEGOTIATION 为关闭状态;
  • token HTTPS 请求正常;
  • DashScope WSS 正常连接;
  • 本地唤醒与按键对话均正常;
  • 连续高频语音上行不再出现相同回溯;
  • 网络中断时进入受控错误与重连流程,而不是设备崩溃重启。