JianFeeeee
e99a657143
修复: rc=2 那半个缝(降权构造 blind/unreadable 的上游锚点);顺带修掉它暴露出的**真 bug**:sha256sum 不在 PATH 时 UnboundLocalError 崩在打出 RESULT 之前
pi 2026-09-18 报的 M18。我复现了它,补锚点时又**意外撞出一个提交态就有的真 bug**(不是 pi 报的、也不是我预想的)。
## 一、M18 复现:rc=2 只有下游锚点、没有上游锚点
`--exitcode-selftest` 原来四例期望只覆盖 **0/1**(实测读出 `, 0,` `, 1,` `, 1,` `, 0,`)。
把 `summary.py` 的 `if blind or unreadable: return 2` 改成 `return 0`:
| 自检 | rc | 该不该响 |
|---|---|---|
| `--exitcode-selftest`(上游) | **0(绿)** | **该红** |
| `--mutants-line-selftest`(下游) | **0(绿)** | 不该(它喂合成 status) |
真跑 blind 确认后果:`nobody + chmod 000 jobs/` ⇒ 变异版 **rc=0**(应 2)
⇒ "环境:没读数"整条链又断回"看着正常",**两个自检都不响**。与 pi 逐字一致。
★ 根因是**结构性障碍**,不是疏忽:`summary.py` 用 `os.access(JOBS_DIR, R_OK|X_OK)` 判 blind,
而 **`os.access` 对 uid=0 恒为 True**(实测:`chmod 000` 后 root `access()==True`、nobody 才 False)
—— 与我早先在 `check-file-modes.sh` 踩的 `[ -x ]` 是**同一个 root 陷阱**,只是换成了 `os.access`。
所以自检以 root 跑时 blind 分支**物理上不可达**。
## 二、补法:降权构造前提(三格都有锚点:0 / 1 / **2**)
`runIn()` 加降权模式:临时目录整棵放开到 nobody 可达 → 按场景收紧
(`blind`:`chmod 000 jobs/`;`unreadable`:目录 755 + 单文件 000)→ 用
`runuser -u nobody`(无则退 `setpriv --reuid=65534`)跑**真脚本**。两条路我都实测得 rc=2。
新增两条案例:`盲读⇒2`、`单文件读不到⇒2`。
★ **前提构造不出来时**(拿不到降权工具)报 **RED 并明说"本机覆盖不到、这不是通过是没有锚点"** ——
照 pi 那句"哪条前提构造不出来就写明,别让它默认看起来是被守着的"。不静默跳过。
**变异验证**(都已还原):
· **M18**(`blind ⇒ return 0`)⇒ 两条降权案例**都红**、exit 1(改前两个自检全绿)。
· **M19**(只把 `unreadable` 那一半改成 0)⇒ **只有**"单文件读不到"那条红 ——
证明两条锚点是**独立**的,不是互相冗余。
## 三★★ 补锚点时撞出的**真 bug**(提交态就有,不是本笔引入)
构造"前提 B(单文件不可读)"时跑出 `UnboundLocalError: cannot access local variable 'r'`:
```python
try:
r = subprocess.run(['sha256sum', '-c', bl], ...) # ← 在 try 内绑定
...
except Exception:
baseline_ok = (False, -1, -1) # ← 设了非 None
...
else: # baseline_ok 非 None ⇒ 走这里
detail = [... for ln in (r.stdout or '').splitlines() ...] # ← r 从未绑定 ⇒ 崩
```
崩点在**打出 `RESULT` 行之前** ⇒ 上游只看到"没打出 RESULT",而真因(一个外部命令缺失)
埋在 traceback 里。**触发条件很实在**:`sha256sum` 不在 PATH(精简镜像 / PATH 被裁的 CI)。
实测:正常 rc=0;`PATH` 里去掉 `sha256sum` ⇒ `UnboundLocalError`、rc=1。
⇒ 两件事一起修:
① `r` 不再依赖 try 内绑定;
② 把"**跑不了这项检查**"与"**检查了、没过**"**分成两种状态**(合并正是这个 bug 的成因)——
现在那一格打 `baseline=(**跑不了 sha256sum 校验**:FileNotFoundError: ...)—— 这一格**不是**"底本对"也**不是**"有残留",是**没读数**`。
并补第 7 条锚点:**故意让 PATH 里没有 `sha256sum`**,要求 ① 不崩 ② 仍打出 `RESULT` ③ 那格说"跑不了"。
**M20**(把 `r` 绑定退回 try 内)⇒ 该案例红、exit 1,报的正是那个 `UnboundLocalError`。
## 四、自检自身的两个问题(照实记)
1. **`--exitcode-selftest` 清理漏了**:清理原来是顺序执行的最后两步,`runIn` 中途抛错就跳过 ——
实测 `/tmp` 里留下 `exitcode-*`。改成 `try/finally`(失败正是最常被重跑的路径),
并加了"抛错时也清理"的验证(模拟中途 throw ⇒ 残留 0)。
2. **PATH 一裁连解释器都找不到**:`no-sha256sum` 那条如果不把 `python3` 解析成绝对路径,
会变成"脚本没跑起来"(退 1),而 **1 又是别的案例的期望值** ⇒ 又一次"没跑起来长得像通过"。
已先解析绝对路径。
## 五、验证与状态
· 7 条锚点全绿:一致⇒0 / unlisted⇒1 / ghosts⇒1 / 说明条目⇒0 / **盲读⇒2** / **单文件⇒2** / **无 sha256sum⇒不崩**。
· 四个自检全 exit 0:`--mutants-line-selftest` 8 例、`--exitcode-selftest` **7 例**、`--skip-selftest` 5 例、`--probe-selftest` 3 例。
· 全套件 `checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0`(与改动前**同样 9 条**)、`mutants=48 ran=47 skipped=1 on_new_criteria=35`。
· 变异:M18 / M19 / M20 全部按预期红并已还原;`/tmp` 无 `exitcode-*`/`barepath-*` 残留。
2026-09-18 05:40:38 +08:00
..
2026-09-14 15:41:52 +08:00
2026-09-15 12:09:37 +08:00
2026-09-18 05:05:38 +08:00
2026-09-15 07:46:54 +08:00
2026-09-18 05:40:38 +08:00
2026-09-14 16:21:27 +08:00
2026-09-15 11:17:23 +08:00
2026-09-17 19:00:11 +08:00
2026-09-14 16:59:16 +08:00
2026-09-17 22:32:25 +08:00
2026-09-14 17:33:05 +08:00
2026-09-15 12:55:25 +08:00
2026-09-18 04:54:59 +08:00
2026-09-14 17:36:02 +08:00
2026-09-16 08:06:36 +08:00
2026-09-15 12:22:23 +08:00
2026-09-17 17:43:03 +08:00
2026-09-17 18:43:00 +08:00
2026-09-17 18:44:36 +08:00
2026-09-15 12:06:15 +08:00
2026-09-15 13:46:56 +08:00
2026-09-15 12:10:49 +08:00
2026-09-15 12:06:15 +08:00
2026-09-17 18:44:36 +08:00
2026-09-18 05:05:38 +08:00
2026-09-15 12:06:15 +08:00
2026-09-16 08:29:24 +08:00
2026-09-17 19:38:15 +08:00
2026-09-17 18:43:00 +08:00
2026-09-14 16:21:27 +08:00
2026-09-17 18:44:36 +08:00
2026-09-14 15:11:47 +08:00
2026-09-15 08:45:54 +08:00
2026-09-15 07:46:54 +08:00
2026-09-14 17:33:05 +08:00
2026-09-18 05:40:38 +08:00
2026-09-18 01:08:04 +08:00