Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/popen_tmo.h
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

134 lines
6.0 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.
/* 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 */