/* 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 #include #include #include #include #include #include #include /* 输出硬上限: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 */