Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/.size-baseline
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
5942 qwen-autofix-fork-signal.yml
397656 qwen-autofix.yml
7061 qwen-ci-flaky-rerun.yml
151937 qwen-code-pr-review.yml
157847 qwen-code-pr-review.yml
79041 qwen-fleet-shepherd.yml
20525 qwen-issue-followup-bot.yml
5760 qwen-pr-safety-precheck.yml
Expand Down
86 changes: 85 additions & 1 deletion .github/workflows/qwen-code-pr-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1842,6 +1842,20 @@ jobs:
# the next job on this reused runner can delete qwen-review/* branches.
# The sweep deletes all review artifacts, not just this PR's: safe because
# a runner executes one job at a time.
#
# The removal owns its own permission repair. A containerised job on this
# shared pool can leave a review worktree owned by another uid and
# read-only (measured, run 32577821716 / PR #9718: a leftover
# scratch-verify tree held files this job's user could not unlink, and
# the NEXT review's checkout died on them with EACCES — both the
# pre-checkout ownership restore and the checkout's own wipe degraded
# because the runner had no passwordless sudo). A removal that gives up
# on the first EACCES re-poisons the next job, so a failed rm gets a
# repair ladder instead: chmod what this user owns, then passwordless
# sudo chown/chmod where the pool member has it, each followed by a
# retry. Members without sudo still degrade to a named warning —
Comment on lines +1854 to +1856

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The comment says each rung is "each followed by a retry", but remove_review_tree retries rm -rf exactly once, after ALL rungs — there is no retry between the chmod rung and the sudo rung, so the root leg escalates even when chmod already repaired the tree. This file is deliberately comment-driven and its cleanup recipe is contract-pinned by tests, so a maintainer auditing when the root leg fires will conclude a chmod-repaired tree is removed before sudo is attempted — the opposite of what runs. Reword the comment (or insert rm -rf "$abs" 2>/dev/null && return 0 between the two rungs to make the code match, which also skips the root leg when chmod sufficed):

Suggested change
# repair ladder instead: chmod what this user owns, then passwordless
# sudo chown/chmod where the pool member has it, each followed by a
# retry. Members without sudo still degrade to a named warning —
# repair ladder instead: chmod what this user owns, then passwordless
# sudo chown/chmod where the pool member has it, with a single retry
# after the ladder. Members without sudo still degrade to a named warning —
中文说明

注释称每个梯级都“各跟一次重试”,但 remove_review_tree 只在所有梯级之后重试一次 rm -rf——chmod 梯级与 sudo 梯级之间并没有重试,因此即使 chmod 已经修好了树,root 梯级仍会升级执行。该文件刻意以注释驱动,且其清理流程被契约测试 pin 住,审计 root 梯级触发时机的维护者会得出“chmod 修好的树会在动用 sudo 之前被删除”的结论——与实际行为相反。建议改写注释(或在两个梯级之间插入 rm -rf "$abs" 2>/dev/null && return 0 使代码与注释一致,这样 chmod 已修复时还能跳过 root 梯级)(见上方 suggestion)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

# nothing unprivileged can remove a foreign-owned tree — but the heal
# chain must never fail the job.
- name: 'Clean review worktrees'
if: 'always()'
timeout-minutes: 5
Expand All @@ -1853,6 +1867,65 @@ jobs:
fi

GIT_SAFE=(git -c core.hooksPath=/dev/null -c core.fsmonitor= -C "$GITHUB_WORKSPACE")

# The repair ladder for one leftover tree (see the step comment).
# A path outside the workspace or resolving through symlinks is
# refused rather than repaired: the sudo leg escalates to root,
# and a planted link would aim a chown/chmod -R outside the
# workspace. Warning echoes strip newlines from the path first:
# leftover names are untrusted glob entries, and a fresh line on
# the runner's stdout would parse as a workflow command.
remove_review_tree() {
local abs="$1"
case "$abs" in
/*) : ;;
*) abs="$GITHUB_WORKSPACE/$abs" ;;
esac
[ -e "$abs" ] || [ -L "$abs" ] || return 0
rm -rf "$abs" 2>/dev/null && return 0
# Refuse a path that resolves through symlinks, but compare
# against the workspace's OWN resolved path: an ancestor the
# workspace itself sits under (a macOS /tmp -> /private/tmp
# local run) is legitimate and must not read as a redirect —
# only a symlink planted BELOW the workspace does. The refusal
# names the branch that fired so the on-call knows which case
# hit.
local ws_real rel abs_real reason=''
ws_real="$(realpath -- "$GITHUB_WORKSPACE" 2>/dev/null)" ||
ws_real="$GITHUB_WORKSPACE"
case "$abs" in
"$GITHUB_WORKSPACE"/*) rel="${abs#"$GITHUB_WORKSPACE/"}" ;;
*) rel='' ;;
esac
abs_real="$(realpath -- "$abs" 2>/dev/null)" || abs_real=''
if [ -z "$rel" ]; then
reason='outside the workspace'
elif [ -L "$abs" ]; then
reason='path is a symlink'
elif [ -z "$abs_real" ]; then
reason='path could not be resolved'
elif [ "$abs_real" != "$ws_real/$rel" ]; then
reason='resolves through symlinks'
fi
if [ -n "$reason" ]; then
echo "::warning::refusing to repair review worktree path (${reason}): ${abs//$'\n'/ }"
return 0
fi
chmod -R u+rwX "$abs" 2>/dev/null || true
rm -rf "$abs" 2>/dev/null && return 0
local sudo_probe='absent'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The failure warning's sudo: diagnostic conflates two states this change is specifically about: "sudo not installed" and "sudo installed but password-gated". On a member with sudo present but no NOPASSWD entry, command -v sudo succeeds while sudo -n true fails, so sudo_probe stays absent and the warning prints sudo: absent — sending the on-call to investigate a missing package when the remediation is a sudoers rule. The incident this step exists for (run 32577821716) was exactly the gated state: "the runner had no passwordless sudo". Corroborated empirically: a probe host with password-gated sudo printed sudo: absent from the unmodified step. Split the probe into three states; the existing sudo: \$sudo_probe test pin survives unchanged.

Suggested change
local sudo_probe='absent'
local sudo_probe='password-gated'
command -v sudo >/dev/null 2>&1 || sudo_probe='absent'
中文说明

失败 warning 里的 sudo: 诊断把两种本改动恰好要区分的状态混为一谈:"未安装 sudo" 与 "安装了 sudo 但需要密码"。在有 sudo 但没有 NOPASSWD 条目的成员上,command -v sudo 成功而 sudo -n true 失败,sudo_probe 保持 absent,warning 输出 sudo: absent——值班人会去排查缺包,而正确的处置是配置 sudoers 规则。本步骤所针对的事故(run 32577821716)恰好就是"有 sudo 但需密码"状态:"the runner had no passwordless sudo"。实测佐证:在一台 sudo 需密码的主机上,未改动的步骤打印了 sudo: absent。把探测拆成三态即可;现有的 sudo: \$sudo_probe 测试断言不受影响。

— qwen3.8-max via Qwen Code /review (v0.22.0)

if command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then
sudo_probe='ok'
sudo -n chown -R "$(id -u):$(id -g)" "$abs" 2>/dev/null || true
sudo -n chmod -R u+rwX "$abs" 2>/dev/null || true
fi
rm -rf "$abs" 2>/dev/null && return 0
echo "::warning::could not remove review worktree: ${abs//$'\n'/ } (permission repair failed; sudo: $sudo_probe; owner: $(ls -ld "$abs" 2>/dev/null | awk '{print $3}'))"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] The newline sanitization this round added covers the two direct ${abs} interpolations only, and it is LF-only — two demonstrated entrances still inject workflow commands into this job's stdout, the exact class the step comment and the contract test claim to have closed. First, the owner: enrichment on this line embeds $(ls -ld "$abs" | awk '{print $3}'): GNU ls piped to a non-tty prints a newline-bearing path raw across lines, and awk prints field 3 of every line, so a leftover named review-pr-x$'\n'a b ::stop-commands::tok emits a standalone ::stop-commands::tok line the runner parses as a workflow command. Second, ${abs//$'\n'/ } strips LF only, and the Actions runner splits step stdout on bare CR (actions/runner ProcessInvoker.cs reads via StreamReader.ReadLine(), which terminates on \r; OutputManager.cs hands every :: line to the command parser), so a CR-bearing name injects a second command line through both warnings. Reachable impact is bounded (set-env/add-path are disabled runner-side — this is annotation forgery / ::stop-commands:: / ::add-mask:: on the privileged review job, not code execution), but the poison tree survives the sweep, so every later job on the runner re-emits it, and the existing ${abs//$'\n'/ } x2 pin passes throughout because it cannot see inside $(…).

Witness — probe of the extracted step against a root-owned leftover named review-pr-x$'\n'a b ::stop-commands::tok\nc d e on a password-gated-sudo host:

line 2: ::warning::could not remove review worktree: …review-pr-x a b ::stop-commands::tok c d e (permission repair failed; sudo: absent; owner: root
line 3: ::stop-commands::tok
line 4: e)

a standalone, runner-parseable command line; with the fix below applied the same probe printed a single line — zero injected commands.

Suggested change
echo "::warning::could not remove review worktree: ${abs//$'\n'/ } (permission repair failed; sudo: $sudo_probe; owner: $(ls -ld "$abs" 2>/dev/null | awk '{print $3}'))"
echo "::warning::could not remove review worktree: ${abs//[$'\r\n']/ } (permission repair failed; sudo: $sudo_probe; owner: $(ls -ld "$abs" 2>/dev/null | awk 'NR==1 {print $3}'))"

Apply the same [$'\r\n'] strip to the refusal warning above (line ~1911), and extend the contract pin to cover every expansion in both warnings rather than only the two ${abs} occurrences.

中文说明

本轮新增的换行净化只覆盖两个直接的 ${abs} 插值,且只剥 LF——仍有两条已被证明的入口可以向本 job 的 stdout 注入 workflow 命令,正是步骤注释与契约测试声称已关闭的那一类。其一,本行的 owner: 增强内嵌 $(ls -ld "$abs" | awk '{print $3}'):GNU ls 管道输出时会把带换行的路径原样跨行打印,awk 会打印每一行的第 3 个字段,因此名为 review-pr-x$'\n'a b ::stop-commands::tok 的残留会输出一条独立的 ::stop-commands::tok 行,被 runner 当作 workflow 命令解析。其二,${abs//$'\n'/ } 只剥 LF,而 Actions runner 按裸 CR 切分步骤 stdout(actions/runner 的 ProcessInvoker.csStreamReader.ReadLine() 读取,\r 即行终止符;OutputManager.cs 会把任何含 :: 的行交给命令解析器),因此带 CR 的名字可经由两条 warning 注入第二条命令行。可达影响有上限(set-env/add-path 已在 runner 侧禁用——这是对特权 review job 的 annotation 伪造 / ::stop-commands:: / ::add-mask::,而非代码执行),但毒树在清扫后依然存活,该 runner 上之后的每个 job 都会再次触发;现有的 ${abs//$'\n'/ } x2 断言全程保持绿色,因为它看不到 $(…) 内部。

critical 证据:对提取出的步骤做 probe——root 属主、名字为 review-pr-x$'\n'a b ::stop-commands::tok\nc d e 的残留,在 sudo 需密码的主机上输出了独立的 ::stop-commands::tok 行;应用下方修复后同一 probe 只输出单行,注入命令数为零。

— qwen3.8-max via Qwen Code /review (v0.22.0)

# return 0 even when the warning echo fails: the heal chain must
# never fail the job.
return 0
}
Comment on lines +1924 to +1929

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The new leftover loop is the first place raw shell-glob names reach step stdout. A leftover entry whose name embeds a newline followed by ::command is echoed here un-sanitized, and the Actions runner parses ::-prefixed lines as workflow commands. A prior containerised job on this pool — exactly the actor this step exists for — can leave a name like review-pr-x$'\n::stop-commands::tok'; when the repair fails on a sudo-less member, this warning emits the raw name and the runner parses the embedded line. The reachable impact is bounded (set-env/add-path have been disabled since 2020, so this is annotation spoofing / stop-commands suppression rather than code execution), but the surface is new in this diff: pre-change, the glob rm -rf … || true never echoed leftover names, and the worktree-list loop's values come from git porcelain, which C-escapes newlines. Strip newlines before echoing:

Suggested change
rm -rf "$abs" 2>/dev/null && return 0
echo "::warning::could not remove review worktree: $abs (permission repair failed)"
}
rm -rf "$abs" 2>/dev/null && return 0
echo "::warning::could not remove review worktree: ${abs//$'\n'/ } (permission repair failed)"
}
中文说明

新增的残留目录循环是 shell glob 原始文件名第一次直接进入步骤 stdout。名字中嵌入换行加 ::command 的残留条目在这里会未经净化地被 echo 出来,而 Actions runner 会把以 :: 开头的行解析为 workflow 命令。池上先前的容器化 job(正是本步骤要处理的角色)可以留下形如 review-pr-x$'\n::stop-commands::tok' 的名字;当修复在无 sudo 成员上失败时,这条 warning 会原样输出该名字,runner 随即解析其中嵌入的命令行。可达影响有上限(set-env/add-path 自 2020 年已禁用,因此只能伪造 annotation / 触发 stop-commands 抑制,而非代码执行),但该注入面是本 diff 新引入的:改动前 glob 的 rm -rf … || true 从不 echo 残留名字,而 worktree 列表循环的值来自 git porcelain(会对换行做 C 转义)。建议在 echo 前剥离换行(见上方 suggestion)。

— qwen3.8-max via Qwen Code /review (v0.22.0)


"${GIT_SAFE[@]}" worktree prune -v || true
"${GIT_SAFE[@]}" worktree list --porcelain \
| awk '$1 == "worktree" && index($0, "/.qwen/tmp/review-pr-") > 0 { sub(/^worktree /, ""); print }' \
Expand All @@ -1873,10 +1946,21 @@ jobs:
continue
;;
esac
# `git worktree remove` unlinks entries the same way rm does,
# so a foreign-owned entry defeats it too; the repair ladder
# retries it, and whatever git still leaves behind goes through
# the same ladder below (registrations are pruned afterwards).
"${GIT_SAFE[@]}" worktree remove --force "$worktree" ||
echo "::warning::could not remove review worktree: $worktree"
remove_review_tree "$worktree"
done || true
rm -rf .qwen/tmp/review-pr-* 2>/dev/null || true
# Survivors of the glob are exactly the permission-poisoned trees;
# run each through the repair ladder individually so one poisoned
# entry cannot mask its siblings.
for leftover in .qwen/tmp/review-pr-*; do
[ -e "$leftover" ] || [ -L "$leftover" ] || continue
remove_review_tree "$leftover"
done
"${GIT_SAFE[@]}" worktree prune -v || true
"${GIT_SAFE[@]}" for-each-ref --format='%(refname:short)' 'refs/heads/qwen-review/*' \
| while read -r review_ref; do
Expand Down
45 changes: 43 additions & 2 deletions scripts/tests/review-worktree-cleanup-workflow.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,41 @@ describe('review worktree cleanup steps', () => {
expect(reviewCleanStep).toContain(
`rm -f ${toPosix(REVIEW_TMP_DIR)}/${LEASE_PREFIX}pr-*.json`,
);
// A failed rm must not be left to poison the next job's checkout: the
// sweep owns its own permission repair — chmod, then passwordless sudo
// chown/chmod where the pool member has it — and retries the removal per
// leftover entry (measured, run 32577821716 / PR #9718: a foreign-owned
// scratch-verify tree killed the next review at checkout with EACCES).
// Pin the ladder's EFFECT, not mechanism substrings: those double-match
// (the non-sudo chmod rung hides inside the sudo line) and let a
// rewrite silently drop the ladder back to warn-and-leave.
const reviewCleanCode = stripComments(reviewCleanStep);
// Three removal attempts: the initial rm plus one retry after EACH
// repair rung, so a chmod-repaired tree never escalates to sudo.
expect(reviewCleanCode.match(/rm -rf "\$abs"/g)).toHaveLength(3);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The nine new pins assert presence and counts of the ladder's text, but not its order or effect — and mutation runs against the real suite show the gap is real. Three behavior-breaking mutations keep all 5 tests green: setting rel='' (every leftover then refuses as "outside the workspace", no rung ever runs, and the incident this PR exists for recurs); relocating the refusal guard BELOW the chmod/sudo rungs (verified in a mirror tree — chmod -R follows a symlink operand, so the privileged rungs could then operate through a planted link); and swapping the sudo block above the chmod rung (breaking the "chmod-repaired tree never escalates to sudo" property the test comment claims). Controls confirm the harness is alive: dropping the sudo rung, inverting !=, or deleting one newline-strip all go red. The pins catch some rewrites — just not order or effect. This supersedes round-1 R1-1: text pins cannot enumerate the rewrite space, and an ordering assertion closes the demonstrated shapes structurally.

Witness — mutation arms in a freshly reset scratch tree, real vitest suite: baseline 5/5 green; rel='' → 5/5 green; guard relocated below the rungs → 5/5 green; control (sudo rung dropped) → 1 test failed.

Suggested change
expect(reviewCleanCode.match(/rm -rf "\$abs"/g)).toHaveLength(3);
expect(reviewCleanCode.match(/rm -rf "\$abs"/g)).toHaveLength(3);
// Order matters as much as presence: the refusal guard must precede the
// repair rungs, or a rewrite can chmod/chown through a planted link
// before the check ever runs.
const guardPos = reviewCleanCode.indexOf('if [ -n "$reason" ]');
expect(guardPos).toBeGreaterThan(-1);
expect(guardPos).toBeLessThan(reviewCleanCode.indexOf('chmod -R u+rwX "$abs"'));
expect(guardPos).toBeLessThan(reviewCleanCode.indexOf('sudo -n chown -R'));

Stronger still: extract the function from the step text and execute it against a tmp fixture (a self-owned read-only tree removed by the chmod rung — skipped when running as root, where CAP_DAC_OVERRIDE makes the fixture vacuous; a planted symlink refused with the target untouched; a newline-bearing name yielding a single-line warning), capability-gated like the existing awkAvailable pattern.

中文说明

这九条新断言 pin 住的是梯子文本的"存在与计数",而不是其"顺序或效果"——对真实套件做的 mutation 运行证明缺口是真实的。三种破坏行为的变异都让 5 条测试全绿:把 rel=''(此后每个残留都会被当作"在工作区之外"拒绝,任何梯级都不会执行,本 PR 要解决的事故会复发);把拒绝守卫移到 chmod/sudo 梯级之下(已在镜像树中验证——chmod -R 会跟随符号链接操作数,特权梯级就可能顺着植入的链接操作);把 sudo 块换到 chmod 梯级之前(破坏测试注释所声称的"chmod 修好的树不会升级到 sudo"属性)。对照实验证明测试框架是活的:删掉 sudo 梯级、反转 !=、删除任一新换行剥离都会变红。这些断言能抓住一部分改写——只是抓不住顺序与效果。本条取代第 1 轮的 R1-1:文本断言无法枚举改写空间,顺序断言能结构性地关闭已证明的变异形状。

证据:在全新重置的临时树中对真实 vitest 套件做 mutation——基线 5/5 绿;rel='' → 5/5 绿;守卫移到梯级之下 → 5/5 绿;对照(删掉 sudo 梯级)→ 1 条测试失败。更彻底的方案是把函数从步骤文本中提取出来、对 tmp 夹具真实执行(自持只读树经 chmod 梯级被删除——以 root 运行时跳过,CAP_DAC_OVERRIDE 会使夹具失效;植入的符号链接被拒绝且目标不受影响;带换行的名字只产生单行 warning),并按现有 awkAvailable 模式做能力门控。

— qwen3.8-max via Qwen Code /review (v0.22.0)

// The non-sudo rung must exist as its own command, not just inside the
// sudo line.
expect(reviewCleanCode).toMatch(/^\s*chmod -R u\+rwX "\$abs"/m);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The rungs' 2>/dev/null || true failure guards are pinned by nothing, and no fixture can make a rung fail (fixture trees are user-owned, so chmod succeeds; root hosts are gated out) — dropping || true from the chmod rung ships 8/8 green, and even a -e-hardened harness cannot catch it (probe: status=0 under both harnesses, since the fixture's chmod never fails). That matters because the leftover for-loop calls remove_review_tree bare under the runner's bash -eo pipefail — NOT exempt from errexit, unlike the piped worktree-list loop's done || true. In production, on a foreign-owned leftover chmod exits EACCES, errexit kills the cleanup step mid-ladder, and the trailing second worktree prune, branch-deletion loop, and lease rm -f never run — the if: always() step goes red and branches/leases leak on the runner.

Witness:

mutant (guard dropped) + shipped suite: Tests 8 passed (8)
errexit probe under the runner's flags: bare call site exit=1 (tail never runs); piped site exit=0 (absorbed)
suggested pin: red on mutant, green on clean

Suggested fix — pin the guarded form, and extend the sudo pins to carry their guards too:

expect(reviewCleanCode).toMatch(/^\s*chmod -R u\+rwX "\$abs" 2>\/dev\/null \|\| true$/m);
中文说明

各梯级的 2>/dev/null || true 失败防护没有被任何断言钉住,也没有任何夹具能让梯级失败(夹具树为当前用户所有,chmod 必然成功;root 主机被门控排除)——删掉 chmod 梯级的 || true 后 8/8 全绿上线,即便换上带 -e 的框架也抓不到(探针:两种框架下 status=0,因为夹具的 chmod 从不失败)。这很要紧:残留 for 循环在 runner 的 bash -eo pipefail 下裸调用 remove_review_tree——不像带 done || true 的 worktree 列表管道循环,它并不豁免 errexit。生产中,遇到他人属主的残留时 chmod 以 EACCES 退出,errexit 会在梯子中途杀死清理步骤,其后的第二次 worktree prune、分支删除循环和租约 rm -f 都不会执行——if: always() 步骤变红,分支/租约泄漏在 runner 上。

证据:变异(删防护)+ 现有套件 8/8 全绿;按 runner 标志做 errexit 探针:裸调用点 exit=1(尾部未执行),管道调用点 exit=0(被吸收);建议断言在变异下红、干净代码下绿。

建议修复——钉住带防护的形式(并把 sudo 断言扩展为同样带上防护):

expect(reviewCleanCode).toMatch(/^\s*chmod -R u\+rwX "\$abs" 2>\/dev\/null \|\| true$/m);

— qwen3.8-max via Qwen Code /review (v0.22.0)

expect(reviewCleanCode).toContain('sudo -n chown -R');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The sudo rung is pinned only by the verb substring sudo -n chown -R; two mutants ship 8/8 green. (1) Its second half sudo -n chmod -R u+rwX "$abs" can be deleted outright: no pin mentions sudo -n chmod (the chmod regex/order pins match only the non-sudo rung), and all fixtures chmod-555 the PARENT, so none observes the rung even on a passwordless-sudo host. (2) The chown ownership target "$(id -u):$(id -g)" can be mutated to root:root: nothing captures id -u/id -g, and no fixture asserts the warning's owner: field. Either way, on a pool member WITH passwordless sudo — the member the rung exists to heal — a foreign-owned leftover with a mode-locked inner directory is chowned back but never made writable (or actively locked to root), the runner-user retry rm still fails, and the ladder degrades to could not remove … (permission repair failed; sudo: ok) with the leftover surviving the job it was supposed to heal in.

Witness:

chmod-rung-deletion mutant: Tests 8 passed (8)
root:root mutant: Tests 8 passed (8)
+ toContain('sudo -n chown -R "$(id -u):$(id -g)" "$abs"'): 1 failed | 7 passed on the mutant, 8/8 on clean

Suggested fix — pin both sudo rung lines in full:

expect(reviewCleanCode).toContain('sudo -n chown -R "$(id -u):$(id -g)" "$abs" 2>/dev/null || true');
expect(reviewCleanCode).toContain('sudo -n chmod -R u+rwX "$abs" 2>/dev/null || true');

or add a capability-gated fixture (probe sudo -n true) that makes a leftover foreign-owned with a 0555 inner dir and asserts removal.

中文说明

sudo 梯级只被动词子串 sudo -n chown -R 钉住;两种变异都能 8/8 全绿上线。(1)其后半段 sudo -n chmod -R u+rwX "$abs" 可被整行删除:没有任何断言提到 sudo -n chmod(chmod 正则/顺序断言只匹配非 sudo 梯级),且所有夹具都把父目录 chmod-555,即使在免密 sudo 主机上也没有夹具能观察到该梯级。(2)chown 属主目标 "$(id -u):$(id -g)" 可被变异为 root:root:没有任何断言捕获 id -u/id -g,也没有夹具断言 warning 的 owner: 字段。两种情况下,在有免密 sudo 的池成员上——正是该梯级要修复的成员——带模式锁定内目录的他人属主残留要么只被 chown 而不可写,要么被直接锁给 root,runner 用户的重试 rm 仍然失败,梯子降级为 could not remove … (permission repair failed; sudo: ok),残留在本应修复它的 job 中存活下来。

证据:删除 chmod 梯级的变异 8/8 全绿;root:root 变异 8/8 全绿;加上 toContain('sudo -n chown -R "$(id -u):$(id -g)" "$abs"') 后,变异下 1 失败 | 7 通过,干净代码 8/8。

建议修复——完整钉住两条 sudo 梯级:

expect(reviewCleanCode).toContain('sudo -n chown -R "$(id -u):$(id -g)" "$abs" 2>/dev/null || true');
expect(reviewCleanCode).toContain('sudo -n chmod -R u+rwX "$abs" 2>/dev/null || true');

或添加能力门控夹具(探测 sudo -n true):把残留做成他人属主、内目录 0555,并断言其被删除。

— qwen3.8-max via Qwen Code /review (v0.22.0)

expect(reviewCleanCode).toContain('remove_review_tree "$leftover"');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The leftover loop's glob — the call site that feeds every permission-poisoned tree into the ladder — is the only glob site in the sweep pinned to nothing. The sibling sites are tied to worktreePrefix (the bulk rm, the awk filter's index, the re-anchor case arm); the only pin on the loop is the call string. A typo or layout rename (for leftover in .qwen/tmp/review-pr-*.bak) matches nothing, so no poisoned tree ever reaches the ladder, and every pin and all eight fixtures stay green — the fixtures invoke the function directly and never execute the loop. Outcome: the foreign-owned EACCES leftover survives the sweep and kills the next review at checkout — the exact incident this diff cites as the reason the ladder exists (run 32577821716, the review of PR 9718).

Witness:

mutant glob '*.bak': Tests 8 passed (8)
+ toContain(`for leftover in ${worktreePrefix}*; do`): 1 failed | 7 passed on the mutant; mutant reverted + pin kept: 8/8

Suggested fix:

expect(reviewCleanStep).toContain(`for leftover in ${worktreePrefix}*; do`);
中文说明

残留循环的 glob——把每棵权限投毒树喂给梯子的调用点——是清扫中唯一没有被任何断言钉住的 glob 位点。兄弟位点都绑定到 worktreePrefix(批量 rm、awk 过滤器的 index、重锚定 case 分支);该循环上唯一的断言只有调用字符串。一个笔误或目录改名(for leftover in .qwen/tmp/review-pr-*.bak)匹配不到任何东西,于是没有任何投毒树进入梯子,所有断言和全部八个夹具依旧为绿——夹具直接调用函数,从不执行这个循环。后果:他人属主的 EACCES 残留在清扫后存活,并在下一次 review 的 checkout 时将其杀死——正是本 diff 引用的、梯子赖以存在的事故(run 32577821716,即 PR 9718 的 review)。

证据:变异 glob *.bak 下 8/8 全绿;加上 toContain(for leftover in ${worktreePrefix}*; do) 后,变异下 1 失败 | 7 通过;还原变异并保留断言后 8/8。

建议修复:

expect(reviewCleanStep).toContain(`for leftover in ${worktreePrefix}*; do`);

— qwen3.8-max via Qwen Code /review (v0.22.0)

// The symlink-refusal guard must survive, including the direction of
// its comparison and the deciding reason it now carries.
expect(reviewCleanCode).toContain(
'refusing to repair review worktree path (${reason})',
);
expect(reviewCleanCode).toContain('!= "$ws_real/$rel"');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This pin captures the condition of the resolves through symlinks refusal arm but not its consequence, and no fixture reaches that arm (the symlink fixture fires the earlier -L arm). Voiding reason='resolves through symlinks' keeps every pin and fixture green. For a leftover whose path resolves through a symlinked ancestor below the workspace (e.g. .qwen left as a symlink by a prior job on the shared pool), the guard then stops refusing: the probe-measured mutant ran the repair ladder THROUGH the planted link — unprivileged chmod restored bits outside the workspace and rm -rf deleted the sibling subtree (stdout empty, exit 0, child gone), where the clean arm refuses and leaves the child intact (400→400). On a passwordless-sudo member the root chown -R/chmod -R legs sit on the identical "$abs" path — the escalation the step comment says this guard exists to stop.

Witness:

mutant (arm body voided): Tests 8 passed (8)
planted-ancestor probe — clean: refusal warning, child intact 400→400; mutant: sibling subtree deleted through the link (stdout empty, exit 0)
proposed fixture: green on clean (9/9), red on the mutant

Suggested fix: add a fixture where .qwen inside the workspace is a symlink to a sibling directory, with a leftover beneath it and rm defeated by a chmod-555 parent (restore the mode in finally); assert one warning containing (resolves through symlinks). Optionally pin the reason string textually.

中文说明

该断言捕获了 resolves through symlinks 拒绝分支的条件,但没有捕获其结果,且没有夹具能到达该分支(符号链接夹具触发的是更早的 -L 分支)。把 reason='resolves through symlinks' 置空后,所有断言和夹具依旧为绿。对于路径经由工作区内某个符号链接祖先解析的残留(例如共享池中上一个 job 把 .qwen 留成符号链接),守卫将不再拒绝:经探针测量的变异会穿过植入的链接执行修复梯子——非特权 chmod 恢复了工作区之外的权限位,rm -rf 删除了兄弟目录的子树(stdout 为空、exit 0、子文件消失),而干净分支会拒绝并保持子文件原样(400→400)。在免密 sudo 成员上,root 的 chown -R/chmod -R 梯级走的正是同一个 "$abs" 路径——即步骤注释所说、该守卫要阻止的提权。

证据:变异(分支体置空)下 8/8 全绿;植入祖先探针——干净代码:拒绝 warning、子文件保持 400→400;变异:兄弟子树被穿过链接删除;建议夹具在干净代码下绿(9/9)、变异下红。

建议修复:添加夹具——工作区内的 .qwen 符号链接到兄弟目录,其下放置残留,并用 chmod-555 父目录使 rm 失败(finally 中恢复权限);断言恰有一条包含 (resolves through symlinks) 的 warning。可选:文本钉住该 reason 字符串。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred to the next round. This round batched the Critical plus the eight pin-level findings (the round batch bound); the four behavioral fixtures form one coherent batch and get it in full next round rather than a rushed tail here. Nothing in this round's commit weakens the cited mutant — the resolves through symlinks arm's consequence stays unwitnessed until the fixture lands.

中文说明

推迟到下一轮。本轮已批处理 Critical 加八条断言级发现(达到单轮批处理上限);四个行为夹具构成一个内聚批次,将在下一轮完整实现,而不是在本轮仓促收尾。本轮提交没有任何改动削弱所引用的变异——resolves through symlinks 分支的后果在夹具落地前仍然没有见证。

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The pin suite pins the guard comparison that consumes $abs_real but not the || abs_real='' fallback that makes its assignment errexit-safe; dropping the fallback ships 8/8 green (nothing in this file pins abs_real or realpath). The leftover loop calls remove_review_tree bare under the runner's -eo pipefail, so a leftover that realpath cannot resolve — a symlink loop, or a dangling link whose target ancestry is missing, under a mode-locked or foreign-owned parent — kills the cleanup step at the unguarded assignment: the trailing worktree prune, branch-deletion loop, and lease rm -f never run, and the if: always() step goes red, where the shipped code warns and continues. (A plain dangling link with existing parent ancestry resolves fine under GNU realpath and does not fire; the review-pr job's lanes are Linux-only.)

Witness:

fallback-drop mutant: Tests 8 passed (8)
probe under the runner's flags — symlink loop: shipped exit=0 (warning + trailing ran) vs mutant exit=1 (loop aborted, trailing skipped); same flip for dangling-with-missing-parent
suggested pin: green on clean, red on the mutant

Suggested fix (the sibling ws_real fallback is equally unpinned):

expect(reviewCleanCode).toContain("abs_real=\"$(realpath -- \"$abs\" 2>/dev/null)\" || abs_real=''");
中文说明

断言套件钉住了消费 $abs_real 的守卫比较,但没有钉住使其赋值在 errexit 下安全的 || abs_real='' 兜底;删除该兜底后 8/8 全绿上线(本文件没有任何断言涉及 abs_realrealpath)。残留循环在 runner 的 -eo pipefail 下裸调用 remove_review_tree,因此 realpath 无法解析的残留——符号链接环,或目标祖先缺失的悬空链接、且位于模式锁定/他人属主的父目录之下——会在未加防护的赋值处杀死清理步骤:其后的 worktree prune、分支删除循环和租约 rm -f 都不会执行,if: always() 步骤变红;而现有代码会发出 warning 并继续。(父目录祖先存在的普通悬空链接在 GNU realpath 下能正常解析,不会触发;review-pr job 的通道均为 Linux。)

证据:删除兜底的变异 8/8 全绿;按 runner 标志做探针——符号链接环:现有代码 exit=0(warning + 尾部执行)对变异 exit=1(循环中止、尾部跳过);目标祖先缺失的悬空链接同样翻转;建议断言在干净代码下绿、变异下红。

建议修复(兄弟的 ws_real 兜底同样未被钉住):

expect(reviewCleanCode).toContain("abs_real=\"$(realpath -- \"$abs\" 2>/dev/null)\" || abs_real=''");

— qwen3.8-max via Qwen Code /review (v0.22.0)

// Leftover names are untrusted glob entries: both warnings must strip
// newlines, or a hostile name injects a workflow command into the log.
expect(reviewCleanCode.match(/\$\{abs\/\/\$'\\n'\/ \}/g)).toHaveLength(2);
// The failure warning carries the deciding state (sudo probe + owner),
// and the function returns 0 unconditionally: even a failed warning
// echo must not fail the `if: always()` job via errexit.
expect(reviewCleanCode).toMatch(
/could not remove review worktree[^\n]*sudo: \$sudo_probe[^\n]*owner:/,
);
expect(reviewCleanCode).toMatch(
/could not remove review worktree[^\n]*\n\s*return 0/,
);
});

it('keeps the pre-checkout agent-state sweep pinned to paths.ts', () => {
Expand All @@ -192,7 +227,9 @@ describe('review worktree cleanup steps', () => {
});

it('uses one identical worktree filter at every list-driven sweep', () => {
const filter = reviewCleanStep.match(/awk '([^']+)'/)?.[1];
// The step's owner-extraction awk is not a worktree filter: anchor
// on the filter's shape, not the first awk in the step.
const filter = reviewCleanStep.match(/awk '(\$1 == "worktree"[^']+)'/)?.[1];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This diff pastes the same filter-extraction regex plus the identical two-line rationale comment into two tests (here and in the behavioral filter test below) instead of deriving it once — even though this file's own convention hoists exactly this kind of shared derivation from the parsed YAML to module scope (worktreePrefix, branchFamily, reviewCleanStep). The coupling is not hypothetical: this round already had to edit both copies in lockstep, and a future filter change that updates only one leaves the pin test pinning one filter while spawnSync('awk', [filter]) executes a different one — the semantic check no longer validates what the pinning test pins, and the drift surfaces later as a confusing failure elsewhere. Hoist one module-level derivation next to reviewCleanStep and reference it in both tests:

// next to reviewCleanStep (rationale comment kept once, here)
const worktreeFilter = reviewCleanStep.match(
  /awk '(\$1 == "worktree"[^']+)'/,
)?.[1];
// in both tests:
const filter = worktreeFilter;
中文说明

本 diff 把同一个过滤器提取正则和完全相同的两行注释复制进了两个测试(此处与下方的行为过滤器测试),而不是只推导一次——尽管本文件的既有惯例就是把这类从解析出的 YAML 推导的共享量提升到模块作用域(worktreePrefixbranchFamilyreviewCleanStep)。这种耦合不是假设:本轮就已经被迫同步修改了两处副本;未来任何只改其中一处的过滤器变更,都会让 pin 测试钉住一个过滤器、而 spawnSync('awk', [filter]) 执行另一个——语义检查不再验证 pin 测试所钉住的东西,偏差会在别处以令人困惑的失败形式出现。建议在 reviewCleanStep 旁提升一个模块级推导,两个测试都引用它(注释只保留一份)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

expect(filter).toBeTruthy();
for (const { id, run } of ciCleanSteps) {
expect(run, id).toContain(`awk '${filter}'`);
Expand All @@ -202,7 +239,11 @@ describe('review worktree cleanup steps', () => {
it.skipIf(!awkAvailable)(
'filter selects review worktrees only, never the main checkout',
() => {
const filter = reviewCleanStep.match(/awk '([^']+)'/)?.[1];
// The step's owner-extraction awk is not a worktree filter: anchor
// on the filter's shape, not the first awk in the step.
const filter = reviewCleanStep.match(
/awk '(\$1 == "worktree"[^']+)'/,
)?.[1];
const main = '/home/runner/work/qwen-code/qwen-code';
const review = `${main}/.qwen/tmp/review-pr-42`;
const out = spawnSync('awk', [filter], {
Expand Down
Loading