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

364 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 关闭 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 正常连接;
- 本地唤醒与按键对话均正常;
- 连续高频语音上行不再出现相同回溯;
- 网络中断时进入受控错误与重连流程,而不是设备崩溃重启。