diff --git a/.task/reviews/2026-10-20-guard-test-blindspot-closure-review.md b/.task/reviews/2026-10-20-guard-test-blindspot-closure-review.md new file mode 100644 index 00000000..925f590c --- /dev/null +++ b/.task/reviews/2026-10-20-guard-test-blindspot-closure-review.md @@ -0,0 +1,91 @@ +# 收口复审:fix/guard-test-blindspot-coverage(B1/B2 修复后) + +- 审查对象:`70c49470a6fdc0bb8bbe7339dafdc271b981d40a..HEAD`(HEAD=`fcdf57fdfc89041194c7ebd773ae8c27a12d5662`,经 `git rev-parse` 确认与分支 `fix/guard-test-blindspot-coverage` 尖端一致)。范围即单个提交 fcdf57f,`git diff --stat` 显示仅改 `tests/test_shell_pipe_hazard.py`(+195/−26);四处脚本改写与其余已闭合结论所涉文件不在本范围内 +- 前置报告:`.task/reviews/2026-10-20-guard-test-blindspot-final.md`(BLOCKED,B1=`until ` 条件终止、B2=循环体 `exit` 含 select) +- 审查身份:全新独立会话,未参与实现;只读审查,除本报告外未修改任何文件;合成脚本与探针全部位于 /tmp,仓库内无残留 +- 审查日期:2026-10-21 + +## 结论:PASS + +上一份报告的两个阻塞项均已闭合且运行时实证 141;发现机制已按非阻塞建议 N2 扩到无扩展名托管脚本与 docker/*.sh(21 个受管脚本,0 解析错误、0 命中,ignored 陈旧克隆仍不可能入扫描);READER_FAMILIES 声明与实现一致、逐族自检仍在且 while 族扩为 4 条用例;放宽后对受管脚本无新误报/新漏检(守护测试全量 12/12 通过);awk 引号串误报已消除、mapfile 词序豁免有据注释。残留 4 项均为声明外/声明豁免面的非阻塞观察(见下),依本轮可证伪边界不构成阻塞。 + +--- + +## 重点核验(按本轮 5 项) + +### 1. 上轮阻塞项闭合 — 通过(检测器报出 + 运行时 141 独立实证) + +`_lex` 新增 `exit_out()`(循环帧链上命令位 `exit` 置 `broken`,遇 `)`/`$(` 帧即停——子壳内 exit 不外溢,与运行时一致)、`_until_condition_reads()`(`do` 时捕获 `found[cond_start:]` 为条件列表,`until` 条件含声明族读者即报,首级以 `!` 开头的列表跳过)、`LOOPS` 扩入 `select`。/tmp 独立探针(不复制套件断言;`set -euo pipefail` 下运行): + +| 形态(`seq 1 200000 | `) | 检测器 | 运行时 | +|---|---|---| +| `until read -r l; do :; done`(B1 复测) | 报 | 141 | +| `until IFS= read -rt 1 l; do :; done` | 报 | 141 | +| `until (read -r l); do :; done`(新:条件读者在子壳内) | 报 | 141 | +| `until /usr/bin/head -1; do :; done`(新:条件读者绝对路径) | 报 | 141 | +| `until timeout 1 head -1; do :; done`(新:条件读者经包装) | 报 | 141 | +| `until true && read -r l; do :; done`(新:读者在条件后继列表) | 报 | 141 | +| `while read -r l; do exit; done` 与条件 `[ … ] && exit`(B2 复测) | 报 | 141 | +| `select x in a b; do exit; done`(新:select 体无条件 exit) | 报 | 141 | +| `while read -r l; do for x in a; do exit; done; done`(新:for 嵌套内 exit) | 报 | 141 | +| `while read -r l; do { echo "$l"; exit; }; done`(新) | 报 | 141 | +| `while read -r l; do case $l in *) exit ;; esac; done`(新) | 报 | 141 | + +反例不报且正确:`(exit 1)`、`x=$(exit 1)`、`case` 体 `echo exit` 均不报且生产者 PIPESTATUS[0]=0(读满,无 SIGPIPE)——子壳/命令替换内 exit 本就不出循环,判定精确。`while` 条件含读者(`while read`/`while grep -q 1`)与 `until ! read`/`until ! grep -q 1` 不报且读满(生产者 0),与声明「条件失败即 EOF 才结束」「! 取反除外」一致。 + +### 2. 发现机制 — 通过(N2 闭合,0 解析错误、0 命中) + +- `SCRIPT_GLOBS` → `SCRIPT_DIRS = ("scripts", "docker")` + `_is_shell_script`(`.sh` 直纳,否则按首行 `#!` 的 `\b(?:ba|da|k|z)?sh\b` 判定)。实测 `git ls-files` 目录级 34 个跟踪文件,shebang 过滤排除 13 个非 shell(`docker/Dockerfile`、`docker/README.md`、9 个 `*.py`(`#!/usr/bin/env python3`)、2 个 `*.go`),得 **21 个受管脚本**,含上轮缺口 `scripts/task`、`scripts/dm`、`scripts/agent-exec`、`docker/entrypoint.sh`;`docker/verify-env.sh`、`tests/load/collaboration/run_pgbench.sh` 仍在。 +- 逐个跑 `_lex` + `hazards`:**0 解析错误、0 命中**(与 `test_managed_scripts_have_no_early_exit_pipe_reader` 一致);`EXEMPT={}` 且过期条目守卫仍在。 +- ignored 陈旧克隆仍不入扫描:发现只经 `git ls-files`(只列跟踪文件),`.gitignore:9 runtime/`(:10 `runtime-backups/`)经 `git check-ignore` 复核;本 worktree 无 runtime/ 目录,测试仍断言无 `runtime/` 前缀。 + +### 3. READER_FAMILIES 声明 ↔ 实现一致 — 通过 + +- 循环族声明扩为「while/until/select 循环:循环内命令位的 break 出本循环、命令位的 exit(子壳内除外)、until 条件含上列读取器(! 取反除外)」,与 `LOOPS`、`break_out`、`exit_out`(`)`/`$(` 截断)、`_until_condition_reads`(`!` 跳过)逐条吻合;否定面声明(`while` 条件读者读满、`mapfile` 无 `-n` 读满)也与实现一致。 +- 前缀/包装清单:`PREFIXES={!,time,command,builtin,env,exec}` + 新 `WRAPPERS={nohup,timeout(−s/−k/−−signal/−−kill-after 带参、1 个时长操作数),nice(−n/−−adjustment),stdbuf(−i/−o/−e/…)}`,与声明「!/time/command/builtin/env/exec 前缀、nohup/timeout/nice/stdbuf 包装命令及其选项与 timeout 时长」一致;`_skip_wrapper_args` 在首操作数处停。新写法探针:`nohup timeout 30 head -1`(包装嵌套)、`timeout --signal=KILL 5 head -1`、`nice -n 5 /usr/bin/head -1` 均报且 141。 +- `test_declared_reader_families_each_flagged` 仍是逐族自检且加严:while 族扩为 4 条用例(break/exit/until-read/select-break)逐条 `subTest` 断言命中,缺族/空字段即失败;套件实测通过。 + +### 4. 放宽后无新误报/新漏检(受管脚本面) — 通过 + +- 守护测试全量:`PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_shell_pipe_hazard -v` → **Ran 12 tests, OK**(上轮 11 个 + 新增 `test_loop_family_and_wrappers_flagged_and_harmful`,后者对 until 条件读取/循环体 exit/select break/四种包装逐条断言命中 **且** bash 运行时 141,对 7 个读满写法断言 rc=0 且检测器不报)。 +- 受管脚本扫描(含新纳入 4 个):21 个全部 0 命中、0 解析错误,无新误报;新族内写法对受管脚本的漏检未发现(本轮 11 个新探针全报出)。 +- 范围内未触碰 scripts/ 与 docker/(diff 仅测试文件),上轮「四处改写语义等价」「已闭合族」结论无需重查、未发现失效迹象。 + +### 5. awk 引号串误报消除、mapfile 词序豁免有据 — 通过 + +- N1 闭合:`_awk_exits_early` 先对单引号程序内 awk 字符串字面量 `AWK_STRING → ""` 再查 `exit|nextfile`。实测 `awk '{print "exit"}'`、`awk 'END{print "nextfile"}'` 不报且 rc=0;真危害 `awk '{print "exit"; exit}'`、`awk '{nextfile}'` 仍报且 141——只剥字符串、不放过语句。 +- N4 闭合:`_mapfile_exits_early` 首操作数截断处新增注释「bash 内建在首个操作数处停止选项解析:`mapfile arr -n 1` 的 -n 不生效、读满输入,不是提前退出」;实测 `mapfile arr -n 1` rc=0(读满),豁免有据且与运行时相符。 + +## 非阻塞观察(依可证伪边界,均在声明外或声明豁免面) + +- **O1**:`while read -r l; do : || exit; done` 被报(声明「命令位的 exit」按词法命中,含 `||` 后位),运行时 rc=0(`:` 恒真、exit 永不执行)——过度近似、安全方向;受管脚本无此写法(扫描 0 命中)。与上轮 awk 引号串同类,若未来想收,需区分 `&&`/`||` 条件语境。 +- **O2**:`until ! awk '{exit 1}'; do :; done` 不报、运行时 141——读者早退且非零退出,`!` 取反后条件首 iter 即成功、循环即止。声明明文「! 取反除外」,属声明豁免面的残留危害(构造性强),记录在案不改判定。 +- **O3**:`awk -v s="exit" '{print s}'` 仍误报(双引号 shell 参数中的 exit 不在 `SHELL_SQ` 剥离面),rc=0。上轮 N1 的残余同类面(上轮即未含此形),安全方向、受管脚本无触发。 +- **O4**:`seq 1 200000 | timeout 5`(无被包装命令的畸形用法)不报、rc=125——timeout 用法错误立即退出同样会使管道读取级消失,但声明语义是「透过包装命令找读者」,无读者可找;畸形用法,不属声明族。 +- **O5**(沿上轮 N3/N6/N7,本轮未重查亦未失效):算术 `$((…|…))` 拆级误报、族外读者(dd/jq/perl/xargs)、`sed -f`/`awk -f` 外部程序文件,均声明外。 + +## 可证伪性注入记录(约束要求) + +本轮独立注入 11 个上轮与套件均未覆盖的新形态(B1 条件读者 4 变体、B2/select/嵌套 exit 5 变体、包装 2 变体),**全部被报出且运行时 141**;另注入 6 个读满写法(含 `(exit 1)`、`x=$(exit 1)`、`until ! grep -q 1`),全部不报且生产者读满(PIPESTATUS[0]=0)。检测器对注入并非照单全收:声明外形态(O1 误报、O2/O4 漏报)如实暴露,失败集中在「`!` 豁免面」与「词法过度近似」两处,均为声明已划界处,不构成族内漏检。 + +## 已运行的验证命令与结果(均只读;合成物全部位于 /tmp) + +| # | 命令 | 结果 | +|---|---|---| +| 1 | `git rev-parse HEAD fix/guard-test-blindspot-coverage` | 二者同为 fcdf57fd…;`git log --oneline 70c4947..HEAD` 仅 fcdf57f;`git diff --stat` 范围内仅 `tests/test_shell_pipe_hazard.py` +195/−26 | +| 2 | `PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_shell_pipe_hazard -v` | **Ran 12 tests, OK** | +| 3 | `git ls-files -- scripts docker …`(旧 glob 与新目录两版对比)+ `_is_shell_script` 差集 | 旧 17 个 → 新 21 个;13 个非 shell(py/go/Dockerfile/README)被 shebang 过滤排除,首行内容逐一核对 | +| 4 | Python 遍历 21 个受管脚本 `_lex`+`hazards` | errors=[],findings=[] | +| 5 | `git check-ignore -v runtime/x`;`grep -n runtime .gitignore` | `.gitignore:9 runtime/`(:10 runtime-backups/);worktree 无 runtime/ 目录 | +| 6 | /tmp 探针矩阵(见第 1、3 节表)+ `set -euo pipefail` 运行时 | 11 危害形态全报且 141;`(exit 1)`/`x=$(exit 1)`/`select :`/`until ! grep -q 1`/`until ! read` 不报且生产者 0 | +| 7 | awk/mapfile/timeout 边界探针 | `{print "exit"}`、`END{print "nextfile"}` 不报 rc=0;`{print "exit"; exit}`、`{nextfile}` 报且 141;`mapfile arr -n 1` rc=0;`timeout 5` rc=125 不报(O4) | +| 8 | `git status`(收尾) | 除本报告外无任何修改 | + +第 6 项首轮执行时我的探针脚本误用 `set -uo pipefail`(缺 `-e`),管道失败后脚本继续走 `echo` 致基线全 0——判定探针自身缺陷后改用与套件一致的 `set -euo pipefail` 重跑,上表数据以重跑为准(如实记录,含失败)。 + +## 未验证事项 + +- 未运行 CI、未做 REPLAY(本审查单元不要求);全部结论基于本地只读命令与 /tmp 合成探针。 +- O2 的 `until ! <读者>` 面仅实测 awk(非零退出)与 `until ! head -1`(timeout 120s 观察到死循环、未等其自然终止);`!` 面其余读者组合未逐一实测。 +- O3 仅实测 `-v s="exit"` 一种双引号传参形;同类其它传参形未穷举。 +- 上轮已闭合项(B1/B2 之外的族内写法、四处脚本改写等价性)按收口范围未重查,仅确认本轮 diff 未触及对应文件与逻辑(grep/sed/head/read/mapfile 判定函数无改动,awk 仅加字符串剥离且经反例确认不回归)。