Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/test_popen_tmo.c
T
AskaEth 1ccb3c7a93 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。
2026-11-12 09:02:07 +08:00

341 lines
13 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <pthread.h>
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;
}