/* 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 #include #include #include #include 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; }