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:
1 parent
1936cf7b91
commit
1ccb3c7a93
7 files changed
+839
-86
No files matched your search
@@ -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: 需 <mac>", 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;
|
||||
|
||||
Reference in new issue
Block a user