Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
196 changes: 196 additions & 0 deletions packages/cli/src/commands/review/base-tree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { execFileSync } from 'node:child_process';
import {
appendFileSync,
chmodSync,
utimesSync,
mkdtempSync,
mkdirSync,
Expand All @@ -29,6 +31,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { runBaseTree, type BaseTreeReport } from './base-tree.js';
import { baseWorktreePath } from './lib/paths.js';
import { isolateHostGitConfig } from './lib/test-utils.js';
import type { BuildTestReport } from './build-test.js';

const okBuild = {
Expand Down Expand Up @@ -302,6 +305,199 @@ describe('runBaseTree', () => {
expect(r.note).toMatch(/base worktree could not be created/);
});

it('refuses while repo-local config defines a content filter — the add would execute it', () => {
// The base tree's `worktree add` checks every file out, and a checkout
// EXECUTES `filter.<name>.smudge` — the same surface `scratch-tree`
// refuses to reset through. The attributes line and a matching file make
// the execution real: without the screen, the add below fired the smudge.
const pwned = join(repo, 'PWNED-base');
git(worktree, 'config', 'filter.evil.smudge', `touch ${pwned}`);
const attrs = git(worktree, 'rev-parse', '--git-path', 'info/attributes');
appendFileSync(attrs, '*.txt filter=evil\n');

const r = run();

expect(r.available).toBe(false);
expect(r.note).toContain('filter.evil.smudge');
expect(existsSync(pwned)).toBe(false);
expect(existsSync(baseWorktreePath(worktree))).toBe(false);
});

it('reports the add breached — never available — when a plant appears during the checkout', () => {
// The screen is a point-in-time read; the deterministic shape for its
// window with the add uses the screen's own disclosed limit — a filter
// in the GLOBAL config is the user's contract and never screened. Here
// its smudge arms a repo-LOCAL filter when the add's initial checkout
// executes it (attributes selecting it are committed at the base):
// absent at the pre-read, standing at every read after — so the report
// is breached and the just-added tree rolled back, never
// `available: true` over an executed plant.
const isolation = isolateHostGitConfig();
try {
writeFileSync(join(repo, '.gitattributes'), '*.txt filter=armer\n');
git(repo, 'add', '-A');
git(repo, 'commit', '-qam', 'base-with-attributes');
const armedBase = git(repo, 'rev-parse', 'HEAD');
execFileSync(
'git',
[
'config',
'--global',
'filter.armer.smudge',
"git config filter.evil.smudge 'touch /tmp/qwen-never'",
],
{ cwd: repo },
);

const r = run({ plan: { mergeBaseSha: armedBase } });

expect(r.available).toBe(false);
expect(r.note).toContain('may have EXECUTED');
expect(r.note).toContain('filter.evil.smudge');
// The breached tree is rolled back, not left planted for the next
// shard.
expect(existsSync(baseWorktreePath(worktree))).toBe(false);
} finally {
isolation.dispose();
}
});

it('reports the add breached when the plant ERASES ITSELF during the checkout', () => {
// The self-erasing shape the key re-read cannot see: the armer's
// smudge arms a repo-LOCAL filter AND unsets it again, per file, so
// the key is gone by the time the post-add re-read runs (probe, git
// 2.39: the re-read answered clean and the command certified a tree
// whose initial checkout had fired the plant). What the plant cannot
// erase is the change to the config file itself — the baseline the
// screen captured beside its clean read names it.
const isolation = isolateHostGitConfig();
try {
writeFileSync(join(repo, '.gitattributes'), '*.txt filter=armer\n');
git(repo, 'add', '-A');
git(repo, 'commit', '-qam', 'base-with-attributes');
const armedBase = git(repo, 'rev-parse', 'HEAD');
execFileSync(
'git',
[
'config',
'--global',
'filter.armer.smudge',
"git config filter.evil.smudge 'touch /tmp/qwen-never'; " +
'git config --unset filter.evil.smudge',
],
{ cwd: repo },
);

const r = run({ plan: { mergeBaseSha: armedBase } });

expect(r.available).toBe(false);
expect(r.note).toContain('may have EXECUTED');
// No key survived the erase — the refusal must name the changed
// file instead of reading as a clean point-in-time re-read.
expect(r.note).toContain('changed');
expect(existsSync(baseWorktreePath(worktree))).toBe(false);
} finally {
isolation.dispose();
}
});

// POSIX-only by construction: the killer arms `kill -9 $PPID` and a
// marker embedded UNQUOTED in the shell-lexed smudge, and on a Windows
// lane the backslashes of the platform path are shell escapes — the
// marker can never land at the asserted path and the positive assertion
// is deterministically red regardless of the guard under test.
it.skipIf(process.platform === 'win32')(
'attributes a plant the add EXECUTED even when the checkout threw after it',
() => {
// A checkout can throw AFTER executing a plant: the smudge fires,
// leaves its self-erasing trace in the common config, and kills git —
// the spawn dies on the signal and `git` throws. The catch used to
// report the add failure alone, burying the execution under it, and
// the next call screened clean over the run (the plant had erased
// itself). The baseline the screen captured still stands at the
// catch, so the paired re-read attributes it there.
const isolation = isolateHostGitConfig();
try {
const pwned = join(repo, 'PWNED-base-kill');
writeFileSync(join(repo, '.gitattributes'), '*.txt filter=killer\n');
git(repo, 'add', '-A');
git(repo, 'commit', '-qam', 'base-with-attributes');
const armedBase = git(repo, 'rev-parse', 'HEAD');
execFileSync(
'git',
[
'config',
'--global',
'filter.killer.smudge',
`git config qwen.plant.x 1; git config --unset qwen.plant.x; touch ${pwned}; kill -9 $PPID`,
],
{ cwd: repo },
);

const r = run({ plan: { mergeBaseSha: armedBase } });

// The plant EXECUTED — and the report says so, instead of reading
// as an add failure.
expect(existsSync(pwned)).toBe(true);
expect(r.available).toBe(false);
expect(r.note).toContain('may have EXECUTED');
expect(r.note).toContain('changed');
} finally {
isolation.dispose();
}
},
);

it('sweeps a stale tree whose own admin config holds a plant, instead of wedging on it', () => {
// The screen's candidate set reads every `<common>/worktrees/*/
// config.worktree`, and it used to run ABOVE the stale sweep: a plant
// parked in the stale base tree's OWN admin dir refused every retry —
// each attempt re-screened state the sweep's next statement would have
// destroyed, and the refusal never moved (measured live: every retry
// refused identically until manual cleanup). The screen now runs below
// the sweep: the discard removes the plant with the tree, the add never
// reads it, and the base tree is built.
const stale = baseWorktreePath(worktree);
git(repo, 'worktree', 'add', '--detach', '-q', stale, baseSha);
const admin = git(
stale,
'rev-parse',
'--path-format=absolute',
'--git-dir',
);
writeFileSync(
join(admin, 'config.worktree'),
'[filter "evil"]\n\tsmudge = touch /tmp/qwen-never\n',
);

const r = run();

expect(r.available).toBe(true);
expect(existsSync(join(admin, 'config.worktree'))).toBe(false);
});

it('adds the base tree with a planted hook and fsmonitor inert', () => {
// The add's initial checkout fires `post-checkout` and refreshes the
// index — running `core.fsmonitor` — from the COMMON dir: one executable
// file and one config write a probe can make. The screen refuses content
// filters (above); these two surfaces are neutralised at the spawn
// instead, and this pins the prefix on THIS command's git — removing it
// creates both markers.
const hooksDir = join(repo, '.git', 'hooks');
git(repo, 'config', 'core.hooksPath', hooksDir);
mkdirSync(hooksDir, { recursive: true });
const hook = join(hooksDir, 'post-checkout');
writeFileSync(hook, `#!/bin/sh\ntouch ${repo}/PWNED-hook\n`);
chmodSync(hook, 0o755);
git(worktree, 'config', 'core.fsmonitor', `touch ${repo}/PWNED-fsm`);

const r = run();

expect(r.available).toBe(true);
expect(existsSync(join(repo, 'PWNED-hook'))).toBe(false);
expect(existsSync(join(repo, 'PWNED-fsm'))).toBe(false);
});

it('ignores an exported GIT_DIR redirect when adding the base tree', () => {
// An exported GIT_DIR overrides repository discovery for every git call
// that inherits it: the add would land in the redirected repository and
Expand Down
66 changes: 64 additions & 2 deletions packages/cli/src/commands/review/base-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,12 @@ import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js';
import { baseWorktreePath } from './lib/paths.js';
import {
discardWorktree,
INERT_GIT_ARGS,
localFilterBreach,
localFilterRefusal,
sanitizedGitEnv,
worktreeCreateFailureDetail,
type LocalFilterBaseline,
type SweepResult,
} from './lib/worktree.js';
import { runBuildTest, type BuildTestReport } from './build-test.js';
Expand Down Expand Up @@ -94,8 +98,11 @@ export interface BaseTreeArgs {
// discovery for every call at once — the base tree would be added into the
// redirected repository and its reuse check would read HEAD from it, an A/B
// against the wrong program while every check against the given tree passes.
// The INERT_GIT_ARGS prefix is for the `worktree add` below: its initial
// checkout fires hooks and `core.fsmonitor` from the common dir otherwise,
// and a probe's plant lands exactly there.
function gitOut(cwd: string, ...args: string[]): string {
const r = spawnSync('git', args, {
const r = spawnSync('git', [...INERT_GIT_ARGS, ...args], {
cwd,
encoding: 'utf8',
env: sanitizedGitEnv(),
Expand All @@ -108,7 +115,7 @@ function gitOut(cwd: string, ...args: string[]): string {
}

function git(cwd: string, ...args: string[]): void {
const r = spawnSync('git', args, {
const r = spawnSync('git', [...INERT_GIT_ARGS, ...args], {
Comment on lines 117 to +118

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] R8-8 (deadline aggregate, location 4 of 5): both git helpers this diff retouches (INERT_GIT_ARGS prefix) spawn with no timeout — the guarded worktree add and the reuse fast-path rev-parse HEAD. Witness (probe, both arms): arm 1 — global-config stall filter (the screen's disclosed limit) + committed attributes: the screen clears, the add blocks the smudge's entire duration (elapsed_ms=30055 for a call that otherwise takes <1s; sleep infinity never returns — event loop stalled in spawnSync, the paired breach re-read never runs, no report at all); arm 2 — reuse fast path with include.path = : runBaseTree never returns; a mid-hang process sample caught the exact spawn git -c core.hooksPath=… -c core.fsmonitor= rev-parse HEAD in wchan=wait_for_partner (FIFO open wait); watchdog rc=124. Fix: timeout: GIT_TIMEOUT_MS on both helpers; a timeout kill then throws through the existing r.error path, so the catch's paired breach re-read still runs.

中文说明

R8-8(期限聚合,共 5 处,第 4 处):本 diff 触碰过的两个 git 助手(加 INERT_GIT_ARGS 前缀)的 spawn 都没有 timeout——受守护的 worktree add 与复用快路径的 rev-parse HEAD。证据(探针,两臂):臂 1——全局配置卡死过滤器(屏蔽的披露限制)+ 已提交的 attributes:屏蔽干净通过,add 阻塞整个 smudge 时长(一次本应 <1 秒的调用 elapsed_ms=30055;sleep infinity 则永不返回——事件循环卡在 spawnSync,成对 breach 复读永不运行,完全没有报告);臂 2——复用快路径 + include.path = :runBaseTree 永不返回;挂起中的进程采样恰好抓到 git -c core.hooksPath=… -c core.fsmonitor= rev-parse HEAD 处于 wchan=wait_for_partner(等待 FIFO 打开);看门狗 rc=124。修复:两个助手都加 timeout: GIT_TIMEOUT_MS;超时杀掉会经既有 r.error 路径抛出,catch 中的成对 breach 复读因此仍能运行。

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

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 — round cap. This round implemented an 8-finding batch within the round cap; this finding is queued for the next round rather than dropped. The GIT_TIMEOUT_MS deadline for the timeout-less spawns is queued as one batch (mechanical but each site needs its own FIFO witness) so the fixes and their witnesses land together.

中文说明

推迟到下一轮 —— 每轮数量上限。本轮在每轮上限内实现了 8 个 finding;该 finding 已排入下一轮队列,不会被丢弃。 为无超时的 spawn 添加 GIT_TIMEOUT_MS 截止时间已作为一个批次排队(改动是机械性的,但每个位置都需要自己的 FIFO 见证),使修复与见证一起落地。

cwd,
encoding: 'utf8',
env: sanitizedGitEnv(),
Expand Down Expand Up @@ -261,12 +268,67 @@ export function runBaseTree(args: BaseTreeArgs): BaseTreeReport {
// The parameter re-narrows: TS narrowing does not cross function scopes.
function buildBaseTree(baseSha: string): BaseTreeReport {
let sweep: SweepResult | undefined;
// Hoisted above the try: the catch below runs the paired re-read when
// the add THREW, and a re-read without the baseline the screen
// captured is blind to every self-erasing shape.
const captured: { baseline: LocalFilterBaseline | null } = {
baseline: null,
};
try {
// Clear a stale base tree left by a crashed run — it would fail `add`. Its
// stderr is kept, because it is usually what explains that failure.
sweep = discardWorktree(worktree, tree);
// BEFORE the checkout runs, directly beside it: the `worktree add`
// below executes whatever the screen detects — the same surface
// `scratch-tree` refuses to reset through, one directory over. The
// screen sits BELOW the sweep, not above it — a plant parked in the
// stale tree's own admin `config.worktree` is state that sweep just
// destroyed and the add never reads, and a screen that ran first
// refused on it forever: every retry re-screened the doomed state and
// wedged the repository out of every review (measured live).
const refusal = localFilterRefusal(

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] R9-1 (location 1 of 2; twin at fetch-pr.ts's screen): sweep→screen→add class at a NEW site — the sweep removes the tree's admin registration BEFORE the screen enumerates candidates, so the NEW tree's own <common>/worktrees/<label>/config.worktree is never a candidate and never enters the baseline. With extensions.worktreeConfig standing in the common config (neither key matches the screen regex, so the screen clears), a writer raced into that file during screen→add is EXECUTED by the add's initial checkout; the plant unsets before the re-read: breach null, available:true, success marker stamped. Round 8's R8-9/R8-10 confirmed this class at the scratch rebuild and probe creation; these are the two add sites round 8 did not name.

Witness: execution premise measured three ways — GIT_TRACE shows worktree add runs the checkout as GIT_DIR=<new-tree>/.git git reset --hard; planting the filter in the new admin config.worktree and running that exact subprocess fired it; a live race planter won 60/60 trials. Detection: self-erasing plant → localFilterBreach null; the same key left standing → non-null.

Suggested fix: baseline the about-to-be-created tree's expected config.worktree as vanished before the add, or treat any <common>/worktrees/*/config.worktree that EXISTS at re-read but is not in baseline.files as a breach.

中文说明

R9-1(共 2 处,第 1 处;孪生站点在 fetch-pr.ts 的屏蔽):sweep→screen→add 类的新站点——清扫在屏蔽枚举候选之前就移除了树的管理注册,因此新树自身的 <common>/worktrees/<label>/config.worktree 永远不会成为候选、永远不入基线。当 common 配置中已存在 extensions.worktreeConfig(这两个键都不匹配屏蔽正则,屏蔽放行)时,在 screen→add 期间竞态写入该文件的植入会被 add 的初始 checkout 执行;植入在复读前 unset:breach 为 null、available:true、成功标记落盘。第 8 轮的 R8-9/R8-10 已在 scratch rebuild 与探针创建确认该类;这里是第 8 轮未点名的两个 add 站点。

证据:执行前提经三种方式实测——GIT_TRACE 显示 worktree addGIT_DIR=<新树>/.git git reset --hard 运行 checkout;把过滤器植入新管理目录 config.worktree 并运行该子进程即触发;实时竞态植入器 60/60 命中。检测侧:自擦除植入 → localFilterBreach 为 null;同一键保留 → 非 null。

建议修复:在 add 之前把即将创建的树的预期 config.worktree 以 vanished 入基线;或把复读时存在但不在 baseline.files 中的任何 <common>/worktrees/*/config.worktree 视为 breach。

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

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 — round cap. This round implemented an 8-finding batch within the round cap; this finding is queued for the next round rather than dropped. Queued with the R8-5 twins (scratch rebuild, probe creation) so all four sweep→screen→add sites get the baseline seed in one coherent change.

中文说明

推迟到下一轮 —— 每轮数量上限。本轮在每轮上限内实现了 8 个 finding;该 finding 已排入下一轮队列,不会被丢弃。 已与 R8-5 同族项(scratch rebuild、probe creation)一起排队,使全部四个 sweep→screen→add 位置在一个一致的变更中获得基线播种。

worktree,
'the worktree add this command runs',
captured,
);
Comment on lines +289 to +293

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] R5-2: This creation checkout is screened but has no paired post-checkout localFilterBreach re-read — the PR gives that re-read to the restore and revert pairs (they are the only two breach call sites), leaving four unpaired sites (this one; fetch-pr.ts:940; scratch reset :277; scratch rebuild :473). A concurrent writer the pipeline itself schedules (a sibling shard's suite, or a setsid'd descendant the reap cannot reach — both named in this PR's own comments) writes filter.evil.smudge into the shared common dir after the screen reads clean but before the add's own config read; the add's initial checkout executes the smudge on the reviewer's machine, the toggler unplants, and every later screen reads clean — the run completes certified clean with attacker code executed.

Witness (probe, this site): plantLanded 12/25, hits 12 — every landed plant fired and the report came back available: true ("base tree built at …"); candidate-fix arm (breach re-read after the add): plantLanded 22/25, hits 0, every landed plant reported available:false naming filter.evil.smudge.

Pair this checkout with localFilterBreach immediately after the add succeeds; on breach return unavailable(breach) (and roll back as the refusal path does).

中文说明

这个创建 checkout 有前置屏蔽,但没有配对的 checkout 后 localFilterBreach 复读——本 PR 只对 restore 与 revert 两对加了该复读(它们是仅有的两处 breach 调用点),留下四个未配对站点(此处;fetch-pr.ts:940;scratch reset :277;scratch rebuild :473)。流水线自己调度的并发写者(兄弟 shard 的套件,或回收够不到的 setsid 子孙——本 PR 自己的注释点名了这两类)在屏蔽读干净之后、add 自己读配置之前,把 filter.evil.smudge 写入共享 common 目录;add 的初始 checkout 就会在审查者机器上执行该 smudge,切换器随后撤除植入,之后每一次屏蔽都读到干净——运行以"干净"认证结束,而攻击者代码已经执行。证据(探针,本站点):plantLanded 12/25, hits 12——每个落地的植入都触发,报告仍为 available: true;候选修复臂(add 后加 breach 复读):plantLanded 22/25, hits 0,每个落地的植入都报告 available:false 并点名 filter.evil.smudge。修复:add 成功后立即配对 localFilterBreach,命中时返回 unavailable(breach)(并像拒绝路径一样回滚)。

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

Comment on lines +289 to +293

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] R5-23 (location 2 of 3): the base-tree worktree add gate carries the same fresh-add false-positive as the fetch-pr gate — see that thread for the full finding, witness, and fix direction (per-gate candidate scope: at fresh-add gates read only <common>/config, the common-root config.worktree, and the include closure). A worktree-local filter in any sibling worktree refuses every base-tree build although the add cannot read it.

中文说明

R5-23(共 3 处,第 2 处):base-tree 的 worktree add 关口带有与 fetch-pr 关口相同的新建 add 误报——完整发现、证据与修复方向见该评论串(按关口划分候选范围:新建 add 关口只读 <common>/config、common 根的 config.worktree 与 include 闭包)。任一兄弟工作树中的工作树本地过滤器都会拒绝每一次 base-tree 构建,尽管 add 读不到它。

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

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: batch capped at the Critical findings. Still valid — same residue-note gap in base-tree's build-gate failure path; queued with R5-22/R5-23.

推迟到下一轮:本轮批次以 Critical 发现为上限。仍然有效——base-tree 构建门失败路径存在同样的残留物注记缺口;与 R5-22/R5-23 一起排队。

if (refusal !== null) return unavailable(refusal);
git(worktree, 'worktree', 'add', '--detach', tree, baseSha);

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] R8-7 (location 3 of 5): the base-tree add has no placement re-check between its screen and this add. The base tree's path shares the ancestor .qwen/tmp with the review worktree (paths.ts builds -base); an attacker with write access to .qwen — the exact threat model this PR's breach tests defend against — swaps that ancestor to a dir holding a consistent copy of the review worktree whose .git gitfile points back at the real common dir, so the screen and the baseline's identity both resolve clean through the copy, and this add then checks out into the attacker dir. Auditor probed live: git worktree list afterwards records the attacker-dir realpath; the breach re-read answers clean (identity compares the unchanged common dir; changedScreenedFiles stats only baselined config files); pre-planted untracked files the checkout will not remove (e.g. a .npmrc aiming npm at an attacker registry) are what the immediately following npm ci build then executes inside. Fix: between the screen and this add, walk the target and its ancestors within the stop boundary and refuse on any symlink.

中文说明

R8-7(共 5 处,第 3 处):base-tree 的 add 在屏蔽与这个 add 之间没有放置复核。base 树路径与 review 工作树共享祖先 .qwen/tmp(paths.ts 构造 -base);对 .qwen 有写权限的攻击者——本 PR 的 breach 测试所防御的那个威胁模型——把该祖先替换为一个目录,其中放着 review 工作树的一致副本、其 .git gitfile 指回真实 common 目录,于是屏蔽与基线身份都透过副本解析为干净,而这个 add 会把 checkout 写进攻击者目录。审计者实测:事后 git worktree list 记录的是攻击者目录的 realpath;breach 复读回答干净(身份比较的是未改变的 common 目录;changedScreenedFiles 只 stat 入基线的配置文件);攻击者预植的、checkout 不会删除的未跟踪文件(例如把 npm 指向攻击者 registry 的 .npmrc)会在紧随其后的 npm ci 构建中被执行。修复:在屏蔽与这个 add 之间,对停止边界内的目标及其祖先做遍历,出现任何符号链接即拒绝。

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

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 — round cap. This round implemented an 8-finding batch within the round cap; this finding is queued for the next round rather than dropped. Placement re-validation (an ancestor symlink swapped between the screen and the add) spans five sites; it is queued as one batch so the shared helper and its witnesses land together.

中文说明

推迟到下一轮 —— 每轮数量上限。本轮在每轮上限内实现了 8 个 finding;该 finding 已排入下一轮队列,不会被丢弃。 放置位置再验证(在 screen 与 add 之间交换祖先符号链接)横跨五个位置;已作为一个批次排队,使共享辅助函数与其见证测试一起落地。

// Re-read AFTER the checkout, paired with the screen above: the
// screen is a point-in-time read, and a concurrent writer the
// pipeline itself schedules (a sibling shard's suite, a toggler the
// reap did not reach) can plant between the two reads — the add's
// initial checkout executes the plant while both reads see nothing.
// A key that APPEARED — or a screened file that CHANGED, the trace
// a self-erasing plant leaves when its unset lands before this
// re-read — is reported as a breach and the just-added tree rolled
// back: the run is never certified clean with a REPO-LOCAL plant
// behind it. A filter defined only in the GLOBAL config is the
// screen's disclosed limit and leaves no repo-local trace the
// re-read can see.
const breach = localFilterBreach(
worktree,
'the worktree add this command ran',
captured.baseline,
);
if (breach !== null) {
discardWorktree(worktree, tree);

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] R9-10 (location 3 of 3; twins: fetch-pr.ts:999, test-efficacy.ts:1647): the breach-rollback call runs discardWorktree — whose git spawns (worktree remove --force, unlock, remove --force --force, plus dropWorktreeRegistration's rev-parse fallback) carry NO timeout — BEFORE returning the already-computed breach report. A FIFO planted where git's config read follows it blocks the rollback forever: the detected breach is never reported, the command hangs, and the finally rmSync(lock) never runs — the build lock leaks until the 30-minute stale sweep.

Witness: probe — with /config replaced by a FIFO, timeout 5 git worktree remove --force wt → exit=124.

Suggested fix: give discardWorktree's spawns timeout: GIT_TIMEOUT_MS, AND wrap the rollback so the breach note returns regardless (const note = breach; try { discardWorktree(worktree, tree); } catch {} return unavailable(note);).

中文说明

R9-10(共 3 处,第 3 处;孪生:fetch-pr.ts:999、test-efficacy.ts:1647):breach 回滚调用在返回已经算好的 breach 报告之前运行 discardWorktree——其 git spawn(worktree remove --force、unlock、remove --force --force,外加 dropWorktreeRegistration 的 rev-parse 回退)都没有 timeout。植入在 git 配置读取会跟随之处的 FIFO 会永久阻塞回滚:已检测到的 breach 永不被报告、命令挂起、finally 的 rmSync(lock) 永不执行——构建锁泄漏直到 30 分钟的陈旧清扫。

证据:探针——/config 被换成 FIFO 时,timeout 5 git worktree remove --force wt → exit=124。

建议修复:给 discardWorktree 的 spawn 加 timeout: GIT_TIMEOUT_MS,并包裹回滚使 breach 注记无论如何都返回(const note = breach; try { discardWorktree(worktree, tree); } catch {} return unavailable(note);)。

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

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 — round cap. This round implemented an 8-finding batch within the round cap; this finding is queued for the next round rather than dropped. Queued with the R8-8 timeout batch (the rollback / runRestore / discardWorktree spawns).

中文说明

推迟到下一轮 —— 每轮数量上限。本轮在每轮上限内实现了 8 个 finding;该 finding 已排入下一轮队列,不会被丢弃。 已与 R8-8 超时批次一起排队(rollback / runRestore / discardWorktree 的 spawn)。

return unavailable(breach);
}
} catch (e) {
// A checkout that THREW may still have executed a plant first — a
// smudge that kills git mid-checkout fires and only THEN makes the
// spawn throw — and the failure detail below would bury the
// execution under an add error. Attribute it while the baseline the
// screen captured still stands; the next call's screen may read
// clean, the plant having erased itself.
if (captured.baseline !== null) {
const breach = localFilterBreach(
worktree,
'the worktree add this command ran',
captured.baseline,
);
if (breach !== null) return unavailable(breach);
}
return unavailable(
worktreeCreateFailureDetail('base', e, String(sweep?.stderr ?? '')),
);
Expand Down
Loading
Loading