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

+38 -42
View File
@@ -59,6 +59,7 @@
#include <sys/un.h>
#include <sys/wait.h>
#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=<path>):把每次 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) {