fix(daemon): M65 P1 三条 + P2-1 — popen 超时/共享缓冲、bt scan 有界收割与毫秒节流

来自 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。
This commit is contained in:
AskaEth committed 2026-11-12 09:02:07 +08:00
1 parent 1936cf7b91
commit 1ccb3c7a93
7 files changed
+839 -86

No files matched your search

+137
View File
@@ -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;