-
Notifications
You must be signed in to change notification settings - Fork 3k
fix(ci): clean review worktrees after cancellation #8474
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
41d4c66
d1f976a
772d71c
02909be
0a8d999
168bbe6
dbdcd7d
3e70e8d
0065bb8
eb1f6a6
f7f0954
1c87324
707aafb
bce9a62
f19e3fa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -397,15 +397,17 @@ jobs: | |
| echo "no prior workspace; nothing to clean" | ||
| exit 0 | ||
| fi | ||
| GIT_SAFE=(git -c core.hooksPath=/dev/null -c core.fsmonitor= -C "$GITHUB_WORKSPACE") | ||
| rm -rf .qwen/tmp/review-pr-* 2>/dev/null || true | ||
| git worktree prune -v || true | ||
| git for-each-ref --format='%(refname:short)' 'refs/heads/qwen-review/*' \ | ||
| "${GIT_SAFE[@]}" worktree prune -v || true | ||
| "${GIT_SAFE[@]}" for-each-ref --format='%(refname:short)' 'refs/heads/qwen-review/*' \ | ||
| | while read -r stale_ref; do | ||
| if [ -n "$stale_ref" ]; then | ||
| git branch -D "$stale_ref" || true | ||
| "${GIT_SAFE[@]}" branch -D "$stale_ref" || | ||
| echo "::warning::could not remove review branch: $stale_ref" | ||
| fi | ||
| done | ||
| git worktree prune -v || true | ||
| done || true | ||
| "${GIT_SAFE[@]}" worktree prune -v || true | ||
| echo "stale agent state cleaned" | ||
|
|
||
| # SECURITY: checkout trusted base code; /review fetches PR diff context. | ||
|
|
@@ -989,6 +991,56 @@ jobs: | |
| --repo "$GITHUB_REPOSITORY" \ | ||
| --body "$body" | ||
|
|
||
| # A cancelled or timed-out review may not reach the CLI's process cleanup. | ||
| # Remove both the worktree directories and Git's worktree registrations so | ||
| # 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. | ||
| - name: 'Clean review worktrees' | ||
| if: 'always()' | ||
| timeout-minutes: 5 | ||
| run: |- | ||
| set -uo pipefail | ||
| if [ ! -e .git ]; then | ||
| echo "no Git checkout; nothing to clean" | ||
| exit 0 | ||
| fi | ||
|
|
||
| GIT_SAFE=(git -c core.hooksPath=/dev/null -c core.fsmonitor= -C "$GITHUB_WORKSPACE") | ||
| "${GIT_SAFE[@]}" worktree prune -v || true | ||
| "${GIT_SAFE[@]}" worktree list --porcelain \ | ||
| | awk '$1 == "worktree" && index($0, "/.qwen/tmp/review-pr-") > 0 { sub(/^worktree /, ""); print }' \ | ||
| | while read -r worktree; do | ||
| [ -n "$worktree" ] || continue | ||
| # Registered paths come from leftover git metadata and are | ||
| # untrusted: the awk filter above matched by substring, so reject | ||
| # `..` traversal and re-anchor to the review prefix before the | ||
| # destructive remove. | ||
| case "$worktree" in | ||
| */../*|../*|*/..) | ||
| echo "::warning::skipping suspicious review worktree path: $worktree" | ||
| continue | ||
| ;; | ||
| "$GITHUB_WORKSPACE/.qwen/tmp/review-pr-"*) : ;; | ||
| *) | ||
| echo "::warning::skipping unexpected review worktree path: $worktree" | ||
| continue | ||
| ;; | ||
| esac | ||
| "${GIT_SAFE[@]}" worktree remove --force "$worktree" || | ||
| echo "::warning::could not remove review worktree: $worktree" | ||
| done || true | ||
| rm -rf .qwen/tmp/review-pr-* 2>/dev/null || true | ||
| "${GIT_SAFE[@]}" worktree prune -v || true | ||
| "${GIT_SAFE[@]}" for-each-ref --format='%(refname:short)' 'refs/heads/qwen-review/*' \ | ||
| | while read -r review_ref; do | ||
| [ -n "$review_ref" ] || continue | ||
|
Comment on lines
+1035
to
+1037
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R2-8: The sweep only removes worktrees whose registered path contains # on branch -D failure, find the holder by ref, remove it, retry:
holder=$("${GIT_SAFE[@]}" worktree list --porcelain \
| awk -v ref="refs/heads/$review_ref" \
'$1 == "worktree" { wt = $2 } $1 == "branch" && $2 == ref { print wt }')
if [ -n "$holder" ]; then
"${GIT_SAFE[@]}" worktree remove --force "$holder" 2>/dev/null || rm -rf "$holder" 2>/dev/null || true
"${GIT_SAFE[@]}" worktree prune -v || true
"${GIT_SAFE[@]}" branch -D "$review_ref" ||
echo "::warning::could not remove review branch: $review_ref"
fiVerified on a scratch repo: the fixed sweep deletes the branch and leaves only the main worktree. 中文说明清扫只移除注册路径包含 — qwen3.8-max via Qwen Code /review (v0.21.5)
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Declined for this round, with thanks for the thorough reproduction. The holder state this targets cannot be produced by this repo's own tooling — all three 中文说明本轮暂不采纳,感谢详尽的复现。该建议针对的 holder 状态无法由本仓库自身工具产生——三处 |
||
| "${GIT_SAFE[@]}" branch -D "$review_ref" || | ||
| echo "::warning::could not remove review branch: $review_ref" | ||
| done || true | ||
| rm -f .qwen/tmp/qwen-review-lease-pr-*.json 2>/dev/null || true | ||
| echo "review worktrees cleaned" | ||
|
|
||
| resolve-pr: | ||
| needs: ['authorize'] | ||
| if: |- | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion] R2-4: The sweep feeds attacker-influenceable leftover git metadata (
.git/worktrees/*/gitdircontents) unnormalized into destructivegit worktree remove --force; git resolves..traversal in the registered path, so the “delete only review artifacts” guarantee rests on a substring check over an untrusted string. The three ci.yml sweep copies share the identical recipe. — Failure scenario: reproduced end-to-end → a prior job with runner-user code execution rewrites.git/worktrees/<id>/gitdirto<ws>/.qwen/tmp/review-pr-42/../../../../target/.gitand plants the backlink; the awk filter matches the raw string, git backlink validation passes, andworktree remove --forcedeletes the attacker-chosentargetdirectory outside the review tree. Impact is bounded (the attacker already has runner-user code execution — no privilege escalation), but the cleanup becomes a delayed, plausibly-denied arbitrary-directory-deletion primitive.Verified on a fixture: the guard skips the traversal path with a warning, the target survives, and legitimate leftover review worktrees are still removed.
中文说明
清扫把可被攻击者影响的残留 git 元数据(
.git/worktrees/*/gitdir内容)未经规范化就喂给破坏性的git worktree remove --force;git 会解析注册路径中的..穿越,因此“只删除 review 产物”的保证仅依赖对不受信任字符串的子串检查。三份 ci.yml 清扫副本的配方完全相同。失败场景:已端到端复现——先前拥有 runner 用户代码执行权限的任务把.git/worktrees/<id>/gitdir改写为<ws>/.qwen/tmp/review-pr-42/../../../../target/.git并植入回链;awk 过滤器匹配原始字符串,git 回链校验通过,worktree remove --force删除了 review 目录之外、由攻击者选定的target目录。影响有界(攻击者已拥有 runner 用户代码执行权限——无提权),但清理变成了一个延迟的、可抵赖的任意目录删除原语。修复已用 fixture 验证:守卫跳过穿越路径并输出警告,目标目录幸存,合法的残留 review worktree 仍会被移除。— qwen3.8-max via Qwen Code /review (v0.21.5)