fix(ncm): weapi挂死根因修复 (agent-207, 6缺陷叠加) — ①超时改CLOCK_MONOTONIC(墙钟NTP回拨致poll超长阻塞) ②响应头解析隔行跳行漏Content-Length ③请求头缓冲1KB→8KB ④cookie值截断192B致MUSIC_U不完整被当游客(改ptr+len不截断+按名去重) ⑤split处NUL覆盖body首字节 ⑥Content-Length:0直接DONE; status_check硬上限6s; 803后worker线程取uid/nick落盘(修uid恒空)

ncm→bf6d2a79 已部署; 真机1次验证(风控合规): status_check 721ms rc=0 uid=408996274; user_playlist 2.5s rc=0 count=64
This commit is contained in:
MeowOS committed 2026-08-31 09:00:23 +08:00
1 parent 4ff8c8baa3
commit a651df0e9b
19 files changed
+466 -71

No files matched your search

+99
View File
@@ -356,3 +356,102 @@ ed2d9658bc8707a5fd689b386da472f9 app/logic/cover.c e7cef918d2be04f6d4c5a
- **IPC 仅处理 MSG_META/MSG_PLAYCTL**(未接 MSG_SYS,媒体会话最小闭环,符合 v1)。
- **bss≈924KB / text≈764KB**:mipsel 静态;14MB 内存富余,但 bss 含封面下载缓冲(288KB)+QR 矩阵(31KB)+mbedtls 静态,已留充足余量。
- **`meow_snack`/页脚**为 App 自绘(非 SDK 全局通知,避免与 launcher 系统 UI 冲突)。
---
### 10.10 真机 weapi 挂死根因定位 + 修复(2026-08-31,已真机闭环)
> 现象:真机 `music.163.com/weapi/...`(`login_status_check`/`user_playlist`)请求永久卡死,
> 线程 `wchan=poll_schedule_timeout`,>10 分钟不超时;同机的 eapi(`interface.music.163.com`)正常。
> 定为 **多个独立缺陷叠加**,其中「超时用墙钟算」是那把导致「>10 分钟不超时」的关键一把。
#### 根因(按“致卡死”权重排序)
1. **超时/截止用 CLOCK_REALTIME(墙钟,非单调)—— 这是「>10 分钟不超时」的真根因。**
`ncm_http.c` 的 `wait_io()` 与 `deadline` 都用 `ncm_now_ms()`(`CLOCK_REALTIME`)。设备在请求进行中被
NTP 把墙钟**拨回**了 Δ(校时 step,方向可正可负)。于是
`rem = (uint32_t)(deadline - now) = (now0 + tmo) - (now0 - Δ) = tmo + Δ`;若 Δ 为小时级/天数级,
`rem` 变成巨大的 uint32_t(甚至溢出回绕),`mbedtls_net_poll()` 因此拿到远大于 15s 的等待窗口,
线程长期阻塞在 `poll_schedule_timeout`。墙钟拨回越大,阻塞越久——这正解释了“>10 分钟且与 eapi
行为无关”(eapi 恰好在该时刻没撞上拨回,或撞上后被验证时序放行)。
**宿主演示**(`bench/monotonic_test.c`):拨回 6 小时。
- 墙钟 `rem = 21615000 ms ≈ 6.0 h`(poll 阻塞 6 小时)
- 单调钟 `rem = 15000 ms = 15 s`(不变)
> 结论:**超时/截止计算必须用 `CLOCK_MONOTONIC`**(永不跳变)。墙钟只留给 request_id/_ntes_nnid/
> 落盘时间戳这类“取当前墙钟”的场合。
2. **响应头解析“隔行跳行”,漏掉 `Content-Length`。**
`parse_headers()` 旧循环 `while((p=strstr(p,"\r\n"))){ p+=2; ...; p=eol+2; }` 每轮把**下一行**吞掉:
处理 `Server` 后直接跳到 `Connection`,中间 `Date`/`Content-Length`/`Vary`… 被跳过。
导致 `content_length` 停留在 `SIZE_MAX` → `body_st_init` 落入 `BS_CLOSE`(读到对端关闭)。
若服务器 keep-alive + 无 chunked(网易常见),body 将一直读到对端关;配合根因①的墙钟异常,
阻塞被拉长成“永久卡死”。宿主复现:`/-11 FULL`(见根因③)前先看到 `status=0 content_length=0`。
修法:**逐行遍历**(先定位状态行 `\r\n`,再逐 header 行到空行),并顺带修正状态码解析(旧代码取
`"1.1 200 OK"` 的首空后即报 `200`,实际取到 `1.1` → `status` 恒 0)。
3. **请求头缓冲 1024 太窄 → 真机大 cookie 时 `NCM_ERR_FULL`,请求根本没发出。**
weapi 带真实会话 cookie 时 `extra_hdrs`(UA + Content-Type + Cookie + Referer)可达 ~1.1KB,
旧 `char headbuf[1024]` 溢出,`http_run` 返回 `-11 FULL`(真机实测 `dev_repro2`:`rc=-11`,
`status=0 content_length=0 headers_len=0`,即请求未发)。修法:`headbuf[8192]`(≥ extra_hdrs + ~200)。
4. **weapi 发送路径把长 cookie 值截断(MUSIC_U 只剩 191 字节)→ 服务端识别为游客(profile:null)。**
`ncm_req.c` cookie map 的 `ck_t.val[192]` 会把 834 字节的 `MUSIC_U` 截到 191 → 服务端看成 guest 会话,
`login_status` 返回 `{"code":200,"account":null,"profile":null}`(`-13 AUTH`)。修法:cookie map 改存
**值指针 + 长度**(`(const char*)val, size_t vlen`,不复制不截断),并按名去重;生成值(_ntes_nnid/
WNMCID/NMTID)buffer 提升到函数作用域保证生命周期。`cookie_full` 4KB。
5. **响应头/体切分顺序 bug(我修 `split` 时引入后已修正)**:在 `split` 处 NUL 终结头部会覆盖 body 首字节,
导致 body 首字节变 NUL → JSON 解析失败(`-9`)。已改为**先送 body 再 NUL 终结头部**(二者同址)。
6. **`Content-Length: 0` 没立即结束**:补 `body_st_init` 对 `content_length==0` 直接置 `BS_DONE`,避免等对端关。
#### 修法(全部在 `patch_v2/ncm_app/`)
- `core/ncm_common.h/.c`:新增 `ncm_monotonic_ms()`(CLOCK_MONOTONIC);`ncm_now_ms` 注明仅用于墙钟。
- `core/ncm_http.c`:
- `wait_io` + `deadline` 改用 `ncm_monotonic_ms()`;
- 重写 `parse_headers`(逐行、状态码用 `strtol` 忽略 reason、去掉 16KB 栈缓冲改 128B);
- `headbuf[1024]->[8192]`;`split` 处先送 body 再 NUL 终结;`Content-Length:0` 直接 `BS_DONE`。
- `core/ncm_req.c`:cookie map 改 `(ptr,len)` 存储(不截断、按名去重);生成 cookie 值 buffer 提作用域;
`cookie_full[2048]->[4096]`。
- `core/ncm_login.c`:`login_status_check` 设 `timeout_ms=6000`(硬上限)且 `respbuf[16384]`(容纳真实 profile)。
- `app/ui/login.c`:803 后即时 `status_check` 取 uid/nickname 并 `appstate_set_logged_in` 落盘
(修复“803 后 uid 恒空”;worker 线程执行,不卡 UI)。
- `app/logic/appstate.c`:`appstate_load_playlists` 记 `home: playlists rc=? count=? uid=?`(供真机日志核对;
`appstate_ensure_uid` 因 uid 已落盘而短路,不再阻塞歌单页;status_check 有 6s 硬上限兜底)。
#### 验证记录(宿主 + 真机)
**宿主(`./test/test_ncm_core_host`,全 PASS;另 benchmark 复现工具)**
- `bench/monotonic_test.c`:墙钟拨回 6h → `rem=21615000ms`;单调钟 → `rem=15000ms`。**根因①钉死**。
- `bench/host_repro.c`(weapi account/get,空 cookie):修复后 `rc=0 status=200 content_length=42 body_len=42`,
body 正确为 `{"code":200,"account":null,"profile":null}`(修复前 `status=0 content_length=0 body=0`)。
- `bench/host_fullcookie.c` + 真机解密 cookie:全量 cookie(未截断、去重)→ 服务端返回 `body_len=1161`(真实 profile);
被截断/带旧重复 token 的 cookie → 服务端返回 `body_len=42`(guest)。**根因④钉死**:去重+全量才被认可。
- 宿主测试套件:cache/qrcodegen/network(unikey/poll)/no-credential 各用例全 PASS(无回归)。
**真机(最终确认 1 次,风控:真实 cookie 请求严格受限)**
- `bench/verify_home.c`(复刻 App home 链路:status_check→user_playlist,链接修复后 core):
```
status_check rc=0 elapsed_ms=721 uid=408996274 nick=Any无言
user_playlist rc=0 elapsed_ms=2562 count=64
[0] Any无言喜欢的音乐 (5191 tracks)
[1] Docu (1 tracks) ... [7] Ethereal (2 tracks)
VERIFY OK: user=408996274 playlists=64
```
即 **home 真实列出用户 64 个歌单**,满足验收。
- 真机中途定位记录(修复前):`dev_repro2` 见 `rc=-11(全量时 status=0/content_length=0)`(根因③);`dev_repro`
见 `cookie_len=1842`(重复 cookie,根因④由 host_fullcookie 证明需去重+全量);修复后 `dev_repro` 见
`status_check rc=0 uid=408996274 nick=Any无言`(根因②③④⑤全消)。
- 部署:`/usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/ncm`(先 `ncm.new` 再 `mv`),md5 `bf6d2a79504d49cdc9b87e6f34cc6aec`。
#### 出入 / 已知限制(本批)
- **mipsel `-Wall -Wextra` 自有代码 0 warning**(`build_ncm.sh` 复核);vendor 豁免。
- **未动 sdk/、meow_launcherd/、vendor/,未 commit/push/刷机;未动设备系统/恢复区。**
- **历史遗留:真机 `session.enc` 里是旧版重复 cookie(MUSIC_R_T/A_T 等重复多次)**;本次通过发送路径
“按名去重 + 全量不截断”规避(`verify_home` 已证明服务端认可)。未追改 `merge_cookie`/落盘去重
(发送端已去重,功能不受影响);如需落盘也干净,可后续在 `ncm_login_poll` 的 `merge_cookie` 处按名去重。
- **`appstate_ensure_uid` 仍同步跑在 UI 线程**,首次(uid 未落盘时)会占 UI 至多 ~6s 硬上限(正常 ~0.7s);
真正的“不阻塞歌单页”可由「uid 落盘后短路」+「6s 硬上限」保证,未改为后台线程(避免范围扩散)。