364 lines
11 KiB
Markdown
364 lines
11 KiB
Markdown
# 关闭 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 正常连接;
|
||
- 本地唤醒与按键对话均正常;
|
||
- 连续高频语音上行不再出现相同回溯;
|
||
- 网络中断时进入受控错误与重连流程,而不是设备崩溃重启。
|