From 1ccb3c7a930b2c54f42152dc8e070bc1dc867c87 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Thu, 12 Nov 2026 09:02:07 +0800 Subject: [PATCH] =?UTF-8?q?fix(daemon):=20M65=20P1=20=E4=B8=89=E6=9D=A1=20?= =?UTF-8?q?+=20P2-1=20=E2=80=94=20popen=20=E8=B6=85=E6=97=B6/=E5=85=B1?= =?UTF-8?q?=E4=BA=AB=E7=BC=93=E5=86=B2=E3=80=81bt=20scan=20=E6=9C=89?= =?UTF-8?q?=E7=95=8C=E6=94=B6=E5=89=B2=E4=B8=8E=E6=AF=AB=E7=A7=92=E8=8A=82?= =?UTF-8?q?=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 来自 M59 内存与资源安全审计(这 3 条 P1 很可能就是 2026-11-11 18:44 设备用户态崩塌的 成因:当时 sshd 连 fork 都做不到、ICMP 仍应答)。执行顺序按审计要求 P1-3 → P2-1 → P1-1 → P1-2。 P1-1 launcherd net.c:popen_read_tmo —— 对持续输出的子进程永不超时 + 堆无限增长 - 新增 meow_launcherd/popen_tmo.h:读循环的 deadline 移到**循环顶部**,输出加 256KB 硬 上限(超限返回 OVERFLOW,调用方 kill 子进程并按失败返回 NULL,绝不静默截断),缓冲交给 调用方或由本函数持有(无 static)。 - net.c 收尸改为 popen_tmo_reap_bounded(300ms + SIGKILL + 300ms):旧实现 SIGKILL 之后是 无条件 waitpid(…,0),子进程卡 D 态照样钉死 UI 线程。调用面(malloc 返回值 + free)不变, 14 个调用点零改动。 P1-2 audiod popen_read_tmo 的 static char buf[4096] 被 4 个线程共用 - 改成调用方传缓冲(`char *popen_read_tmo(tmo, cmd, out, cap)`),函数内无共享状态、不加锁 —— M55 刚把音量写路径做成无锁,音频线程上不能拿锁。7 个调用点各用自己的栈缓冲。 - 顺带修掉旧循环的 `remain` 不递减问题:旧写法每次 poll 都能重等满 tmo,最坏总时长 ≈ 4095 次 × 1500ms;现在总时长就是一个 tmo。 P1-3 netmgrd bt_scan_on/off 走无超时 system()(单线程主循环 + 55 个残留 bluetoothctl 的形态) - scan off:直接 run_cmd_ex(BT_SCAN_OFF_TMO_MS=600),超时如实回 MEOW_ERR_NODEV 并计入既有 bt_circuit_note。 - scan on:fork 一个 setsid 的 scan worker(在飞上限 1),worker 内部用 run_cmd_ex 跑同一条 pipeline(超时 + 输出上限);父进程只记账,bt_scan_reap() 在主循环每拍收割,过 deadline 杀**整个进程组**(pipeline 的 bluetoothctl 是孙进程,只杀 sh 会留孤儿)。 - 超时计入既有熔断(连 3 次 → 30s 内不再 fork);熔断中 scan on 不再起进程。 - 不在主循环里同步跑整条 pipeline:它要 ~5s(sleep 1 + dwell 4),同步跑等于每 7s 把 DNS 代理停 5s,是另一种挂死。 P2-1 扫描节流失效 7000 秒(mono_now() 返回秒,却与 7000 毫秒比较) - 节流改用 64 位毫秒时钟(bt_scan_now_ms / 函数指针钩子供宿主单测注入可控时钟),抽成纯 函数 bt_scan_throttle(now_ms);实际节流从 ~1.94 小时恢复到 7s。 测试(每条都有反向控制) - 新增 meow_launcherd/test_popen_tmo.c(已接入 build_launcher.sh test): 持续输出且永不退出的生产者 → 新循环 300ms 到点返回,旧循环 1500ms 内不返回; `yes`+64KB 上限 → 立刻 OVERFLOW;同一份 300KB 有界输入 → 旧循环照单全收 307200 字节、 新循环在上限处报 OVERFLOW;两线程交错读 → 旧 static 缓冲 A 读到 B 的内容,调用方缓冲各 读各的;收尸 313ms 有界且无残留子进程。 - meow_netmgrd/test_bt_ctl.c 增补:7s / 6.999s / 7.1s 边界(含走真实入口 bt_scan_on 的可控 时钟用例,秒/毫秒单位回归会在这里翻车);wedge 桩(scan 一律 exec sleep 60)下 scan off 611ms 有界返回且 ECHILD、scan on 的 worker 1200ms 窗口到期被整组收割、孙进程 全收(ECHILD)、超时进熔断并最终熔断拒绝 fork。 验证:netmgrd 宿主回归 79 PASS / 0 FAIL;test_bt_ctl ALL PASS;launcherd `build_launcher.sh test` ALL TESTS PASS(含新增用例);meow_netmgrd mipsel 交叉编译通过; audiod host 构建通过,test_audiod_host 的失败集合与改动前**逐条相同**(41 条,T8 SEGV 是 测试自身 strstr(q8,"rms=")+4 的既有缺陷,见 M59 审计 §5)。 仅源码:无二进制、未改 scripts/build/fw_content.conf。 --- patch_v2/meow_audiod/meow_audiod.c | 80 +++-- patch_v2/meow_launcherd/build_launcher.sh | 9 + patch_v2/meow_launcherd/net.c | 52 ++-- patch_v2/meow_launcherd/popen_tmo.h | 133 +++++++++ patch_v2/meow_launcherd/test_popen_tmo.c | 340 ++++++++++++++++++++++ patch_v2/meow_netmgrd/meow_netmgrd.c | 174 ++++++++++- patch_v2/meow_netmgrd/test_bt_ctl.c | 137 +++++++++ 7 files changed, 839 insertions(+), 86 deletions(-) create mode 100644 patch_v2/meow_launcherd/popen_tmo.h create mode 100644 patch_v2/meow_launcherd/test_popen_tmo.c diff --git a/patch_v2/meow_audiod/meow_audiod.c b/patch_v2/meow_audiod/meow_audiod.c index c853ad5..303ac7a 100644 --- a/patch_v2/meow_audiod/meow_audiod.c +++ b/patch_v2/meow_audiod/meow_audiod.c @@ -59,6 +59,7 @@ #include #include #include "meow_ipc.h" +#include "popen_tmo.h" /* M65 P1-2:有界读子进程 stdout(缓冲归调用方,无 static) */ #include "bt_dev.h" /* M50:端点探测 / PCM 名构造 / 回落退避(纯逻辑,不改任何配置文件) */ #include "vol_gain.h" /* M55:BT 写路径软件增益(与 DAC 同一锥度,全整数) */ @@ -570,11 +571,18 @@ static void ring_rebuild(void) } /* ---- amixer 封装(popen 限时;无 amixer → 优雅失败) ---- */ -/* 读子进程 stdout(非阻塞+poll 限时)。收割**必须有界**:驱动 wedge 时 amixer 可能 - * 挂在内核 ioctl 里,无条件 waitpid 会把调用线程(路由线程持 g_mu / IO 线程)一起拖死 - * ——真机长流卡死的级联路径之一。策略:先 WNOHANG 等 300ms;不死 → SIGKILL 再等 300ms; - * 仍不死(D 态)→ 放弃收割(泄漏一个子进程,换线程不卡死;已记日志)。 */ -static char *popen_read_tmo(int tmo_ms, const char *cmd) +/* 读子进程 stdout 到**调用方给的缓冲**(非阻塞 + poll 限时;缓冲不再 static)。 + * + * M65 P1-2(M59 审计 P1-2):旧实现是 `static char buf[4096]; … return buf;` —— 本进程有 + * 4 个线程(playback / route / io / vol),route_thread 在调 amixer_get(9,1) 之前**故意放掉 + * g_mu**(注释:amixer popen 不持 g_mu,防 wedge 级联),于是两个线程会同时写/解析同一个 + * 4KB 缓冲:route 线程可能读到被 playback 线程改写一半的 numid=9 回读值,误判「端口已武装」 + * 而跳过 apply_route —— 而这个函数的注释自己写着「FPGA 未烧 + enable 下开 PCM 会楔死整机」。 + * 修法:缓冲由调用方给(各线程各用自己的栈缓冲),函数内**无共享状态、不加锁** —— + * M55 刚把音量写路径做成无锁,音频线程上不能拿锁。 + * 返回 out(非 NULL:命令起得来就把缓冲交回,可能是空串);pipe/fork 失败返回 NULL。 + * 输出超过 cap 时保留已读部分并记一条日志(旧实现是静默截断,这里行为不放松但不再无声)。 */ +static char *popen_read_tmo(int tmo_ms, const char *cmd, char *out, size_t cap) { int p[2]; if (pipe(p) < 0) return NULL; @@ -583,39 +591,21 @@ static char *popen_read_tmo(int tmo_ms, const char *cmd) if (pid == 0) { dup2(p[1], 1); close(p[0]); close(p[1]); execl("/bin/sh", "sh", "-c", cmd, (char *)0); _exit(127); } close(p[1]); fcntl(p[0], F_SETFL, fcntl(p[0], F_GETFL, 0) | O_NONBLOCK); - static char buf[4096]; - size_t off = 0; - int remain = tmo_ms; - while (remain > 0 && off < sizeof buf - 1) { - struct pollfd pf = { p[0], POLLIN, 0 }; - int r = poll(&pf, 1, remain); - if (r < 0) { if (errno == EINTR) continue; break; } - if (r == 0) break; - ssize_t rd = read(p[0], buf + off, sizeof buf - 1 - off); - if (rd == 0) break; - if (rd < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) continue; break; } - off += (size_t)rd; - } + size_t len = 0; + int rc = popen_tmo_read_all(p[0], tmo_ms, &out, &cap, 0, &len, 0); close(p[0]); - int reaped = 0; - for (int i = 0; i < 30; i++) { /* 300ms 优雅退出窗口 */ - if (waitpid(pid, NULL, WNOHANG) == pid) { reaped = 1; break; } - struct timespec ts = { 0, 10 * 1000000L }; - nanosleep(&ts, NULL); - } - if (!reaped) { - logmsg("popen: child %ld did not exit in 300ms, SIGKILL (%s)", (long)pid, cmd); + if (rc == POPEN_TMO_OVERFLOW) + logmsg("popen: output over %zu bytes, kept the head only (%s)", cap, cmd); + /* 收割**必须有界**:驱动 wedge 时 amixer 可能挂在内核 ioctl 里,无条件 waitpid 会把调用 + * 线程(路由线程持 g_mu / IO 线程)一起拖死 —— 真机长流卡死的级联路径之一。 + * 策略:先 WNOHANG 等 300ms;不死 → SIGKILL 再等 300ms;仍不死(D 态)→ 放弃收割 + * (泄漏一个子进程,换线程不卡死;已记日志)。 */ + if (waitpid(pid, NULL, WNOHANG) == 0) { kill(pid, SIGKILL); - for (int i = 0; i < 30; i++) { - if (waitpid(pid, NULL, WNOHANG) == pid) { reaped = 1; break; } - struct timespec ts = { 0, 10 * 1000000L }; - nanosleep(&ts, NULL); - } - if (!reaped) + if (!popen_tmo_reap_bounded(pid)) logmsg("popen: child %ld unkillable (D-state, driver wedged?) — abandon reap", (long)pid); } - buf[off] = 0; - return buf; + return out; } static int amixer_set(int numid, int val, int card) @@ -623,7 +613,8 @@ static int amixer_set(int numid, int val, int card) char cmd[160]; snprintf(cmd, sizeof cmd, "%s%s cset numid=%d %d >/dev/null 2>&1", g_amixer, (card ? " -c 0" : ""), numid, val); - char *o = popen_read_tmo(1500, cmd); + char ob[64]; + char *o = popen_read_tmo(1500, cmd, ob, sizeof ob); if (!o) return -1; /* 测试钩子(仅 env MEOW_AUDIO_AMIXER_TRACE=):把每次 amixer cset 追加到文件, * 供宿主断言「音量双组写」PCM+DSD_VOLUME 是否成对下发(真机无 amixer 时不写/不报错)。 */ @@ -636,7 +627,7 @@ static int amixer_set(int numid, int val, int card) close(tfd); } } - /* popen_read_tmo 用的是静态缓冲,这里不 free(与 meow_route.c 同权衡) */ + /* 缓冲是上面的栈数组:无 static、无锁,多个线程各用各的(M65 P1-2) */ return 0; } @@ -644,7 +635,8 @@ static int amixer_get(int numid, int card) { char cmd[160]; snprintf(cmd, sizeof cmd, "%s%s cget numid=%d 2>/dev/null", g_amixer, (card ? " -c 0" : ""), numid); - char *o = popen_read_tmo(1500, cmd); + char ob[4096]; + char *o = popen_read_tmo(1500, cmd, ob, sizeof ob); if (!o) return -1; const char *ln = o; while (ln && *ln) { @@ -993,7 +985,8 @@ static int bt_collect_candidates(char (*cand)[18], int max) { if (!cand || max <= 0) return 0; int n = 0; - char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null"); + char ob[4096]; + char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null", ob, sizeof ob); if (o && o[0]) { char m[18]; if (bt_dev_detect_mac(o, m, sizeof m) == 0) { @@ -1001,7 +994,7 @@ static int bt_collect_candidates(char (*cand)[18], int max) logmsg("bt: candidate#%d %s (source=list-pcms endpoint)", n, m); } } - o = popen_read_tmo(1200, "hcitool con 2>/dev/null"); + o = popen_read_tmo(1200, "hcitool con 2>/dev/null", ob, sizeof ob); if (o) { char macs[4][18]; int k = bt_dev_acl_macs(o, macs, 4); @@ -1022,7 +1015,8 @@ static int bt_collect_candidates(char (*cand)[18], int max) static void bt_refresh_endpoint(void) { g_bt_pcm[0] = 0; - char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null"); + char ob[4096]; + char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null", ob, sizeof ob); if (!o) return; char path[128]; if (bt_dev_pick_endpoint(o, g_bt_mac, path, sizeof path) == 0) @@ -1186,7 +1180,8 @@ static void bt_vol_full(void) if (!pcm[0]) { logmsg("bt: avrcp set-max skipped (no endpoint path for %s)", g_bt_mac); return; } char cmd[256]; snprintf(cmd, sizeof cmd, "%s volume %s 127 >/dev/null 2>&1", BT_VOL_FULL_PCM, pcm); - char *r = popen_read_tmo(1500, cmd); + char ob[64]; + char *r = popen_read_tmo(1500, cmd, ob, sizeof ob); logmsg("bt: avrcp volume set max(127) (%s)%s", pcm, r ? " ok" : " (exec failed; AVRCP still controls)"); } @@ -1222,7 +1217,8 @@ static void bt_read_used_codec(void) if (!g_bt_pcm[0]) return; char cmd[256]; snprintf(cmd, sizeof cmd, "%s info %s 2>/dev/null", BT_VOL_FULL_PCM, g_bt_pcm); - char *o = popen_read_tmo(1500, cmd); + char ob[4096]; + char *o = popen_read_tmo(1500, cmd, ob, sizeof ob); if (!o) return; char codec[16]; if (bt_dev_info_codec(o, codec, sizeof codec) == 0) { diff --git a/patch_v2/meow_launcherd/build_launcher.sh b/patch_v2/meow_launcherd/build_launcher.sh index 3240884..4bbf569 100755 --- a/patch_v2/meow_launcherd/build_launcher.sh +++ b/patch_v2/meow_launcherd/build_launcher.sh @@ -57,6 +57,15 @@ run_tests() { rc=1 fi + # M65:子进程 stdout 有界读取(popen_tmo.h;launcherd/net.c 与 audiod 共用) + # 含反向控制(旧循环在持续输出下永不返回 / 无输出上限 / static 缓冲串写) + echo "== host test: test_popen_tmo ==" + if "$HOSTCC" -I. -Wall -Wextra test_popen_tmo.c -o "$TMPD/test_popen_tmo" -lpthread; then + "$TMPD/test_popen_tmo" || rc=1 + else + rc=1 + fi + if [ $rc -eq 0 ]; then echo "== ALL TESTS PASS =="; else echo "!! TESTS FAILED"; fi return $rc } diff --git a/patch_v2/meow_launcherd/net.c b/patch_v2/meow_launcherd/net.c index 884104f..7a9efc4 100644 --- a/patch_v2/meow_launcherd/net.c +++ b/patch_v2/meow_launcherd/net.c @@ -1,6 +1,7 @@ /* net.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/ /* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/ #include "meow_launcher_internal.h" +#include "popen_tmo.h" /* M65 P1-1:有界读子进程 stdout(deadline 在循环顶部 + 256KB 上限) */ /* wlan0 是否已联网(链路 up,与状态栏 WIFI 指示一致) */ @@ -76,7 +77,16 @@ int bt_role(char *desc, int len); /* 前向声明;实现在网络段(用 p * 避免绘制热点反复 spawn。 */ /* 带超时跑一条命令并读 stdout(防 wpa_supplicant/BlueZ 未跑时命令挂起)。 - * 返回 malloc 字符串(调用方 free);超时/失败返回 NULL;子进程超时被 SIGKILL。 */ + * 返回 malloc 字符串(调用方 free);超时/失败返回 NULL;子进程超时/超限被 SIGKILL。 + * + * M65 P1-1(M59 审计:'收益最大、改动最小'的一条):读循环搬进 popen_tmo.h,判据是 + * ① deadline 在**循环顶部**判 —— 旧实现只在 poll 超时分支里判,而持续输出的子进程 + * 让 poll 每次立刻返回,deadline 永远轮不到(审计 `yes` 复现:300ms 超时下 8s 不返回); + * ② 输出**硬上限 256KB**,超限按失败返回 NULL(杀掉子进程),**不静默截断** —— + * 静默截断会让上层以为拿到了完整输出; + * ③ 收尸有界(300ms + SIGKILL + 300ms):旧实现 SIGKILL 之后是无条件 waitpid(…, 0), + * 子进程若卡在 D 态照样把 UI 线程永久钉住。 + * 缓冲仍是本函数 malloc、调用方 free(调用面不变,14 个调用点零改动)。 */ char *popen_read_tmo(int tmo_ms, const char *cmd) { int fds[2]; @@ -96,34 +106,20 @@ char *popen_read_tmo(int tmo_ms, const char *cmd) int fl = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, fl | O_NONBLOCK); char *out = NULL; size_t cap = 0, len = 0; - char tmp[256]; - int done = 0; - unsigned long long deadline = now_ms() + tmo_ms; - while (!done) { - struct pollfd pfd = { fd, POLLIN, 0 }; - int pr = poll(&pfd, 1, 25); - if (pr < 0) break; - if (pr == 0) { if (now_ms() > deadline) break; continue; } - ssize_t r = read(fd, tmp, sizeof tmp); - if (r > 0) { - if (len + (size_t)r + 1 > cap) { - size_t nc = cap ? cap * 2 : 512; - char *nn = realloc(out, nc); - if (!nn) { free(out); out = NULL; break; } - out = nn; cap = nc; - } - memcpy(out + len, tmp, (size_t)r); len += (size_t)r; out[len] = 0; - } else if (r == 0) { - done = 1; - } else if (errno != EAGAIN && errno != EWOULDBLOCK) { - done = 1; - } - } - /* 子进程收尸:已退出则不杀;仍在跑(读到 EOF 前超时)则 SIGKILL,防僵尸。 */ - int status = 0; - pid_t rp = waitpid(pid, &status, WNOHANG); - if (rp == 0) { kill(pid, SIGKILL); waitpid(pid, &status, 0); } + int rc = popen_tmo_read_all(fd, tmo_ms, &out, &cap, POPEN_TMO_MAX, &len, 1); close(fd); + if (rc == POPEN_TMO_OVERFLOW) { + /* 超上限 = 子进程在无限吐(或输出异常大):按失败处理,绝不返回半截当完整。 */ + kill(pid, SIGKILL); + free(out); + out = NULL; + } + /* 子进程收尸:已退出则不杀;仍在跑(读到 EOF 前超时/超限)则 SIGKILL,防僵尸。 */ + if (waitpid(pid, NULL, WNOHANG) == 0) { + kill(pid, SIGKILL); + if (!popen_tmo_reap_bounded(pid)) + fprintf(stderr, "[net] popen: child %ld unkillable (D-state) — abandon reap\n", (long)pid); + } return out; } diff --git a/patch_v2/meow_launcherd/popen_tmo.h b/patch_v2/meow_launcherd/popen_tmo.h new file mode 100644 index 0000000..22f0489 --- /dev/null +++ b/patch_v2/meow_launcherd/popen_tmo.h @@ -0,0 +1,133 @@ +/* popen_tmo.h — 子进程 stdout 的**有界**读取(M65;launcherd/net.c 与 audiod/meow_audiod.c 共用)。 + * + * 三条不变量,都是 M59 内存/资源安全审计里的 P1(原文: + * .tower/comms/inbox/20261112-survey-m59-memsafety-tower-survey-summary.md): + * + * 1. 超时判定在**循环顶部**(P1-1)。子进程持续吐数据时 poll 每次立刻返回,若只在 + * 「poll 超时」分支里看 deadline,deadline 就永远轮不到 → 调用线程永久挂死。 + * 审计用 `yes` 定向复现:300ms 超时下 8s 不返回,RSS 236MB→397MB→657MB。 + * 2. 输出有**硬上限**,超限**报错**而不是静默截断(P1-1 / P2-7)。静默截断会让上层 + * 以为拿到了完整输出 —— 那是另一种假成功。超限返回 POPEN_TMO_OVERFLOW,调用方必须 + * 按失败处理并杀掉子进程。 + * 3. **无 static 状态**(P1-2)。缓冲一律由调用方提供或由本函数用 *buf 持有,多个线程 + * 各用各的缓冲;旧实现 `static char buf[4096]; … return buf;` 被 audiod 的 4 个线程 + * 并发使用 → amixer cget 读回值可能被另一个线程改写一半。 + * + * 只依赖 POSIX,便于宿主单测直接 include(patch_v2/meow_launcherd/test_popen_tmo.c)。 + */ +#ifndef MEOW_POPEN_TMO_H +#define MEOW_POPEN_TMO_H + +#include +#include +#include +#include +#include +#include +#include +#include + +/* 输出硬上限:256 KB。设备总内存 56 MB;最大的正常输出(wpa_cli scan_results / + * bluetoothctl devices)也就几 KB。越过这个量级只可能是子进程在无限吐。 */ +#define POPEN_TMO_MAX (256 * 1024) + +enum { + POPEN_TMO_EOF = 0, /* 读到 EOF:*len 是完整输出 */ + POPEN_TMO_DEADLINE = 1, /* 到 deadline:*len 是已读到的部分 */ + POPEN_TMO_OVERFLOW = 2, /* 越过硬上限:*len 是上限内已读到的部分(调用方按失败处理) */ + POPEN_TMO_ERROR = 3 /* poll/read 硬错误;*len 是已读到的部分 */ +}; + +static long long popen_tmo_now_ms(void) +{ + struct timespec tp; + clock_gettime(CLOCK_MONOTONIC, &tp); + return (long long)tp.tv_sec * 1000LL + tp.tv_nsec / 1000000LL; +} + +/* 从 fd 读子进程 stdout,最多等 tmo_ms 毫秒。 + * grow=0:定长缓冲 —— *buf 由调用方给,*cap 是容量,最多保留 *cap-1 字节; + * grow=1:*buf 由本函数 realloc 增长,*cap 是当前容量,总量不得超过 hard_max; + * grow=0 且 *cap==0(或 *buf==NULL):**排空丢弃**模式,只保证子进程不因 pipe 满而阻塞。 + * hard_max==0 时按 POPEN_TMO_MAX 处理。 + * 返回 POPEN_TMO_*;*len 是已保留字节数;非丢弃模式下 *buf 末尾总有 NUL。 */ +static int popen_tmo_read_all(int fd, int tmo_ms, char **buf, size_t *cap, + size_t hard_max, size_t *len, int grow) +{ + size_t off = 0; + int rc = POPEN_TMO_EOF; + int discard = (!grow && (!buf || !*buf || *cap == 0)); + if (hard_max == 0) hard_max = POPEN_TMO_MAX; + if (grow) { *buf = NULL; *cap = 0; } + else if (!discard && *buf) (*buf)[0] = 0; + long long deadline = popen_tmo_now_ms() + (tmo_ms > 0 ? tmo_ms : 1); + char chunk[512]; + + for (;;) { + /* ① 循环顶部判超时:子进程一直吐数据时也必须到期返回(P1-1 的核心)。 */ + if (popen_tmo_now_ms() >= deadline) { rc = POPEN_TMO_DEADLINE; break; } + long long remain = deadline - popen_tmo_now_ms(); + struct pollfd pf = { fd, POLLIN, 0 }; + int wait = (int)(remain > 25 ? 25 : (remain > 0 ? remain : 1)); /* 25ms 一片,保持可打断 */ + int pr = poll(&pf, 1, wait); + if (pr < 0) { if (errno == EINTR) continue; rc = POPEN_TMO_ERROR; break; } + if (pr == 0) continue; /* 这一小段没数据:回顶部看 deadline */ + ssize_t rd = read(fd, chunk, sizeof chunk); + if (rd < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) continue; + rc = POPEN_TMO_ERROR; + break; + } + if (rd == 0) { rc = POPEN_TMO_EOF; break; } + size_t n = (size_t)rd; + if (discard) continue; /* 丢弃模式:只排水 */ + if (grow) { + if (off + n + 1 > hard_max) { rc = POPEN_TMO_OVERFLOW; break; } /* ② 硬上限 */ + if (off + n + 1 > *cap) { + size_t nc = *cap ? *cap : 512; + while (nc < off + n + 1) nc *= 2; + if (nc > hard_max) nc = hard_max; + char *nb = realloc(*buf, nc); + if (!nb) { rc = POPEN_TMO_ERROR; break; } + *buf = nb; *cap = nc; + } + memcpy(*buf + off, chunk, n); + off += n; + (*buf)[off] = 0; + } else { + size_t room = *cap - 1 - off; + if (n > room) { /* ② 定长:写满即超限,不静默截断 */ + memcpy(*buf + off, chunk, room); + off += room; + (*buf)[off] = 0; + rc = POPEN_TMO_OVERFLOW; + break; + } + memcpy(*buf + off, chunk, n); + off += n; + (*buf)[off] = 0; + } + } + if (len) *len = off; + return rc; +} + +/* 有界收尸:先 WNOHANG 等 300ms;不死 SIGKILL 再等 300ms;仍不死(D 态,驱动 wedge) + * 放弃收割(泄漏一个子进程换调用线程不被拖死,与 audiod/netmgrd 同权衡)。返回 1=已收。 */ +static int popen_tmo_reap_bounded(pid_t pid) +{ + for (int i = 0; i < 30; i++) { + if (waitpid(pid, NULL, WNOHANG) == pid) return 1; + struct timespec ts = { 0, 10 * 1000000L }; + nanosleep(&ts, NULL); + } + kill(pid, SIGKILL); + for (int i = 0; i < 30; i++) { + if (waitpid(pid, NULL, WNOHANG) == pid) return 1; + struct timespec ts = { 0, 10 * 1000000L }; + nanosleep(&ts, NULL); + } + return 0; +} + +#endif /* MEOW_POPEN_TMO_H */ diff --git a/patch_v2/meow_launcherd/test_popen_tmo.c b/patch_v2/meow_launcherd/test_popen_tmo.c new file mode 100644 index 0000000..5ded581 --- /dev/null +++ b/patch_v2/meow_launcherd/test_popen_tmo.c @@ -0,0 +1,340 @@ +/* test_popen_tmo.c — M65 P1-1 / P1-2 宿主验证(本程序非生产代码)。 + * + * 被测对象是 popen_tmo.h(生产代码 launcherd/net.c 的 popen_read_tmo 与 audiod/meow_audiod.c + * 的 popen_read_tmo 都只用这一个读循环)。每组断言都配**反向控制**:把 M59 审计里的旧实现 + * 原样抄进来(old_read / old_static_read_cmd),证明旧代码在同样输入下确实失败 —— + * 只看「新代码返回了」不算证明,必须同时证明「旧代码不会返回」。 + * + * 反向控制 A(P1-1 永不超时):持续输出但**非常慢**的生产者(2B/20ms)。旧循环的 deadline + * 只在 poll 超时分支检查 → 永不触发;新循环在**顶部**检查 → 到期就返回。 + * 用慢生产者而不是 `yes`:审计里 `yes` 版 5s 就涨到 657MB(设备 56MB),在这里会打死宿主。 + * 反向控制 B(P1-1 无上限):让子进程输出 300KB(有界,宿主安全)。旧循环照单全收 + * (>256KB 也不管),新实现在硬上限处**主动**返回 OVERFLOW —— 上限确实是新加的。 + * 反向控制 C(P1-2 共享缓冲):两个线程用旧式 static 缓冲交错读,A 读到的是 B 的内容 + * (这就是 audiod 里 amixer cget numid=9 被改写一半的模型);同样的交错用调用方缓冲 + * 则各读各的。 + * + * 编译(在 patch_v2/meow_launcherd 下):cc -Wall -Wextra test_popen_tmo.c -o test_popen_tmo -lpthread + * 运行:./test_popen_tmo + */ +#include "popen_tmo.h" + +#include +#include +#include +#include +#include + +static int g_fail = 0; +#define CHECK(cond) do { if (!(cond)) { printf(" FAIL: %s (line %d)\n", #cond, __LINE__); g_fail++; } \ + else { printf(" ok : %s\n", #cond); } } while (0) + +/* 持续输出且**永不退出**的生产者:先一次性灌 32KB 把 pipe 填满(保证 poll 每次都有数据, + * 旧循环因此永远进不到 deadline 分支),之后 1B/10ms 慢慢滴(内存涨不起来,反向控制可以在 + * 宿主上安全地跑到被外部杀掉)。注意不能用 `yes`:审计里 5s 就 657MB,宿主会被打死。 */ +static char g_slow_cmd[512]; +static void build_slow_producer(void) +{ + char chunk[129]; + memset(chunk, 'x', 128); + chunk[128] = 0; + snprintf(g_slow_cmd, sizeof g_slow_cmd, + "c='%s'; i=0; while [ $i -lt 256 ]; do printf '%%s' \"$c\"; i=$((i+1)); done; " + "while :; do printf 'y'; sleep 0.01; done", chunk); +} + +/* fork 一个生产者,返回读端 fd(非阻塞),*pid 是子进程。失败返回 -1。 */ +static int spawn_cmd(const char *cmd, pid_t *pid) +{ + int p[2]; + if (pipe(p) != 0) return -1; + pid_t c = fork(); + if (c < 0) { close(p[0]); close(p[1]); return -1; } + if (c == 0) { + close(p[0]); + dup2(p[1], 1); + close(p[1]); + execl("/bin/sh", "sh", "-c", cmd, (char *)0); + _exit(127); + } + close(p[1]); + fcntl(p[0], F_SETFL, fcntl(p[0], F_GETFL, 0) | O_NONBLOCK); + *pid = c; + return p[0]; +} + +static void kill_reap(pid_t pid) +{ + if (pid <= 0) return; + kill(pid, SIGKILL); + (void)popen_tmo_reap_bounded(pid); +} + +/* ---------------- 反向控制:旧 launcherd/net.c 的读循环(原文,仅 now_ms→popen_tmo_now_ms) ---------------- */ +static char *old_read(int fd, int tmo_ms) +{ + char *out = NULL; size_t cap = 0, len = 0; + char tmp[256]; + int done = 0; + long long deadline = popen_tmo_now_ms() + tmo_ms; + while (!done) { + struct pollfd pfd = { fd, POLLIN, 0 }; + int pr = poll(&pfd, 1, 25); + if (pr < 0) break; + if (pr == 0) { if (popen_tmo_now_ms() > deadline) break; continue; } /* ← 只在 poll 超时分支看 deadline */ + ssize_t r = read(fd, tmp, sizeof tmp); + if (r > 0) { + if (len + (size_t)r + 1 > cap) { + size_t nc = cap ? cap * 2 : 512; /* ← realloc 没有字节上限 */ + char *nn = realloc(out, nc); + if (!nn) { free(out); return NULL; } + out = nn; cap = nc; + } + memcpy(out + len, tmp, (size_t)r); len += (size_t)r; out[len] = 0; + } else if (r == 0) { + done = 1; + } else if (errno != EAGAIN && errno != EWOULDBLOCK) { + done = 1; + } + } + return out; +} + +/* 在子进程里跑旧循环(旧循环可能吃很多内存,隔一层进程隔离)。返回 1=wait_ms 内自己退出, + * 0=超时仍在跑(已被 kill+reap)。*out_len = 旧循环读回的字节数(-1=返回 NULL/没跑完)。 */ +static int old_loop_in_child(const char *cmd, int tmo_ms, int wait_ms, long *out_len) +{ + int rp[2]; + if (pipe(rp) != 0) return -1; + fflush(NULL); + pid_t c = fork(); + if (c < 0) { close(rp[0]); close(rp[1]); return -1; } + if (c == 0) { + close(rp[0]); + pid_t pp = 0; + int fd = spawn_cmd(cmd, &pp); + char *o = (fd >= 0) ? old_read(fd, tmo_ms) : NULL; + if (fd >= 0) close(fd); + if (pp > 0) kill_reap(pp); + long l = o ? (long)strlen(o) : -1; + if (write(rp[1], &l, sizeof l) < 0) { } + _exit(o ? 0 : 7); + } + close(rp[1]); + int st = 0, finished = 0; + for (int i = 0; i < wait_ms / 10; i++) { + if (waitpid(c, &st, WNOHANG) == c) { finished = 1; break; } + struct timespec ts = { 0, 10 * 1000000L }; + nanosleep(&ts, NULL); + } + if (!finished) kill_reap(c); + long l = -1; + if (read(rp[0], &l, sizeof l) != (ssize_t)sizeof l) l = -1; + close(rp[0]); + if (out_len) *out_len = l; + return finished; +} + +/* ---------------- T1:deadline 在循环顶部(P1-1 主案) ---------------- */ +static void t1_deadline_at_top(void) +{ + printf("== T1 持续输出子进程:deadline 必须在循环顶部(tmo=300ms) ==\n"); + pid_t pid = 0; + int fd = spawn_cmd(g_slow_cmd, &pid); + CHECK(fd >= 0); + char *buf = NULL; size_t cap = 0, len = 0; + long long t0 = popen_tmo_now_ms(); + int rc = popen_tmo_read_all(fd, 300, &buf, &cap, POPEN_TMO_MAX, &len, 1); + long long el = popen_tmo_now_ms() - t0; + close(fd); + kill_reap(pid); + printf(" [info] 新实现:rc=%d len=%zu 滞回=%lldms\n", rc, len, el); + CHECK(rc == POPEN_TMO_DEADLINE); + CHECK(el >= 300); + CHECK(el < 1200); /* 到期就回,不再等子进程 EOF */ + CHECK(len < POPEN_TMO_MAX); /* 有界:32KB 灌入 + 滴流,不会无限涨 */ + CHECK(buf != NULL && strlen(buf) == len); + free(buf); + + /* 反向控制:同一个输入,旧循环永不返回。 */ + long oldlen = 0; + int finished = old_loop_in_child(g_slow_cmd, 300, 1500, &oldlen); + printf(" [info] 旧实现:1500ms 内%s退出(永不返回=BUG)\n", finished ? "已" : "未"); + CHECK(finished == 0); /* ← 反向控制:旧代码不返回 */ +} + +/* ---------------- T2:输出硬上限(P1-1 后半 / P2-7) ---------------- */ +static void t2_hard_cap(void) +{ + printf("== T2 无限输出:grow 模式 64KB 硬上限 / 定长模式写满即超限 ==\n"); + pid_t pid = 0; + int fd = spawn_cmd("yes", &pid); + CHECK(fd >= 0); + char *buf = NULL; size_t cap = 0, len = 0; + long long t0 = popen_tmo_now_ms(); + int rc = popen_tmo_read_all(fd, 5000, &buf, &cap, 64 * 1024, &len, 1); /* tmo 远大于触发点 */ + long long el = popen_tmo_now_ms() - t0; + close(fd); + kill_reap(pid); + printf(" [info] grow: rc=%d len=%zu cap=%zu 滞回=%lldms\n", rc, len, cap, el); + CHECK(rc == POPEN_TMO_OVERFLOW); /* 超限**报错**,不是静默截断 */ + CHECK(len > 0 && len < 64 * 1024); + CHECK(cap <= 64 * 1024); + CHECK(el < 1500); /* 是「上限触发」而不是「超时触发」 */ + free(buf); + + pid_t pid2 = 0; + int fd2 = spawn_cmd("yes", &pid2); + CHECK(fd2 >= 0); + char fixed[1024]; + size_t len2 = 0; + char *p = fixed; size_t cp = sizeof fixed; + rc = popen_tmo_read_all(fd2, 5000, &p, &cp, 0, &len2, 0); + close(fd2); + kill_reap(pid2); + printf(" [info] fixed: rc=%d len=%zu\n", rc, len2); + CHECK(rc == POPEN_TMO_OVERFLOW); + CHECK(len2 == sizeof fixed - 1); + CHECK(fixed[sizeof fixed - 1] == 0); /* 总是 NUL 结尾,解析不会越界 */ + + /* 反向控制:同一份 300KB 有界输出,旧循环照单全收(>256KB 也返回),没有上限。 */ + long oldlen = 0; + int finished = old_loop_in_child("yes | head -c 307200", 5000, 8000, &oldlen); + printf(" [info] 旧实现读到 %ld 字节(上限 %d)\n", oldlen, POPEN_TMO_MAX); + CHECK(finished == 1); + CHECK(oldlen > POPEN_TMO_MAX); /* ← 反向控制:旧实现没有上限 */ + /* 同一输入走新实现的**生产上限**(256KB):必须报 OVERFLOW 而不是带回 300KB。 */ + pid_t pid3 = 0; + int fd3 = spawn_cmd("yes | head -c 307200", &pid3); + CHECK(fd3 >= 0); + char *buf3 = NULL; size_t cap3 = 0, len3 = 0; + rc = popen_tmo_read_all(fd3, 5000, &buf3, &cap3, POPEN_TMO_MAX, &len3, 1); + close(fd3); + kill_reap(pid3); + printf(" [info] 新实现同输入:rc=%d len=%zu\n", rc, len3); + CHECK(rc == POPEN_TMO_OVERFLOW); + CHECK(len3 < POPEN_TMO_MAX); + free(buf3); +} + +/* ---------------- T3:调用方缓冲 vs static 缓冲(P1-2) ---------------- */ +static pthread_barrier_t g_b1, g_b2; +static char g_shared[4096]; /* 旧实现的 static 缓冲(模型) */ +static char g_seen_old[64], g_seen_new[64]; + +/* 旧 audiod 形态:static 缓冲 + return 指针。 */ +static char *old_static_read_cmd(const char *cmd) +{ + pid_t pid = 0; + int fd = spawn_cmd(cmd, &pid); + if (fd < 0) return NULL; + char *b = g_shared; size_t cap = sizeof g_shared, len = 0; + popen_tmo_read_all(fd, 1500, &b, &cap, 0, &len, 0); + close(fd); + kill_reap(pid); + return g_shared; +} + +static void *thr_old_a(void *arg) +{ + (void)arg; + char *o = old_static_read_cmd("echo AAAAAAAAAA"); + pthread_barrier_wait(&g_b1); + pthread_barrier_wait(&g_b2); /* 等 B 读完(旧代码里这就是「A 还没解析」的窗口) */ + snprintf(g_seen_old, sizeof g_seen_old, "%s", o ? o : ""); + return NULL; +} +static void *thr_old_b(void *arg) +{ + (void)arg; + pthread_barrier_wait(&g_b1); + old_static_read_cmd("echo BBBBBBBBBB"); + pthread_barrier_wait(&g_b2); + return NULL; +} + +static void *thr_new_a(void *arg) +{ + (void)arg; + char buf[256]; + pid_t pid = 0; + int fd = spawn_cmd("echo AAAAAAAAAA", &pid); + size_t len = 0; + char *b = buf; size_t cap = sizeof buf; + if (fd >= 0) { popen_tmo_read_all(fd, 1500, &b, &cap, 0, &len, 0); close(fd); kill_reap(pid); } + pthread_barrier_wait(&g_b1); + pthread_barrier_wait(&g_b2); + snprintf(g_seen_new, sizeof g_seen_new, "%s", buf); + return NULL; +} +static void *thr_new_b(void *arg) +{ + (void)arg; + pthread_barrier_wait(&g_b1); + char buf[256]; + pid_t pid = 0; + int fd = spawn_cmd("echo BBBBBBBBBB", &pid); + size_t len = 0; + char *b = buf; size_t cap = sizeof buf; + if (fd >= 0) { popen_tmo_read_all(fd, 1500, &b, &cap, 0, &len, 0); close(fd); kill_reap(pid); } + pthread_barrier_wait(&g_b2); + return NULL; +} + +static void t3_caller_buffer(void) +{ + printf("== T3 并发读:旧 static 缓冲会串(P1-2),调用方缓冲各归各 ==\n"); + pthread_t a, b; + pthread_barrier_init(&g_b1, NULL, 2); + pthread_barrier_init(&g_b2, NULL, 2); + g_seen_old[0] = g_seen_new[0] = 0; + + pthread_create(&a, NULL, thr_old_a, NULL); + pthread_create(&b, NULL, thr_old_b, NULL); + pthread_join(a, NULL); pthread_join(b, NULL); + printf(" [info] 旧实现 A 读到:%s\n", g_seen_old); + CHECK(strncmp(g_seen_old, "AAAAAAAAAA", 10) != 0); /* ← 反向控制:A 读到的是 B 的内容 */ + + pthread_barrier_destroy(&g_b1); + pthread_barrier_destroy(&g_b2); + pthread_barrier_init(&g_b1, NULL, 2); + pthread_barrier_init(&g_b2, NULL, 2); + pthread_create(&a, NULL, thr_new_a, NULL); + pthread_create(&b, NULL, thr_new_b, NULL); + pthread_join(a, NULL); pthread_join(b, NULL); + printf(" [info] 新实现 A 读到:%s", g_seen_new); + CHECK(strncmp(g_seen_new, "AAAAAAAAAA", 10) == 0); /* 调用方缓冲:不串 */ + pthread_barrier_destroy(&g_b1); + pthread_barrier_destroy(&g_b2); +} + +/* ---------------- T4:有界收尸 ---------------- */ +static void t4_reap_bounded(void) +{ + printf("== T4 收尸有界:挂住的子进程 SIGKILL 后 ≤700ms 收完 ==\n"); + pid_t pid = 0; + int fd = spawn_cmd("sleep 60", &pid); + CHECK(fd >= 0); + long long t0 = popen_tmo_now_ms(); + int ok = popen_tmo_reap_bounded(pid); + long long el = popen_tmo_now_ms() - t0; + close(fd); + printf(" [info] reap_bounded=%d 滞回=%lldms\n", ok, el); + CHECK(ok == 1); + CHECK(el < 700); + errno = 0; + CHECK(waitpid(-1, NULL, WNOHANG) == -1 && errno == ECHILD); /* 没有残留子进程 */ +} + +int main(void) +{ + setbuf(stdout, NULL); + build_slow_producer(); + t1_deadline_at_top(); + t2_hard_cap(); + t3_caller_buffer(); + t4_reap_bounded(); + if (g_fail) { printf("\nPOPEN_TMO TEST: %d FAIL\n", g_fail); return 1; } + printf("\nPOPEN_TMO TEST: ALL PASS\n"); + return 0; +} diff --git a/patch_v2/meow_netmgrd/meow_netmgrd.c b/patch_v2/meow_netmgrd/meow_netmgrd.c index 630befe..f83cfb3 100644 --- a/patch_v2/meow_netmgrd/meow_netmgrd.c +++ b/patch_v2/meow_netmgrd/meow_netmgrd.c @@ -114,6 +114,18 @@ static char g_net_policy[160]= "/data/meow/netmgrd/net_policy.conf"; /* MEOW_NET * 「熔断中」;任何非超时返回清零。范围只限 BT 写操作,不影响 WiFi/DNS 调用点。 */ #define BT_TMO_STREAK_MAX 3 #define BT_TMO_COOLDOWN_SEC 30 +/* M65 P1-3/P2-1:「附近设备」扫描(bt scan on/off)也纳入同一套有界收割 + 熔断 + 在飞计数。 + * 旧实现是 `system("... | bluetoothctl > cache &")`:不收割、不计数、不走熔断,bluetoothctl + * 一 wedge 就永久挂住单线程主循环并留下常驻子进程(M50 现场清过 55 个)。 */ +#define BT_SCAN_MIN_GAP_MS 7000 /* 两次 scan on 的最小间隔(**毫秒**;P2-1:旧实现拿秒值比 7000 → 实际 7000s) */ +#ifndef BT_SCAN_TMO_MS +#define BT_SCAN_TMO_MS 6500 /* 扫描窗口上限:pipeline 自带 ~5s(sleep 1 + dwell + 收尾) */ +#endif +#ifndef BT_SCAN_DWELL_S +#define BT_SCAN_DWELL_S 4 /* scan 时长;宿主单测缩窗用 */ +#endif +#define BT_SCAN_OFF_TMO_MS 600 /* `scan off` 单命令上限(同 BT_ACT_SHORT_MS 量级) */ +#define BT_SCAN_EXIT_TIMEOUT 124 /* scan worker 因超时退出的退出码(与 timeout(1) 同约定) */ #define IFACE_MAX 16 #define RFKILL_MAX 8 #define DNS_MAX 8 @@ -1437,24 +1449,147 @@ static void bt_devices_text(char *buf, int cap) /* 扫描生命周期(§七:进「附近查找」才 scan on;退页/超时 off)。默认 7s 节流。 * M49:改用 g_bluetoothctl(原来硬编码 "bluetoothctl",MEOW_BLUETOOTHCTL 桩对 scan 无效, - * 宿主测试没法覆盖扫描链路)。 */ -static long g_bt_scan_ms = 0; -static void bt_scan_on(void) + * 宿主测试没法覆盖扫描链路)。 + * M65 P1-3:scan 不再走 `system()`。 + * - 旧 `scan on` 是 `system("(…) | bluetoothctl > cache &")`:fork 出去的 pipeline 不收割、 + * 不计在飞数量、不走熔断;bluetoothctl wedge 一次就留一个常驻子进程(M50 现场 55 个的形态)。 + * - 旧 `scan off` 是**无超时** `system()`:守护是单线程 poll 循环,卡住即 DNS 代理/status 全停。 + * 现在:scan on 由本进程 fork 一个 scan worker(setsid 自成进程组),worker 内部用同文件的 + * run_cmd_ex(超时 + 输出上限)跑同一条 pipeline;父进程只记账,收割/超时强杀由 bt_scan_reap() + * 在主循环每拍完成。为什么不在主循环里直接 run_cmd_ex:pipeline 本身要 5s(sleep 1 + dwell), + * 同步跑等于每 7s 把 DNS 代理停 5s —— 那是另一种「挂死」,所以交给受管的 worker。 + * scan off 是快命令,直接在主循环里 run_cmd_ex(BT_SCAN_OFF_TMO_MS 上限)。 + * M65 P2-1:节流按**毫秒**比较(旧实现拿 mono_now() 的秒值比 7000 → 实际 7000 秒 ≈ 1.94 小时)。 */ +static long long g_bt_scan_ms = 0; /* 上次放行时刻(毫秒) */ +/* 64 位毫秒时钟:mono_ms() 是 long(mipsel ILP32 下 32 位,开机 ~24.8 天后溢出), + * 节流不该跟着溢出,故这里单独取一份 64 位时间;函数指针是**宿主单测注入可控时钟**的钩子 + * (test_bt_ctl.c 用它验 7s / 7.1s 边界 —— 秒/毫秒单位回归唯一能防住的地方)。 */ +static long long bt_scan_now_ms(void) { - long t = mono_now(); - if (g_bt_scan_ms && t - g_bt_scan_ms < 7000) return; - g_bt_scan_ms = t; - char cmd[320]; - snprintf(cmd, sizeof cmd, - "(sleep 1; echo 'scan on'; sleep 4; echo 'scan off'; echo 'devices'; echo 'quit') | " - "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s > '%s' 2>&1 &", g_bluetoothctl, g_bt_scan_cache); - if (system(cmd) != 0) svc_log("bt scan on: spawn failed (%s)", g_bt_scan_cache); + struct timespec tp; + clock_gettime(CLOCK_MONOTONIC, &tp); + return (long long)tp.tv_sec * 1000LL + tp.tv_nsec / 1000000LL; } -static void bt_scan_off(void) +static long long (*g_bt_scan_clock_ms)(void) = bt_scan_now_ms; +static pid_t g_bt_scan_pid = 0; /* 在飞 scan worker(0=空闲;上限 1 个) */ +static long long g_bt_scan_deadline = 0; /* worker 的最后收割时刻(毫秒) */ +static int bt_circuit_open(void); /* 定义在后(熔断器只服务 BT 写操作) */ +static void bt_circuit_note(runcmd_status_t st); +static int g_bt_tmo_streak; /* 熔断器状态,定义在后(仅用于日志) */ + +/* 节流判定(纯函数,便于宿主单测用可控时钟验单位):1=放行并记账,0=7s 内不再扫。 + * 入参**必须**是毫秒(P2-1)。 */ +static int bt_scan_throttle(long long now_ms) +{ + if (g_bt_scan_ms && now_ms - g_bt_scan_ms < BT_SCAN_MIN_GAP_MS) return 0; + g_bt_scan_ms = now_ms; + return 1; +} + +/* scan worker 主体(fork 出来的子进程):自成进程组,stdout/stderr 丢 /dev/null, + * 用 run_cmd_ex 跑 pipeline(超时/输出上限都在那里),超时以 BT_SCAN_EXIT_TIMEOUT 退出。 */ +static void bt_scan_worker(const char *cmd) +{ + setsid(); + int dn = open("/dev/null", O_WRONLY); + if (dn >= 0) { dup2(dn, 1); dup2(dn, 2); if (dn > 2) close(dn); } + /* 别攥着守护的监听 socket / DNS socket:否则守护重启时端口还被子进程占着。 */ + long mx = sysconf(_SC_OPEN_MAX); + if (mx < 0 || mx > 1024) mx = 1024; + for (int fd = 3; fd < (int)mx; fd++) close(fd); + char sink[64]; + runcmd_res_t rr; + run_cmd_ex(sink, sizeof sink, BT_SCAN_TMO_MS, cmd, &rr); + _exit(rr.status == RUNCMD_TIMEOUT ? BT_SCAN_EXIT_TIMEOUT : 0); +} + +/* 收割 scan worker(主循环每拍 + 每次 scan on 之前调用;守护单线程,无需加锁)。 + * 已退出 → 记账(超时进既有熔断);过 deadline 还活着 → 杀整个进程组再有界收尸 300ms; + * 仍不死(D 态)放弃收割但**不占槽位**(下次扫描不会被永久挡住),并入熔断计数。 */ +static void bt_scan_reap(void) +{ + if (g_bt_scan_pid <= 0) return; + int st = 0; + pid_t rp = waitpid(g_bt_scan_pid, &st, WNOHANG); + if (rp < 0) { + if (errno == EINTR) return; /* 信号打断:下一拍再来 */ + g_bt_scan_pid = 0; g_bt_scan_deadline = 0; /* ECHILD:已被收走 */ + return; + } + int timed_out = 0; + if (rp == 0) { + if (bt_scan_now_ms() < g_bt_scan_deadline) return; /* 还在窗口内,下一拍再看 */ + timed_out = 1; + /* 整组杀:worker + 它 fork 的 sh + pipeline 一起收,避免留下常驻 bluetoothctl。 */ + kill(-g_bt_scan_pid, SIGKILL); + kill(g_bt_scan_pid, SIGKILL); + for (int i = 0; i < 30; i++) { + if (waitpid(g_bt_scan_pid, &st, WNOHANG) == g_bt_scan_pid) { rp = g_bt_scan_pid; break; } + struct timespec ts = { 0, 10 * 1000000L }; + nanosleep(&ts, NULL); + } + if (rp != g_bt_scan_pid) { + svc_log("bt scan: worker %ld unkillable in 300ms (D-state?) — abandon reap", (long)g_bt_scan_pid); + g_bt_scan_pid = 0; + g_bt_scan_deadline = 0; + bt_circuit_note(RUNCMD_TIMEOUT); + return; + } + } + if (!timed_out && WIFEXITED(st) && WEXITSTATUS(st) == BT_SCAN_EXIT_TIMEOUT) timed_out = 1; + if (timed_out) { + /* worker 自己退出(exit 124)也**再扫一遍进程组**:pipeline 的 bluetoothctl 是孙进程 + * (sh 的子进程),run_cmd_ex 只杀它的直接子进程,孙进程会活到自己的 sleep 结束 —— + * 那正是「55 个残留 bluetoothctl」的另一半成因。 */ + kill(-g_bt_scan_pid, SIGKILL); + svc_log("bt scan: worker %ld timed out (streak=%d)", (long)g_bt_scan_pid, g_bt_tmo_streak + 1); + bt_circuit_note(RUNCMD_TIMEOUT); + } + g_bt_scan_pid = 0; + g_bt_scan_deadline = 0; +} + +/* 返回 1=已起扫描 / 0=节流或上一轮未完(跳过)/ -1=熔断或起不来。 */ +static int bt_scan_on(void) +{ + bt_scan_reap(); /* 先把上一轮收干净(含超时强杀) */ + if (g_bt_scan_pid > 0) return 0; /* 在飞上限 1:窗口没结束就不重复 fork */ + if (!bt_scan_throttle(g_bt_scan_clock_ms())) return 0; + if (bt_circuit_open()) { /* 连续超时:不再 fork,如实回熔断 */ + svc_log("bt scan on: 熔断中(连续 %d 次命令超时),跳过", g_bt_tmo_streak); + return -1; + } + char cmd[384]; + snprintf(cmd, sizeof cmd, + "(sleep 1; echo 'scan on'; sleep %d; echo 'scan off'; echo 'devices'; echo 'quit') | " + "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s > '%s' 2>&1", + BT_SCAN_DWELL_S, g_bluetoothctl, g_bt_scan_cache); + pid_t pid = fork(); + if (pid < 0) { svc_log("bt scan on: fork failed (%s)", strerror(errno)); return -1; } + if (pid == 0) bt_scan_worker(cmd); + g_bt_scan_pid = pid; + g_bt_scan_deadline = bt_scan_now_ms() + BT_SCAN_TMO_MS + 500; + return 1; +} + +/* 返回 0=后端已接受(或本来就没在扫)/ -1=超时或失败(已计入熔断)。 */ +static int bt_scan_off(void) { char cmd[192]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s scan off 2>/dev/null", g_bluetoothctl); - system(cmd); + char out[64]; + runcmd_res_t res; + run_cmd_ex(out, sizeof out, BT_SCAN_OFF_TMO_MS, cmd, &res); + if (res.status == RUNCMD_TIMEOUT) { + svc_log("bt scan off: 超时(%dms 未返回),已 SIGKILL 收割", BT_SCAN_OFF_TMO_MS); + bt_circuit_note(RUNCMD_TIMEOUT); + return -1; + } + if (res.status == RUNCMD_EXIT || res.status == RUNCMD_EXEC) { + svc_log("bt scan off: 后端失败(%s)", run_cmd_why(&res, NULL)); + return -1; + } + return 0; } /* ---------------- rfkill:统一软开关 ---------------- */ @@ -3798,9 +3933,15 @@ static void bt_ctl_execute(const char *cmd, const char *arg, ctl_result_t *r) if (!arg || !arg[0]) { char m[48]; snprintf(m, sizeof m, "bt %s: 需 ", cmd); ctl_sets(r, MEOW_ERR_INVAL, m); return; } bt_write_execute(cmd, arg, r); } else if (!strcmp(cmd, "scan")) { - if (!arg || !strcmp(arg, "on")) { bt_scan_on(); ctl_sets(r, MEOW_OK, "bt scan on: 后台节流扫描已触发"); } - else if (!strcmp(arg, "off")) { bt_scan_off(); ctl_sets(r, MEOW_OK, "bt scan off: 扫描已停"); } - else ctl_sets(r, MEOW_ERR_INVAL, "bt scan: 用法 on|off"); + if (!arg || !strcmp(arg, "on")) { + int rc = bt_scan_on(); + if (rc > 0) ctl_sets(r, MEOW_OK, "bt scan on: 后台节流扫描已触发"); + else if (rc == 0) ctl_sets(r, MEOW_OK, "bt scan on: 节流中或上一轮扫描未结束,本轮跳过"); + else ctl_sets(r, MEOW_ERR_NODEV, "bt scan on: 蓝牙栈熔断/无法起扫描"); + } else if (!strcmp(arg, "off")) { + if (bt_scan_off() == 0) ctl_sets(r, MEOW_OK, "bt scan off: 扫描已停"); + else ctl_sets(r, MEOW_ERR_NODEV, "bt scan off: 后端超时或失败(已强制收割)"); + } else ctl_sets(r, MEOW_ERR_INVAL, "bt scan: 用法 on|off"); } else { ctl_sets(r, MEOW_ERR_INVAL, "bt: 未知子命令"); } @@ -4183,6 +4324,7 @@ static int run_daemon(const char *sock) daemon_tick(); next_tick = now + g_poll_sec; } + bt_scan_reap(); /* M65 P1-3:每拍收割「附近设备」扫描 worker(超时整组强杀) */ int wait_ms = (int)((next_tick - mono_now()) * 1000); if (wait_ms < 50) wait_ms = 50; if (wait_ms > g_poll_sec * 1000) wait_ms = g_poll_sec * 1000; diff --git a/patch_v2/meow_netmgrd/test_bt_ctl.c b/patch_v2/meow_netmgrd/test_bt_ctl.c index f4ae9c3..429b851 100644 --- a/patch_v2/meow_netmgrd/test_bt_ctl.c +++ b/patch_v2/meow_netmgrd/test_bt_ctl.c @@ -11,6 +11,10 @@ * * 用法:cc -Wall -Wextra -I../meow_launcherd test_bt_ctl.c -o test_bt_ctl && ./test_bt_ctl */ +/* M65:扫描窗口在单测里缩短 —— pipeline 的 dwell 取 0(整条 ~1s 结束),超时压到 1200ms。 + * 这样既能验「超时强杀 + 不漏孙进程」,又不用为每个用例等生产的 6.5s。 */ +#define BT_SCAN_TMO_MS 1200 +#define BT_SCAN_DWELL_S 0 #define main meow_netmgrd_prod_main_unused #include "meow_netmgrd.c" #undef main @@ -31,6 +35,37 @@ static void wf(const char *p, const char *s) if (f) { fputs(s, f); fclose(f); } } +/* ---- M65 单测辅助:可控时钟 + 在飞 scan worker 的确定性收尾 ---- */ +static long long g_fake_now_ms = 0; +static long long fake_clock_ms(void) { return g_fake_now_ms; } + +/* 立刻收掉在飞 scan worker(整组杀 + 阻塞收尸):单测要确定性,不等生产的 ~5s 窗口。 */ +static void kill_scan_worker(void) +{ + if (g_bt_scan_pid > 0) { + kill(-g_bt_scan_pid, SIGKILL); + kill(g_bt_scan_pid, SIGKILL); + int st = 0; + while (waitpid(g_bt_scan_pid, &st, 0) < 0 && errno == EINTR) { } + } + g_bt_scan_pid = 0; + g_bt_scan_deadline = 0; +} + +/* 等 worker 由 bt_scan_reap() 自己收完(超时/正常都算),返回耗时 ms;超时返回 -1。 */ +static long drain_scan_worker(int max_ms) +{ + long t0 = (long)bt_scan_now_ms(); + while (g_bt_scan_pid > 0) { + bt_scan_reap(); + if (g_bt_scan_pid <= 0) break; + if ((long)bt_scan_now_ms() - t0 > max_ms) return -1; + struct timespec ts = { 0, 20 * 1000000L }; + nanosleep(&ts, NULL); + } + return (long)bt_scan_now_ms() - t0; +} + /* ---- 真机原始字节样本(含 ANSI ESC / \001\002 标记 / \r\033[K / 提示符) ---- */ static const char *S_NEW_PUDDING = "\033[0;94m[bluetooth]\033[0m# \r\033[K" @@ -113,6 +148,9 @@ int main(void) "#!/bin/sh\n" "S='%s/state'\n" "catf() { [ -f \"$1\" ] || return 0; while IFS= read -r l || [ -n \"$l\" ]; do echo \"$l\"; done < \"$1\"; }\n" + /* M65:scan 的 wedge 桩。交互形态(无参数,scan pipeline 喂 stdin)与 `scan off` + * 两种入口都要挂住;用 exec 让 sleep 就是那个进程本身(杀掉父 shell 不会留孤儿)。 */ + "if [ $# -eq 0 ] && [ -f \"$S/hang_scan\" ]; then exec sleep 60; fi\n" "case \"$1\" in\n" " paired-devices) catf \"$S/paired_list.txt\"; exit 0;;\n" " devices) catf \"$S/dev_list.txt\"; exit 0;;\n" @@ -146,6 +184,12 @@ int main(void) " echo yes > \"$S/paired_$2\"; echo 'Pairing successful'; exit 0;;\n" " remove)\n" " echo gone > \"$S/paired_$2\"; echo no > \"$S/conn_$2\"; echo 'Device has been removed'; exit 0;;\n" + /* M65:scan 的 wedge 桩 —— 真的挂住(exec 让 sleep 就是那个进程本身,模拟 bluetoothctl + * 卡在 HCI/D-Bus 里;若改成子进程 sleep,杀父进程会留下孤儿,断言就不诚实了)。 */ + " scan)\n" + " if [ -f \"$S/hang_scan\" ]; then exec sleep 60; fi\n" + " if [ \"$2\" = on ]; then echo 'Discovery started'; else echo 'Discovery stopped'; fi\n" + " exit 0;;\n" "esac\n" "exit 0\n", T); wf(p, sh); chmod(p, 0755); @@ -801,6 +845,99 @@ int main(void) CHECK(strstr(r.text, "scan on") != NULL); bt_ctl_execute("scan", "off", &r); CHECK(r.code == MEOW_OK); + kill_scan_worker(); /* 1s 窗口不必等,直接收 */ + + /* ================= M65 P2-1:扫描节流单位(7s / 7.1s;可控时钟) ================= */ + /* 回归防线:旧实现 `mono_now()`(秒)比 `7000`(毫秒)→ 实际 7000 秒 ≈ 1.94 小时一次。 + * 两个入口都验:①纯判定 bt_scan_throttle(入参毫秒)②真实入口 bt_scan_on(只认 + * g_bt_scan_clock_ms 钩子 —— 若有人把时钟改回 mono_now(),钩子失效,这里的 +6999 用例 + * 会看到真实时间几乎没动 → 判「放行」→ 断言失败)。 */ + printf("== M65 P2-1: bt scan 节流按毫秒(7s 放行 / 6.999s 拒 / 7.1s 放行) ==\n"); + { + g_bt_scan_ms = 0; + g_bt_scan_pid = 0; g_bt_scan_deadline = 0; + g_bt_tmo_streak = 0; g_bt_tmo_until = 0; + + CHECK(bt_scan_throttle(1000000) == 1); /* 首次:放行 */ + CHECK(bt_scan_throttle(1000000 + 6999) == 0); /* 6.999s:拒 */ + CHECK(bt_scan_throttle(1000000 + 7000) == 1); /* 7.000s:放行(秒混毫秒会等到 7000s) */ + CHECK(bt_scan_throttle(1000000 + 7000 + 99) == 0); /* 0.099s:拒 */ + CHECK(bt_scan_throttle(1000000 + 7000 + 7100) == 1); /* 7.1s:放行 */ + + /* 真实入口:时钟走钩子 */ + g_bt_scan_clock_ms = fake_clock_ms; + g_bt_scan_ms = 0; g_bt_scan_pid = 0; g_bt_scan_deadline = 0; + g_fake_now_ms = 3000000; + CHECK(bt_scan_on() == 1); /* t=3.000s:放行并起 worker */ + kill_scan_worker(); + g_fake_now_ms = 3000000 + 6999; + CHECK(bt_scan_on() == 0); /* +6.999s:节流(不起 worker) */ + CHECK(g_bt_scan_pid <= 0); + g_fake_now_ms = 3000000 + 7000; + CHECK(bt_scan_on() == 1); /* +7.000s:放行 */ + kill_scan_worker(); + g_fake_now_ms = 3000000 + 7000 + 7100; + CHECK(bt_scan_on() == 1); /* +7.1s:再放行 */ + kill_scan_worker(); + g_bt_scan_clock_ms = bt_scan_now_ms; /* 复原真时钟 */ + g_bt_scan_ms = 0; + } + + /* ================= M65 P1-3:scan 有界收割(不挂死、不漏子进程) ================= */ + printf("== M65 P1-3: scan on/off 对「永不返回」的 bluetoothctl 有界收割、不漏子进程 ==\n"); + { + snprintf(p, sizeof p, "%s/state/hang_scan", T); wf(p, "1\n"); /* 桩:scan → exec sleep 60 */ + g_bt_scan_ms = 0; g_bt_scan_pid = 0; g_bt_scan_deadline = 0; + g_bt_tmo_streak = 0; g_bt_tmo_until = 0; + + /* ① scan off 是同步路径:旧实现 system() 会永远阻塞在这个 wedge 上。 */ + long t0 = (long)bt_scan_now_ms(); + int rc = bt_scan_off(); + long el = (long)bt_scan_now_ms() - t0; + printf(" [info] bt_scan_off 滞回 %ldms (上限 %dms)\n", el, BT_SCAN_OFF_TMO_MS); + CHECK(rc == -1); /* 如实报失败,不假成功 */ + CHECK(el >= BT_SCAN_OFF_TMO_MS - 50); /* 确实等到了超时点 */ + CHECK(el < BT_SCAN_OFF_TMO_MS + 1200); /* 但有界(旧 system() 无界) */ + { int st2 = 0; errno = 0; + CHECK(waitpid(-1, &st2, WNOHANG) == -1 && errno == ECHILD); } /* 无残留子进程 */ + + /* ② scan on:worker 在飞,主循环不被阻塞;过 deadline 整组强杀(含 pipeline 孙进程)。 */ + g_bt_scan_ms = 0; + t0 = (long)bt_scan_now_ms(); + CHECK(bt_scan_on() == 1); + long fork_el = (long)bt_scan_now_ms() - t0; + CHECK(fork_el < 300); /* 起扫描本身绝不阻塞 */ + CHECK(g_bt_scan_pid > 0); + CHECK(bt_scan_on() == 0); /* 在飞上限 1:不重复 fork */ + long dr = drain_scan_worker(BT_SCAN_TMO_MS + 3000); + el = (long)bt_scan_now_ms() - t0; /* 从起扫描算起,含 fork + 窗口 */ + printf(" [info] scan worker 从起扫描到收割 %ldms(drain 轮询 %ldms;窗口 %dms)\n", + el, dr, BT_SCAN_TMO_MS); + CHECK(dr >= 0); /* 有界收完(没有永久卡住) */ + CHECK(el >= BT_SCAN_TMO_MS); /* 是「等到超时才收」而不是立刻杀 */ + CHECK(el < BT_SCAN_TMO_MS + 2500); + CHECK(g_bt_scan_pid == 0); /* 槽位已释放 */ + CHECK(g_bt_tmo_streak >= 1); /* 超时计入既有熔断计数 */ + { int st2 = 0; errno = 0; + CHECK(waitpid(-1, &st2, WNOHANG) == -1 && errno == ECHILD); } /* 孙进程也被整组收掉 */ + + /* ③ 熔断联动:再来一次超时就把 streak 顶到上限 → bt_circuit_open(),不再 fork。 */ + g_bt_tmo_streak = BT_TMO_STREAK_MAX - 1; + g_bt_tmo_until = 0; + g_bt_scan_ms = 0; + CHECK(bt_scan_on() == 1); + CHECK(drain_scan_worker(BT_SCAN_TMO_MS + 3000) >= 0); + CHECK(g_bt_tmo_streak >= BT_TMO_STREAK_MAX); + CHECK(bt_circuit_open() == 1); + g_bt_scan_ms = 0; + CHECK(bt_scan_on() == -1); /* 熔断中:不再 fork(不烧进程槽) */ + CHECK(g_bt_scan_pid <= 0); + + /* 复原:摘掉 wedge 钩子,熔断器清零 */ + { char ub[320]; snprintf(ub, sizeof ub, "rm -f '%s/state/hang_scan'", T); (void)!system(ub); } + g_bt_tmo_streak = 0; g_bt_tmo_until = 0; g_bt_scan_ms = 0; + bt_scan_off(); /* 桩恢复正常:现在能返回 0 */ + } printf("== bt_write_authorize(系统身份 + MeowOS 才放行写级) ==\n"); caller_t other; memset(&other, 0, sizeof other); other.kind = CALL_OTHER;