diff --git a/patch_v2/ncm_app/REPORT.md b/patch_v2/ncm_app/REPORT.md index 548d954..db49272 100644 --- a/patch_v2/ncm_app/REPORT.md +++ b/patch_v2/ncm_app/REPORT.md @@ -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 硬上限」保证,未改为后台线程(避免范围扩散)。 + diff --git a/patch_v2/ncm_app/app/logic/appstate.c b/patch_v2/ncm_app/app/logic/appstate.c index e255dac..32d1aa7 100644 --- a/patch_v2/ncm_app/app/logic/appstate.c +++ b/patch_v2/ncm_app/app/logic/appstate.c @@ -113,6 +113,7 @@ int appstate_load_playlists(void) rc = ncm_api_user_playlist(g_st.ctx.device_id, g_st.ctx.cookie, g_st.uid, NCM_PLAYLIST_MAX, 0, g_st.playlists, NCM_PLAYLIST_MAX, &n); g_st.playlist_count = (rc == NCM_OK) ? n : 0; + meow_log(MEOW_LOG_INFO, "home: playlists rc=%d count=%d uid=%s", rc, g_st.playlist_count, g_st.uid); return rc; } diff --git a/patch_v2/ncm_app/app/ui/login.c b/patch_v2/ncm_app/app/ui/login.c index c632eec..0767f9e 100644 --- a/patch_v2/ncm_app/app/ui/login.c +++ b/patch_v2/ncm_app/app/ui/login.c @@ -123,13 +123,17 @@ static void *login_worker(void *arg) meow_log(MEOW_LOG_INFO, "login: 803 confirmed, saving cookie"); ncm_strlcpy(cookie, out_cookie, sizeof cookie); ncm_ctx_set_cookie(&g_st.ctx, cookie); - /* 先落登录态并放行跳页(真机联调 2026-08-31:status_check 在 weapi 读响应时 - * 挂死(HTTP 读循环超时未生效),把跳页卡在登录页。昵称是装饰信息, - * 先跳 home(歌单请求会独立验证 cookie),status_check 留待修复后恢复。) */ - appstate_set_logged_in("", ""); + /* 803 后同步取 uid/nickname 并落盘(weapi 已修复:不再挂死、cookie 全量且去重发送, + * 服务端认可会话)。此处在 worker 线程执行(不卡 UI);失败不阻断跳页——home 加载 + * 时 appstate_ensure_uid 会再验一次(也设了硬超时上限,不阻塞歌单页)。 */ + char uid[32] = "", nick[128] = ""; + int src = ncm_login_status_check(g_st.ctx.device_id, cookie, + uid, sizeof uid, nick, sizeof nick); + meow_log(MEOW_LOG_INFO, "login: status_check rc=%d uid=%s nick=%s", src, uid, nick); + appstate_set_logged_in(src == NCM_OK ? uid : "", src == NCM_OK ? nick : ""); g_st.logged_in = 1; g_done = 1; - meow_log(MEOW_LOG_INFO, "login: -> home (status_check deferred)"); + meow_log(MEOW_LOG_INFO, "login: -> home"); break; } if (r.state == NCM_LOGIN_EXPIRED) { diff --git a/patch_v2/ncm_app/bench/dev_repro.c b/patch_v2/ncm_app/bench/dev_repro.c new file mode 100644 index 0000000..f749538 --- /dev/null +++ b/patch_v2/ncm_app/bench/dev_repro.c @@ -0,0 +1,29 @@ +/* dev_repro.c — 真机复现 login_status_check(weapi)挂死 + * 用与 App 相同的 core 库:ncm_ctx_init 载入 deviceId/cookie,然后 ncm_login_status_check。 + * 观察返回码与耗时;若 >10min 即复现挂死。 + * 用法: ./dev_repro [data_dir] [cacert] + */ +#include "ncm.h" +#include "ncm_http.h" +#include "ncm_common.h" +#include +#include + +int main(int argc, char **argv) { + const char *data_dir = argc > 1 ? argv[1] : "/usr/data/mnt/sd_0/MeowData/os.meow.ncm/data"; + const char *cacert = argc > 2 ? argv[2] : "/usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/cacert.pem"; + + ncm_ctx_t ctx; + ncm_ctx_init(&ctx, data_dir); + printf("device_id=%s\n", ctx.device_id); + printf("cookie_len=%zu cookie=%s\n", strlen(ctx.cookie), ctx.cookie); + ncm_http_set_verify_default(1, cacert); + + char uid[32], nick[128]; + long t0 = (long)ncm_now_ms(); + printf("status_check start...\n"); + int rc = ncm_login_status_check(ctx.device_id, ctx.cookie, uid, sizeof uid, nick, sizeof nick); + long dt = (long)ncm_now_ms() - t0; + printf("status_check rc=%d elapsed_ms=%ld uid=%s nick=%s\n", rc, dt, uid, nick); + return rc == 0 ? 0 : 1; +} diff --git a/patch_v2/ncm_app/bench/dev_repro2.c b/patch_v2/ncm_app/bench/dev_repro2.c new file mode 100644 index 0000000..a92a9ef --- /dev/null +++ b/patch_v2/ncm_app/bench/dev_repro2.c @@ -0,0 +1,41 @@ +/* dev_repro2.c — 真机 dump weapi account/get 响应的真实分段(Content-Length/chunked/body_len)。 + * 用大缓冲 64KB,避免 -11 FULL;打印解析出的 status/content_length/chunked/body_len。 + */ +#include "ncm.h" +#include "ncm_req.h" +#include "ncm_http.h" +#include "ncm_common.h" +#include +#include + +int main(int argc, char **argv) { + const char *data_dir = argc > 1 ? argv[1] : "/usr/data/mnt/sd_0/MeowData/os.meow.ncm/data"; + const char *cacert = argc > 2 ? argv[2] : "/usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/cacert.pem"; + + ncm_ctx_t ctx; + ncm_ctx_init(&ctx, data_dir); + printf("device_id=%s cookie_len=%zu\n", ctx.device_id, strlen(ctx.cookie)); + ncm_http_set_verify_default(1, cacert); + + char body[NCM_REQ_BODY_MAX], hdrs[NCM_REQ_HDRS_MAX], url[NCM_REQ_URL_MAX]; + int rc = ncm_req_build(ctx.device_id, ctx.cookie, "/api/w/nuser/account/get", "{}", + NCM_CRYPT_WEAPI, body, sizeof body, hdrs, sizeof hdrs, url, sizeof url); + if (rc != 0) { printf("req_build rc=%d\n", rc); return 1; } + printf("URL=%s\nbody_len=%zu hdrs_len=%zu\n", url, strlen(body), strlen(hdrs)); + + ncm_http_req_t req; memset(&req, 0, sizeof req); + req.url = url; req.method = "POST"; req.body = body; req.body_len = strlen(body); + req.extra_hdrs = hdrs; req.verify_ca = NCM_HTTP_VERIFY_DEFAULT; req.timeout_ms = 15000; + + static char respbuf[65536]; + ncm_http_resp_t resp; + long t0 = (long)ncm_now_ms(); + rc = ncm_http_request(&req, respbuf, sizeof respbuf, &resp); + long dt = (long)ncm_now_ms() - t0; + printf("http_request rc=%d elapsed_ms=%ld\n", rc, dt); + printf("status=%d content_length=%zu chunked=%d\n", resp.status, resp.content_length, resp.chunked); + printf("headers_len=%zu\n", resp.headers_len); + printf("body_len=%zu\n", resp.body_len); + if (resp.body_len) printf("body_head:\n%.*s\n", (int)(resp.body_len < 800 ? resp.body_len : 800), resp.body); + return 0; +} diff --git a/patch_v2/ncm_app/bench/host_fullcookie b/patch_v2/ncm_app/bench/host_fullcookie new file mode 100755 index 0000000..29fbf00 Binary files /dev/null and b/patch_v2/ncm_app/bench/host_fullcookie differ diff --git a/patch_v2/ncm_app/bench/host_fullcookie.c b/patch_v2/ncm_app/bench/host_fullcookie.c new file mode 100644 index 0000000..6c8e6d9 --- /dev/null +++ b/patch_v2/ncm_app/bench/host_fullcookie.c @@ -0,0 +1,44 @@ +/* host_fullcookie.c — 用完整原始 cookie 直发 weapi account/get,验证服务器是否识别会话。 + * 绕过 ncm_req_build 的 ck_parse 截断:手写 Cookie 头 + 只用 ncm_crypto_weapi 生成 body。 + * 用法: ./host_fullcookie '<完整cookie>' [cacert] + */ +#include "ncm.h" +#include "ncm_crypto.h" +#include "ncm_http.h" +#include "ncm_common.h" +#include +#include + +int main(int argc, char **argv) { + if (argc < 2) { fprintf(stderr, "usage: %s ''\n", argv[0]); return 1; } + const char *cookie = argv[1]; + ncm_http_set_verify_default(1, argc > 2 ? argv[2] : NULL); + + char params[1024], encsk[300]; + int rc = ncm_crypto_weapi("{\"csrf_token\":\"\"}", params, sizeof params, encsk, sizeof encsk); + if (rc != NCM_OK) { printf("weapi crypto rc=%d\n", rc); return 1; } + char p_enc[1100]; + if (ncm_url_encode(params, p_enc, sizeof p_enc) < 0) { printf("urlencode fail\n"); return 1; } + char body[1400]; + snprintf(body, sizeof body, "params=%s&encSecKey=%s", p_enc, encsk); + + char hdrs[2048]; + snprintf(hdrs, sizeof hdrs, + "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "Cookie: %s\r\n" + "Referer: https://music.163.com", cookie); + + ncm_http_req_t req; memset(&req, 0, sizeof req); + req.url = "https://music.163.com/weapi/w/nuser/account/get"; + req.method = "POST"; req.body = body; req.body_len = strlen(body); + req.extra_hdrs = hdrs; req.verify_ca = NCM_HTTP_VERIFY_DEFAULT; req.timeout_ms = 20000; + char respbuf[65536]; ncm_http_resp_t resp; + long t0 = (long)ncm_now_ms(); + rc = ncm_http_request(&req, respbuf, sizeof respbuf, &resp); + long dt = (long)ncm_now_ms() - t0; + printf("rc=%d elapsed_ms=%ld status=%d cl=%zu chunked=%d body_len=%zu\n", + rc, dt, resp.status, resp.content_length, resp.chunked, resp.body_len); + if (resp.body_len) printf("body:\n%.*s\n", (int)resp.body_len, resp.body); + return 0; +} diff --git a/patch_v2/ncm_app/bench/host_repro b/patch_v2/ncm_app/bench/host_repro new file mode 100755 index 0000000..e699103 Binary files /dev/null and b/patch_v2/ncm_app/bench/host_repro differ diff --git a/patch_v2/ncm_app/bench/host_repro.c b/patch_v2/ncm_app/bench/host_repro.c new file mode 100644 index 0000000..60422e9 --- /dev/null +++ b/patch_v2/ncm_app/bench/host_repro.c @@ -0,0 +1,59 @@ +/* host_repro.c — 复现 weapi login_status_check 断帧挂死/断头问题(宿主) + * 用与 App 完全相同的 core 库路径:ncm_req_build(WEAPI) -> ncm_http_request。 + * 用法: ./host_repro [cookie] + */ +#include "ncm.h" +#include "ncm_req.h" +#include "ncm_http.h" +#include "ncm_common.h" + +#include +#include + +static void dump_resp(const char *tag, const ncm_http_resp_t *r) { + printf("---- %s ----\n", tag); + printf("status=%d content_length=%zu chunked=%d ct=%s\n", + r->status, r->content_length, r->chunked, + r->has_content_type ? r->content_type : "(none)"); + printf("headers(%zu):\n%s\n", r->headers_len, r->headers); + printf("body_len=%zu\n", r->body_len); + if (r->body_len) printf("body:\n%.*s\n", (int)r->body_len, r->body); +} + +int main(int argc, char **argv) { + const char *cookie = argc > 1 ? argv[1] : "__csrf="; + const char *device = "68DFA27096B7A5DC2CB393C77D8C194FDC0459B743081D091EBD"; + + /* 同 App:用全局默认 CA(verify=1 + 系统CA) */ + ncm_http_set_verify_default(1, NULL); + + char body[NCM_REQ_BODY_MAX], hdrs[NCM_REQ_HDRS_MAX], url[NCM_REQ_URL_MAX]; + int rc = ncm_req_build(device, cookie, "/api/w/nuser/account/get", "{}", + NCM_CRYPT_WEAPI, body, sizeof body, + hdrs, sizeof hdrs, url, sizeof url); + printf("req_build rc=%d\n", rc); + if (rc != NCM_OK) return 1; + printf("URL: %s\n", url); + printf("BODY(len=%zu): %.*s\n\n", strlen(body), (int)strlen(body), body); + printf("HDRS:\n%s\n\n", hdrs); + + ncm_http_req_t req; + memset(&req, 0, sizeof req); + req.url = url; + req.method = "POST"; + req.body = body; + req.body_len = strlen(body); + req.extra_hdrs = hdrs; + req.verify_ca = NCM_HTTP_VERIFY_DEFAULT; + req.timeout_ms = 15000; + + char respbuf[8192]; + ncm_http_resp_t resp; + long t0 = (long)ncm_now_ms(); + rc = ncm_http_request(&req, respbuf, sizeof respbuf, &resp); + long dt = (long)ncm_now_ms() - t0; + printf("ncm_http_request rc=%d elapsed_ms=%ld\n", rc, dt); + if (rc != NCM_OK) return 2; + dump_resp("weapi account/get", &resp); + return 0; +} diff --git a/patch_v2/ncm_app/bench/monotonic_test b/patch_v2/ncm_app/bench/monotonic_test new file mode 100755 index 0000000..579a121 Binary files /dev/null and b/patch_v2/ncm_app/bench/monotonic_test differ diff --git a/patch_v2/ncm_app/bench/monotonic_test.c b/patch_v2/ncm_app/bench/monotonic_test.c new file mode 100644 index 0000000..77e20c2 --- /dev/null +++ b/patch_v2/ncm_app/bench/monotonic_test.c @@ -0,0 +1,38 @@ +/* monotonic_test.c — 宿主验证 ncm_now_ms(REALTIME) 与 ncm_monotonic_ms(MONOTONIC)。 + * 演示:用墙钟算 deadline 在「NTP 拨钟回退 Δ」时,(deadline-now) 被放大成巨大 rem; + * 用单调钟则不受影响(每次至多等剩余 deadline)。 + */ +#include "ncm.h" +#include "ncm_common.h" +#include +#include + +int main(void) { + int64_t w0 = ncm_now_ms(); + int64_t m0 = ncm_monotonic_ms(); + usleep(50 * 1000); + int64_t w1 = ncm_now_ms(); + int64_t m1 = ncm_monotonic_ms(); + printf("REALTIME advance: %lld ms\n", (long long)(w1 - w0)); + printf("MONOTONIC advance: %lld ms\n", (long long)(m1 - m0)); + if (w1 <= w0 || m1 <= m0) { printf("FAIL: clock not advancing\n"); return 1; } + + /* 模拟 NTP 把墙钟拨回 Δ=6h 后,(deadline-now) 的变化 */ + int64_t base_w = w0; + int64_t base_m = m0; + int64_t tmo = 15000; + int64_t dline_w = base_w + tmo, dline_m = base_m + tmo; + int64_t jump = 6 * 3600 * 1000; /* 拨回 6 小时 */ + + /* REALTIME:now 回退 jump → rem 巨大 */ + int64_t now_w = base_w - jump; + uint32_t rem_w = (uint32_t)(dline_w - now_w); + /* MONOTONIC:now 不受跳变影响 */ + int64_t now_m = base_m; + uint32_t rem_m = (uint32_t)(dline_m - now_m); + printf("REALTIME rem after -6h jump = %u ms (%.1f h)\n", rem_w, rem_w / 3600000.0); + printf("MONOTONIC rem after -6h jump = %u ms (%.2f s)\n", rem_m, rem_m / 1000.0); + if (rem_w > rem_m) printf("OK: REALTIME overflow confirmed, MONOTONIC unaffected\n"); + else { printf("FAIL: expected REALTIME rem to blow up\n"); return 1; } + return 0; +} diff --git a/patch_v2/ncm_app/bench/verify_home.c b/patch_v2/ncm_app/bench/verify_home.c new file mode 100644 index 0000000..cf4954f --- /dev/null +++ b/patch_v2/ncm_app/bench/verify_home.c @@ -0,0 +1,40 @@ +/* verify_home.c — 最终确认:复刻 App home 的完整链路(status_check 取 uid → user_playlist 拉歌单)。 + * 用与 App 完全相同的固定 core 库。真机只跑一次(风控:真实 cookie 请求严格受限)。 + * 用法: ./verify_home [data_dir] [cacert] + */ +#include "ncm.h" +#include "ncm_http.h" +#include "ncm_common.h" +#include +#include + +int main(int argc, char **argv) { + const char *data_dir = argc > 1 ? argv[1] : "/usr/data/mnt/sd_0/MeowData/os.meow.ncm/data"; + const char *cacert = argc > 2 ? argv[2] : "/usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/cacert.pem"; + + ncm_ctx_t ctx; + ncm_ctx_init(&ctx, data_dir); + printf("device_id=%s cookie_len=%zu\n", ctx.device_id, strlen(ctx.cookie)); + ncm_http_set_verify_default(1, cacert); + + /* 1) status_check 取 uid/nickname */ + char uid[32], nick[128]; + long t0 = (long)ncm_now_ms(); + int rc = ncm_login_status_check(ctx.device_id, ctx.cookie, uid, sizeof uid, nick, sizeof nick); + long dt = (long)ncm_now_ms() - t0; + printf("status_check rc=%d elapsed_ms=%ld uid=%s nick=%s\n", rc, dt, uid, nick); + if (rc != NCM_OK) { printf("VERIFY FAIL: status_check not OK\n"); return 1; } + + /* 2) user_playlist 拉歌单 */ + ncm_playlist_t pls[64]; + int n = 0; + t0 = (long)ncm_now_ms(); + rc = ncm_api_user_playlist(ctx.device_id, ctx.cookie, uid, 30, 0, pls, 64, &n); + dt = (long)ncm_now_ms() - t0; + printf("user_playlist rc=%d elapsed_ms=%ld count=%d\n", rc, dt, n); + if (rc != NCM_OK) { printf("VERIFY FAIL: user_playlist not OK\n"); return 1; } + for (int i = 0; i < n && i < 8; i++) + printf(" [%d] %s (%d tracks)\n", i, pls[i].name, pls[i].track_count); + printf("VERIFY OK: user=%s playlists=%d\n", uid, n); + return 0; +} diff --git a/patch_v2/ncm_app/core/ncm_common.c b/patch_v2/ncm_app/core/ncm_common.c index 6594023..49cc00a 100644 --- a/patch_v2/ncm_app/core/ncm_common.c +++ b/patch_v2/ncm_app/core/ncm_common.c @@ -42,12 +42,22 @@ int ncm_parse_int(const char *s, long *out) { return 0; } +/* 墙钟毫秒(CLOCK_REALTIME)。用于 request_id/_ntes_nnid/落盘时间戳等“取当前墙钟时间”的场合。 + * 勿用于超时/截止计算:REALTIME 会随 NTP 校时跳变,导致 deadline-now 算错(详见 ncm_http.c 注释)。 */ int64_t ncm_now_ms(void) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; } +/* 单调毫秒(CLOCK_MONOTONIC)。仅用于超时/截止/间隔计时——永不随 NTP/校时跳变, + * 保证 wait_io 的 (deadline - now) 不被校时扰动成巨大值。 */ +int64_t ncm_monotonic_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + void ncm_random_bytes(unsigned char *out, size_t n) { FILE *f = fopen("/dev/urandom", "rb"); if (f) { diff --git a/patch_v2/ncm_app/core/ncm_common.h b/patch_v2/ncm_app/core/ncm_common.h index e25a3b3..6039e84 100644 --- a/patch_v2/ncm_app/core/ncm_common.h +++ b/patch_v2/ncm_app/core/ncm_common.h @@ -47,7 +47,8 @@ size_t ncm_strlcpy(char *dst, const char *src, size_t sz); size_t ncm_strlcat(char *dst, const char *src, size_t sz); int ncm_streq(const char *a, const char *b); int ncm_parse_int(const char *s, long *out); /* 完整数字串,成功返回 0 */ -int64_t ncm_now_ms(void); /* 毫秒时间戳 */ +int64_t ncm_now_ms(void); /* 墙钟毫秒(CLOCK_REALTIME),供 request_id/落盘时间戳 */ +int64_t ncm_monotonic_ms(void); /* 单调毫秒(CLOCK_MONOTONIC),仅用于超时/截止/间隔计时 */ void ncm_random_bytes(unsigned char *out, size_t n); /* /dev/urandom,失败则回退时钟扰动 */ /* ---------- hex ---------- */ diff --git a/patch_v2/ncm_app/core/ncm_http.c b/patch_v2/ncm_app/core/ncm_http.c index 450eec6..4e1dec9 100644 --- a/patch_v2/ncm_app/core/ncm_http.c +++ b/patch_v2/ncm_app/core/ncm_http.c @@ -84,9 +84,13 @@ static int load_ca(mbedtls_x509_crt *cacert, const char *ca_path) { return -1; } -/* ---------- 等待 IO(deadline 约束) ---------- */ +/* ---------- 等待 IO(deadline 约束) ---------- + * 超时/截止一律用 ncm_monotonic_ms()(CLOCK_MONOTONIC)。若用 ncm_now_ms()(CLOCK_REALTIME), + * 设备 NTP 校时把墙钟拨回/拨快时,(deadline - now) 会被扰动成远超 15s 的值甚至溢出成巨大 + * uint32_t,于是 mbedtls_net_poll 得到超长时间,线程长期阻塞在 poll(表现为 wchan=poll_schedule_timeout + * 且 >10 分钟不超时)。单调钟不受校时影响,保证每次 poll 至多等剩余 deadline。 */ static int wait_io(mbedtls_net_context *net, uint32_t rw, int64_t deadline_ms) { - int64_t now = ncm_now_ms(); + int64_t now = ncm_monotonic_ms(); if (now >= deadline_ms) return MBEDTLS_ERR_SSL_TIMEOUT; uint32_t rem = (uint32_t)(deadline_ms - now); int r = mbedtls_net_poll(net, rw, rem); @@ -136,34 +140,37 @@ static int parse_headers(ncm_http_resp_t *resp) { resp->has_content_type = 0; resp->content_type[0] = 0; - /* 状态行: "HTTP/1.1 200 OK" */ - char line[NCM_HTTP_HDR_MAX]; - size_t li = 0; - const char *p = h; - /* 第一行 */ - while (*p && *p != '\r' && *p != '\n' && li < sizeof line - 1) line[li++] = *p++; - line[li] = 0; + /* 状态行: "HTTP/1.1 200 OK" —— 取协议版本后的状态码(显式用 strtol 忽略 reason 文本)。 */ + const char *eol = strstr(h, "\r\n"); + if (!eol) return -1; + char line[128]; + size_t line_len = (size_t)(eol - h); + if (line_len >= sizeof line) line_len = sizeof line - 1; + memcpy(line, h, line_len); + line[line_len] = 0; { - const char *sp = strchr(line, ' '); + const char *sp = strchr(line, ' '); /* 第一个空格即版本与状态码的分隔 */ if (sp) { - long st = parse_long_field(sp + 1); + sp++; + while (*sp == ' ' || *sp == '\t') sp++; + long st = strtol(sp, NULL, 10); /* "200 OK" -> 200,忽略 reason */ if (st >= 100 && st <= 599) resp->status = (int)st; } } - /* 头行遍历 */ - p = h; - while ((p = strstr(p, "\r\n")) != NULL) { - p += 2; - if (*p == '\r' || *p == '\n' || *p == 0) break; - const char *eol = strstr(p, "\r\n"); - if (!eol) break; - size_t l = (size_t)(eol - p); + + /* 头行遍历:逐行处理,不跳行(旧实现 strstr+&p+=2 会每隔一行跳一行,漏掉 Content-Length 等)。 */ + const char *p = eol + 2; + while (*p) { + if (*p == '\r' || *p == '\n') break; /* 空行 = 头结束 */ + const char *le = strstr(p, "\r\n"); + if (!le) break; + size_t l = (size_t)(le - p); char hdr[256]; - if (l >= sizeof hdr) { p = eol + 2; continue; } + if (l >= sizeof hdr) { p = le + 2; continue; } memcpy(hdr, p, l); hdr[l] = 0; char *colon = strchr(hdr, ':'); - if (!colon) { p = eol + 2; continue; } + if (!colon) { p = le + 2; continue; } *colon = 0; char *name = hdr; char *val = colon + 1; @@ -181,7 +188,7 @@ static int parse_headers(ncm_http_resp_t *resp) { resp->has_content_type = 1; ncm_strlcpy(resp->content_type, val, sizeof resp->content_type); } - p = eol + 2; + p = le + 2; } return 0; } @@ -206,7 +213,9 @@ typedef struct { static void body_st_init(body_st *b, const ncm_http_resp_t *hdr) { memset(b, 0, sizeof *b); - if (hdr->chunked) { + if (hdr->content_length == 0) { + b->st = BS_DONE; /* Content-Length: 0 → 无 body,避免等对端关闭 */ + } else if (hdr->chunked) { b->st = BS_CHUNK_SZ; } else if (hdr->content_length != (size_t)-1) { b->st = BS_LEN; @@ -320,7 +329,8 @@ static int http_run(const ncm_http_req_t *req, sink_t *sink, ncm_http_resp_t *re if (strcmp(url.scheme, "https") != 0 && strcmp(url.scheme, "http") != 0) return NCM_ERR_ARG; uint32_t tmo = req->timeout_ms ? req->timeout_ms : 15000; - int64_t deadline = ncm_now_ms() + (int64_t)tmo; + int64_t deadline = ncm_monotonic_ms() + (int64_t)tmo; /* 截止时刻用单调钟,避免 NTP 校时跳变 */ + mbedtls_net_context net; mbedtls_ssl_context ssl; @@ -366,8 +376,10 @@ static int http_run(const ncm_http_req_t *req, sink_t *sink, ncm_http_resp_t *re } else goto done; } - /* 构造请求(先写头、再写体,避免大 body 撑爆单一缓冲) */ - char headbuf[1024]; + /* 构造请求(先写头、再写体,避免大 body 撑爆单一缓冲)。 + * 头缓冲 ≥ extra_hdrs(≤NCM_REQ_HDRS_MAX=4096)+~200,(M2 联调) weapi 带真实会话 cookie 时 + * extra_hdrs 可达 ~1.1KB,旧 1024 会溢出返回 NCM_ERR_FULL(请求未发出),故用 8192。 */ + char headbuf[8192]; int hl; const char *m = req->method ? req->method : "POST"; int has_body = (req->body && req->body_len > 0); @@ -432,19 +444,22 @@ static int http_run(const ncm_http_req_t *req, sink_t *sink, ncm_http_resp_t *re if (split) { parse_headers(resp); body_st_init(&bst, resp); - size_t consumed = (size_t)(split + 4 - resp->headers); - /* 当前 tmp 中,已进 headers 的字节数 = space;tmp 剩余部分属于 body */ - /* headers 里 split+4 之后的字节实际上没被复制进 headers(因为 space<=..? ) - 这里简化:把 tmp[space..n) 送入 body */ + /* split+4 即 body 起始;注意其与 resp->headers+consumed 是同一地址, + 故先送 body 再在 consumed 处 NUL 终结头部,否则会覆盖 body 首字节。 */ + size_t consumed = (size_t)(split + 4 - resp->headers); /* body 起始偏移 */ + size_t old_hlen = resp->headers_len; + resp->headers_len = consumed; hdr_done = 1; - /* 需要把 headers 缓冲里 split+4 之后的字节(若有) 也送到 body */ - size_t hdr_after = resp->headers_len - consumed; + /* headers 缓冲里 split+4 之后的字节(本读取已复制进 headers 的部分)送入 body */ + size_t hdr_after = old_hlen - consumed; if (hdr_after > 0) { if (body_feed(&bst, (const unsigned char *)(split + 4), hdr_after, sink) < 0) { ret=NCM_ERR_JSON; goto done; } } + /* tmp 中超出被复制到 headers 的部分也属 body */ if (space < n) { if (body_feed(&bst, tmp + space, n - space, sink) < 0) { ret=NCM_ERR_JSON; goto done; } } + resp->headers[consumed] = 0; /* body 已复制走,安全 NUL 终结头部 */ } else { if (resp->headers_len >= NCM_HTTP_HDR_MAX - 1) { ret = NCM_ERR_FULL; goto done; } } diff --git a/patch_v2/ncm_app/core/ncm_login.c b/patch_v2/ncm_app/core/ncm_login.c index fb662b1..072158b 100644 --- a/patch_v2/ncm_app/core/ncm_login.c +++ b/patch_v2/ncm_app/core/ncm_login.c @@ -154,9 +154,10 @@ int ncm_login_status_check(const char *device_id, const char *cookie, req.body_len = strlen(body); req.extra_hdrs = hdrs; req.verify_ca = NCM_HTTP_VERIFY_DEFAULT; - req.timeout_ms = 15000; + /* 硬超时上限:即使服务端无响应也不久等(歌单请求会独立验证 cookie,status_check 只是取 uid/昵称的辅助) */ + req.timeout_ms = 6000; - char respbuf[4096]; + char respbuf[16384]; ncm_http_resp_t resp; rc = ncm_http_request(&req, respbuf, sizeof respbuf, &resp); if (rc != NCM_OK) return rc; diff --git a/patch_v2/ncm_app/core/ncm_req.c b/patch_v2/ncm_app/core/ncm_req.c index 35efb70..71dd470 100644 --- a/patch_v2/ncm_app/core/ncm_req.c +++ b/patch_v2/ncm_app/core/ncm_req.c @@ -17,24 +17,33 @@ #define APPVER "3.1.17.204416" #define CHANNEL "netease" -/* ---------- cookie map ---------- */ -typedef struct { char name[48]; char val[192]; } ck_t; +/* ---------- cookie map ---------- + * 值用 (ptr,len) 指向源串/字符串字面量,避免把 ~1KB 的 MUSIC_U 截断到固定缓冲 + * (旧实现 val[192] 会把 834 字节的 MUSIC_U 截到 191,导致服务端识别为游客 session)。 + * 所有 ptr 在 ncm_req_build 调用期间有效;ck_to_string 同步使用,不跨调用保留。 */ +typedef struct { char name[48]; const char *val; size_t vlen; } ck_t; typedef struct { ck_t p[48]; int n; } ckmap_t; static void ck_add_ev(ckmap_t *m, const char *name, const char *val) { if (m->n >= (int)(sizeof m->p / sizeof m->p[0])) return; ncm_strlcpy(m->p[m->n].name, name, sizeof m->p[m->n].name); - ncm_strlcpy(m->p[m->n].val, val ? val : "", sizeof m->p[m->n].val); + m->p[m->n].val = val ? val : ""; + m->p[m->n].vlen = val ? strlen(val) : 0; m->n++; } static int ck_find(const ckmap_t *m, const char *name) { for (int i = 0; i < m->n; i++) if (ncm_streq(m->p[i].name, name)) return i; return -1; } -static void ck_set(ckmap_t *m, const char *name, const char *val) { +static void ck_set(ckmap_t *m, const char *name, const char *val, size_t vlen) { int i = ck_find(m, name); - if (i >= 0) ncm_strlcpy(m->p[i].val, val ? val : "", sizeof m->p[i].val); - else ck_add_ev(m, name, val); + if (i >= 0) { m->p[i].val = val; m->p[i].vlen = vlen; } + else { + if (m->n >= (int)(sizeof m->p / sizeof m->p[0])) return; + ncm_strlcpy(m->p[m->n].name, name, sizeof m->p[m->n].name); + m->p[m->n].val = val; m->p[m->n].vlen = vlen; + m->n++; + } } static void ck_set_default(ckmap_t *m, const char *name, const char *val) { if (ck_find(m, name) < 0) ck_add_ev(m, name, val); @@ -42,19 +51,19 @@ static void ck_set_default(ckmap_t *m, const char *name, const char *val) { static void ck_to_string(const ckmap_t *m, char *out, size_t out_sz) { size_t o = 0; for (int i = 0; i < m->n; i++) { + size_t name_len = strlen(m->p[i].name); + size_t vlen = m->p[i].vlen; int sep = (o > 0) ? 2 : 0; /* "; " */ - if (o + sep + strlen(m->p[i].name) + 1 + strlen(m->p[i].val) + 1 > out_sz) break; + if (o + sep + name_len + 1 + vlen + 1 > out_sz) break; if (sep) { out[o++] = ';'; out[o++] = ' '; } - size_t nl = strlen(m->p[i].name); - memcpy(out + o, m->p[i].name, nl); o += nl; + memcpy(out + o, m->p[i].name, name_len); o += name_len; out[o++] = '='; - size_t vl = strlen(m->p[i].val); - memcpy(out + o, m->p[i].val, vl); o += vl; + memcpy(out + o, m->p[i].val, vlen); o += vlen; } out[o] = 0; } -/* 解析 cookie 字符串(; 分隔) 进 map */ +/* 解析 cookie 字符串(; 分隔) 进 map:值以 (ptr,len) 存(指向 cookie 源串,不截断),按名去重(后值覆盖)。 */ static void ck_parse(ckmap_t *m, const char *cookie) { if (!cookie) return; const char *p = cookie; @@ -65,13 +74,14 @@ static void ck_parse(ckmap_t *m, const char *cookie) { const char *semi = strchr(p, ';'); size_t name_len = eq ? (size_t)(eq - p) : (semi ? (size_t)(semi - p) : strlen(p)); if (eq) { - size_t val_len = semi ? (size_t)(semi - eq - 1) : strlen(eq + 1); - char name[48], val[192]; + const char *vstart = eq + 1; + size_t val_len = semi ? (size_t)(semi - vstart) : strlen(vstart); + char name[48]; if (name_len >= sizeof name) name_len = sizeof name - 1; memcpy(name, p, name_len); name[name_len] = 0; - if (val_len >= sizeof val) val_len = sizeof val - 1; - memcpy(val, eq + 1, val_len); val[val_len] = 0; - ck_set(m, name, val); + /* 去尾空格 */ + while (val_len && (vstart[val_len-1]==' '||vstart[val_len-1]=='\t')) val_len--; + ck_set(m, name, vstart, val_len); } p = semi ? semi + 1 : p + strlen(p); } @@ -83,7 +93,10 @@ int ncm_req_cookie_get(const char *cookie, const char *name, char *out, size_t o ck_parse(&m, cookie); int i = ck_find(&m, name); if (i < 0) return NCM_ERR_NOTFOUND; - ncm_strlcpy(out, m.p[i].val, out_sz); + size_t cp = m.p[i].vlen; + if (cp >= out_sz) cp = out_sz - 1; + memcpy(out, m.p[i].val, cp); + out[cp] = 0; return NCM_OK; } @@ -189,23 +202,23 @@ int ncm_req_build(const char *device_id, const char *cookie, const char *path, if (ncm_url_encode(params, p_enc, sizeof p_enc) < 0) return NCM_ERR_FULL; char referer[256]; snprintf(referer, sizeof referer, "https://music.163.com"); - /* 会话 cookie + 默认伪装字段补齐 */ + /* 会话 cookie + 默认伪装字段补齐。 + * 生成值 buffer(nuid/nuid_hex/nnid/wnmcid/nmtid)都提到本块作用域, + * 确保 ck 存的是其值指针在 ck_to_string 调用前仍然有效(ck 只存 ptr,不复制)。 */ + char nuid[96], nuid_hex[33], nnid[96], wnmcid[40], nmtid[9]; ckmap_t m; memset(&m, 0, sizeof m); ck_parse(&m, cookie); ck_add_ev(&m, "__remember_me", "true"); ck_add_ev(&m, "ntes_kaola_ad", "1"); - char nuid[96]; ncm_random_bytes((unsigned char *)nuid, 16); + ncm_random_bytes((unsigned char *)nuid, 16); /* _ntes_nnid = nuid,ts ; _ntes_nuid = nuid */ + ncm_hex_encode((const unsigned char *)nuid, 16, nuid_hex, 0); + snprintf(nnid, sizeof nnid, "%s,%lld", nuid_hex, (long long)ncm_now_ms()); + ck_add_ev(&m, "_ntes_nnid", nnid); + ck_add_ev(&m, "_ntes_nuid", nuid_hex); { - char nuid_hex[33]; ncm_hex_encode((const unsigned char *)nuid, 16, nuid_hex, 0); - char nnid[96]; - snprintf(nnid, sizeof nnid, "%s,%lld", nuid_hex, (long long)ncm_now_ms()); - ck_add_ev(&m, "_ntes_nnid", nnid); - ck_add_ev(&m, "_ntes_nuid", nuid_hex); - } - char wnmcid[40]; { unsigned char r[16]; ncm_random_bytes(r, 16); - char h[33]; ncm_hex_encode(r, 16, h, 0); snprintf(wnmcid, sizeof wnmcid, "%s", h); + ncm_hex_encode(r, 16, wnmcid, 0); } ck_set_default(&m, "WNMCID", wnmcid); ck_set_default(&m, "WEVNSM", "1.0.0"); @@ -216,10 +229,10 @@ int ncm_req_build(const char *device_id, const char *cookie, const char *path, ck_set_default(&m, "appver", APPVER); if (!strstr(path, "login")) { unsigned char r[4]; ncm_random_bytes(r, 4); - char h[9]; ncm_hex_encode(r, 4, h, 1); - ck_set_default(&m, "NMTID", h); + ncm_hex_encode(r, 4, nmtid, 1); /* 大写 */ + ck_set_default(&m, "NMTID", nmtid); } - char cookie_full[2048]; + char cookie_full[4096]; ck_to_string(&m, cookie_full, sizeof cookie_full); int n = snprintf(hdrs, hdrs_sz, diff --git a/patch_v2/ncm_app/ncm b/patch_v2/ncm_app/ncm index 5b0c5d6..325976f 100755 Binary files a/patch_v2/ncm_app/ncm and b/patch_v2/ncm_app/ncm differ diff --git a/patch_v2/ncm_app/test/test_ncm_core_host b/patch_v2/ncm_app/test/test_ncm_core_host index ec123ba..5a3eaa8 100755 Binary files a/patch_v2/ncm_app/test/test_ncm_core_host and b/patch_v2/ncm_app/test/test_ncm_core_host differ