Skip to content

docs(hook-pitfalls): #33 基于 shlex 的守卫看见的是你打的字,不是 bash 构造的 argv - #299

Merged
daymade merged 1 commit into
mainfrom
docs/hook-pitfall-33-shell-expansions
Aug 16, 2026
Merged

docs(hook-pitfalls): #33 基于 shlex 的守卫看见的是你打的字,不是 bash 构造的 argv#299
daymade merged 1 commit into
mainfrom
docs/hook-pitfall-33-shell-expansions

Conversation

@daymade

@daymade daymade commented Aug 16, 2026

Copy link
Copy Markdown
Owner

#297 / #298。fresh-context 独立审阅在我已宣布守卫可靠之后,找出三条
零设置、日常语法的完全绕过——都让守卫的目标形态 git commit -a 对它隐形。

三条绕过(均已用 probe 脚本核过真实 argv)

打的字 bash 真实 argv
git commit {-a,-m} x commit -a -m x(花括号展开)
git commit $'-a' -m x commit -a -m x(ANSI-C 引用)
git${IFS:0:1}commit${IFS:0:1}-a git commit -a(IFS 当分隔符)

其中 {-a,-m} 被审阅者推到真 git 执行,真把未提交改动卷进了工作树。
三条都可回溯到守卫最早的提交,不是新引入的。

根因是范畴错误

shlex词法器:切词 + 处理引号。而 bash 在拿到 argv 之前还要做
花括号/波浪号/参数/命令替换/算术展开、词分割、路径名展开——shlex 一个都不做,
也从没声称要做。所以 {-a,-m} 作为一个以 { 开头的字面 token 到达,
永远进不了只对 - 开头 token 生效的旗标扫描器。

守卫按自己的口径没 bug,它只是在回答另一个问题
「用户打了什么」而不是「实际会跑什么」。

两条元教训(条目正文里的重点)

  1. 自己的绕过排查会被"刚修的那个 bug"塑形,因而恰好在下一个 bug 所在处最盲。
    我那轮对抗测试 26 条危险形态,全部围绕注释/重定向/续行/引号——
    零条用到 shell 展开,因为展开不在我当时脑子里的那个 bug 里。
  2. 「我们接受一部分漏报」是预算,会悄悄超支。 该守卫早已声明放弃变量间接
    $G commit -am),但那要求先写一条赋值语句;这三条零设置,
    且是人们出于非规避理由天天在打的语法(cp file.{txt,bak} 完全日常)。

附带修掉的一条误杀(Finding C)

N>&word 只有 word 整个是数字时才是 fd dup,否则是到文件的重定向
(实测 2>&1x 真在磁盘建出名为 1x 的文件)。原正则只写了纯数字那支,
于是 git commit -- 2>&1x 被误杀。

数字

  • 守卫套件 77 → 97 全绿
  • 双向标定(指向修复前版本):86 pass / 11 fail,红的恰好是 8 条绕过 + 3 条误杀,
    无一旁落;正控(cp file.{txt,bak} / awk '{print $1}' / find -exec {} \; /
    message 里写花括号是数据)在两版都绿——证明它们不是靠修复才通过的
  • 真实语料 A/B:27,641 条相关命令跑前后两版,退出码差异 0 条

给 Tier-0 闸门加归一化器却不给最后这个数字,就是无界的误杀风险——
而闸门上的误杀比刚补的那个洞更糟。

守卫本体修复在私有仓(daymade/scripts f0ff4a3)。

🤖 Generated with Claude Code

2026-08-16:fresh-context 独立审阅在我已宣布守卫可靠**之后**,找出三条零设置、
日常语法的完全绕过——都能让守卫的目标形态 `git commit -a` 对它隐形,
其中一条被推到真 git 执行、真把未提交改动卷进了工作树。

  git commit {-a,-m} x              → 实 argv = commit -a -m x(花括号展开)
  git commit $'-a' -m x             → 实 argv = commit -a -m x(ANSI-C 引用)
  git${IFS:0:1}commit${IFS:0:1}-a   → 实 argv = git commit -a(IFS 当分隔符)

根因是一个范畴错误:shlex 是**词法器**,切词 + 处理引号;而 bash 在拿到 argv 之前
还要做花括号/波浪号/参数/命令替换/算术展开、词分割、路径名展开。shlex 一个都不做、
也从没声称要做。守卫按自己的口径没 bug,它只是在回答另一个问题——
「用户打了什么」而不是「实际会跑什么」。

条目里给了 5 条修法,以及两条更该记住的元教训:

① **自己的绕过排查会被"刚修的那个 bug"塑形,因而恰好在下一个 bug 所在处最盲。**
   我自己那轮对抗测试 26 条危险形态,全部围绕注释/重定向/续行/引号——
   没有一条用到 shell 展开,因为展开不在我当时脑子里的那个 bug 里。
   拿到「找任何绕过」这句话的 fresh-context 审阅者没有这个锚,就找到了。

② **「我们接受一部分漏报」是预算,会悄悄超支。** 该守卫早已声明放弃变量间接
   (`$G commit -am`),但那要求先写一条赋值语句;这三条零设置、且是人们
   出于非规避理由天天在打的语法(`cp file.{txt,bak}` 完全日常)。
   声明的边界很窄,实际的洞很宽。

修法要点:不必实现 bash 展开(那是 shfmt 级的活),只要让危险旗标别再被藏住——
分词前在原始文本、且只在引号外把 `{a,b}` 归一成 ` a b `、`$'x'` 成 `x`、
`${IFS…}` 成空格即可;花括号规则须限定"含逗号且不含空白",否则会吃掉
`find -exec {} \;` 与 `awk '{print $1}'`。

以及必须量:真实语料 27,641 条相关命令跑前后两版,**退出码差异 0 条**——
三条完全绕过被堵死,对真实行为零影响。给 Tier-0 闸门加归一化器却不给这个数字,
就是无界的误杀风险,而闸门上的误杀比你刚补的那个洞更糟。

守卫本体的修复见私有仓 daymade/scripts f0ff4a3(套件 77→97;双向标定对修复前版本
86-11,红的恰好是 8 条绕过 + 3 条误杀,正控在两版都绿)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@daymade
daymade merged commit a5023d6 into main Aug 16, 2026
4 checks passed
@daymade
daymade deleted the docs/hook-pitfall-33-shell-expansions branch August 16, 2026 04:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant