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
@@ -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 <poll.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <signal.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
/* 输出硬上限: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 */
|
||||
Reference in new issue
Block a user