# 关闭 TLS Renegotiation:语音上行崩溃原因、证据与修改说明 ## 1. 文档目的 本文说明设备在本地唤醒后进行语音上行时发生的一次 `LoadProhibited` 崩溃,包括: - 崩溃发生在哪条调用链; - 日志能够直接证明什么; - 为什么判断为 TLS 动态 RX buffer 与写操作之间的时序问题; - 为什么选择关闭 TLS renegotiation; - 修改方法; - 对 HTTPS、WSS、鉴权、重连、内存和兼容性的影响; - 后续真机验证方法。 对应现场日志: ```text monitor-20260812-170239 ``` 对应固件版本: ```text 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` 并重启。 崩溃前关键日志: ```text 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 异常类型是空地址读取 寄存器信息: ```text 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 日志解析出的回溯: ```text 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()` 中存在: ```c 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 不是堆内存申请失败 崩溃前最近一次内存上下文: ```text 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. 触发条件 当前固件同时启用了: ```text 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 锁则允许出现: ```text 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()` 不再编译或执行: ```text 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` 中加入: ```text # CONFIG_MBEDTLS_SSL_RENEGOTIATION is not set ``` 这样新建 build 目录、清理配置或其他开发者重新生成 `sdkconfig` 时,默认都关闭 renegotiation。 ### 7.2 当前本地构建配置 当前 `sdkconfig` 同步改为: ```text # CONFIG_MBEDTLS_SSL_RENEGOTIATION is not set ``` 因此当前工作区下一次直接运行 `idf.py build` 就会使用关闭后的配置,不需要先删除 build 目录。 也可以在菜单中搜索配置符号: ```text idf.py menuconfig 按 / 搜索 MBEDTLS_SSL_RENEGOTIATION ``` 搜索结果会显示该选项所在菜单位置。 ## 8. 修改后的完整运行链 修改前: ```text 麦克风 PCM -> Opus 编码 -> uplink_send_task -> WebSocket send_bin -> TLS write -> 检查 renegotiation record counter -> 读取动态 RX 的 in_ctr -> 极端时序下 in_ctr=NULL -> memcmp(NULL) -> LoadProhibited -> 设备重启 ``` 修改后: ```text 麦克风 PCM -> Opus 编码 -> uplink_send_task -> WebSocket send_bin -> TLS write -> 不编译/不执行 renegotiation counter check -> 正常写入 TLS record -> 服务端接收音频 ``` 连接真的失效时,仍走项目已有的恢复链: ```text 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 正常连接; - 本地唤醒与按键对话均正常; - 连续高频语音上行不再出现相同回溯; - 网络中断时进入受控错误与重连流程,而不是设备崩溃重启。