Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
971494d
fix(review): screen content filters before the probe tree's restore t…
wenshao Aug 20, 2026
a25e8e6
fix(review): screen every pipeline checkout and fail closed on unread…
qwen-code-ci-bot Aug 22, 2026
1fe2fae
fix(review): screen every creation checkout and inert the probe check…
qwen-code-ci-bot Aug 23, 2026
49ba19a
fix(review): inert creation checkouts and close the include, FIFO, an…
qwen-code-ci-bot Aug 23, 2026
c06aab3
fix(review): close the newline fail-open, bound refusals, and pin the…
qwen-code-ci-bot Aug 23, 2026
4c5bff7
fix(review): close the recursion, transport-key, EACCES, and teardown…
qwen-code-ci-bot Aug 23, 2026
c959a08
fix(review): screen the head fetch, complete the transport set, gate …
qwen-code-ci-bot Aug 23, 2026
c137f03
fix(review): pin the gate's shape and HEAD, bound the screen spawns, …
qwen-code-ci-bot Aug 24, 2026
deba877
Merge remote-tracking branch 'origin/main' into fix/review-filter-scr…
qwen-code-ci-bot Aug 24, 2026
8cfb0ab
fix(review): NUL-read the screen's values, widen the destination judg…
qwen-code-ci-bot Aug 24, 2026
5258b5e
fix(review): judge refspec grammar fail-closed, screen the main confi…
qwen-code-ci-bot Aug 24, 2026
c9cec1e
fix(review): remove the probe verdict channel on every exit path (#9741)
qwen-code-ci-bot Aug 24, 2026
eb6a46b
Merge branch 'main' into fix/review-filter-screen-v2
qwen-code-dev-bot Aug 24, 2026
1567ea0
fix(review): close the round-10/11 config-screen holes named by revie…
Aug 24, 2026
7e65377
test(cli): stabilize gate-rejected update and serve-wiring tests (#9741)
Aug 25, 2026
ad017a8
Merge branch 'main' into fix/review-filter-screen-v2
Aug 25, 2026
526575b
Merge branch 'main' into fix/review-filter-screen-v2
qwen-code-dev-bot Aug 25, 2026
832ad51
Merge branch 'main' into fix/review-filter-screen-v2
wenshao Aug 26, 2026
776e147
Merge branch 'main' into fix/review-filter-screen-v2
qwen-code-dev-bot Aug 26, 2026
377ec3a
Merge branch 'main' into fix/review-filter-screen-v2
qwen-code-dev-bot Aug 27, 2026
4c4eca3
Merge branch 'main' into fix/review-filter-screen-v2
qwen-code-dev-bot Aug 27, 2026
90e1494
fix(review): inert the remaining branch deletes and resume probe, gat…
Aug 27, 2026
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
123 changes: 121 additions & 2 deletions packages/cli/src/commands/review/base-tree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
// actually succeeded, since an A/B against a half-built tree measures the build,
// not the diff.

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { execFileSync } from 'node:child_process';
import {
appendFileSync,
chmodSync,
utimesSync,
mkdtempSync,
mkdirSync,
Expand All @@ -29,8 +31,28 @@ 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 { gitConfigPath } from './lib/test-utils.js';
import type { BuildTestReport } from './build-test.js';

// The race witness needs a writer landing BETWEEN the opening screen and the
// creation add; the sweep is the code that runs between them, so it is the
// seam. Defaults to the real implementation — every other test runs it.
const seam = vi.hoisted(() => ({
realDiscard: undefined as ((...args: unknown[]) => unknown) | undefined,
beforeDiscard: undefined as (() => void) | undefined,
}));
vi.mock('./lib/worktree.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('./lib/worktree.js')>();
seam.realDiscard = actual.discardWorktree as (...a: unknown[]) => unknown;
return {
...actual,
discardWorktree: (...args: unknown[]) => {
seam.beforeDiscard?.();
return seam.realDiscard!(...args);
},
};
});

const okBuild = {
ok: true,
toolchain: 'npm',
Expand Down Expand Up @@ -94,7 +116,10 @@ describe('runBaseTree', () => {
git(repo, 'worktree', 'add', '--detach', '-q', worktree, headSha);
});

afterEach(() => rmSync(repo, { recursive: true, force: true }));
afterEach(() => {
seam.beforeDiscard = undefined;
rmSync(repo, { recursive: true, force: true });
});

it('creates a sibling worktree holding the BASE commit, not the head', () => {
const r = run();
Expand All @@ -113,6 +138,100 @@ describe('runBaseTree', () => {
expect(r.path).toBe(`${worktree}-base`);
});

it('REFUSES the creation checkout when the common config plants a content filter', () => {
// The creation checkout rewrites every file the base commit carries,
// which EXECUTES a planted filter, and nothing in the pipeline wipes the
// common dir the plant persists in. The screen must refuse the build the
// way the probe-creation screen refuses its checkout — an unavailable A/B
// (infrastructure), never an executed filter.
const pwned = join(repo, 'PWNED-base-create');
appendFileSync(
join(repo, '.git', 'config'),
`[filter "evil"]\n\tsmudge = touch ${gitConfigPath(pwned)}\n`,
);
mkdirSync(join(repo, '.git', 'info'), { recursive: true });
writeFileSync(join(repo, '.git', 'info', 'attributes'), '* filter=evil\n');

const builds: string[] = [];
const r = run({}, (w) => {
builds.push(w);
return okBuild;
});

expect(r.available).toBe(false);
expect(r.note).toContain('content filter');
expect(builds).toHaveLength(0);
expect(existsSync(pwned)).toBe(false);
expect(existsSync(baseWorktreePath(worktree))).toBe(false);
});

it('the creation checkout is INERT — a planted post-checkout hook never fires', () => {
// The screen certifies FILTERS only; `worktree add` still fires
// `post-checkout` from the shared common hooks dir (measured live, git
// 2.39 and 2.43, on this exact `--detach` shape). A probe writes the
// hook with the same facility as the filter plant — one write + chmod —
// so the creation spawn must carry the inert overrides too.
const pwned = join(repo, 'PWNED-base-hook');
mkdirSync(join(repo, '.git', 'hooks'), { recursive: true });
writeFileSync(
join(repo, '.git', 'hooks', 'post-checkout'),
`#!/bin/sh\ntouch ${pwned}\n`,
);
chmodSync(join(repo, '.git', 'hooks', 'post-checkout'), 0o755);

const r = run();

// Inert is not "the build failed": the tree came up and the build ran.
expect(r.available).toBe(true);
expect(existsSync(pwned)).toBe(false);
});

it('refuses a planted core.fsmonitor at the screen — and never executes it', () => {
// fsmonitor rides the refusal regex on the include posture: a planted
// command is execution the screen cannot certify. The creation refuses
// naming the key — and, refused or not, the planted command never
// fires. (The creation spawn ALSO carries the empty `-c` override, as
// defense against a config swapped in the screen-to-spawn window.)
const pwned = join(repo, 'PWNED-base-fsmonitor');
appendFileSync(
join(repo, '.git', 'config'),
'[core]\n\tfsmonitor = touch ' + gitConfigPath(pwned) + '\n',
);

const r = run();

expect(r.available).toBe(false);
expect(r.note).toContain('core.fsmonitor');
expect(existsSync(pwned)).toBe(false);
});

it('re-screens after the sweep — a plant between the screen and the add is refused', () => {
// The lock excludes other base-tree builders, not shards running attacker
// code: a concurrent probe can land the two-write plant the instant the
// stale tree's sweep finishes — after the opening screen read, before
// the creation add re-parses the config (measured live: 6/6 race
// iterations executed the plant). The seam stands in for that writer.
const pwned = join(repo, 'PWNED-base-race');
seam.beforeDiscard = () => {
appendFileSync(
join(repo, '.git', 'config'),
`[filter "evil"]\n\tsmudge = touch ${gitConfigPath(pwned)}\n`,
);
mkdirSync(join(repo, '.git', 'info'), { recursive: true });
writeFileSync(
join(repo, '.git', 'info', 'attributes'),
'* filter=evil\n',
);
};

const r = run();

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

it('builds in the base tree, and only there', () => {
const seen: string[] = [];
const r = run({}, (w) => {
Expand Down
36 changes: 35 additions & 1 deletion packages/cli/src/commands/review/base-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js';
import { baseWorktreePath } from './lib/paths.js';
import {
discardWorktree,
INERT_GIT_ARGS,
localFilterRefusal,
sanitizedGitEnv,
worktreeCreateFailureDetail,
type SweepResult,
Expand Down Expand Up @@ -260,12 +262,44 @@ export function runBaseTree(args: BaseTreeArgs): BaseTreeReport {

// The parameter re-narrows: TS narrowing does not cross function scopes.
function buildBaseTree(baseSha: string): BaseTreeReport {
// The creation checkout rewrites every file the base commit carries, which
// EXECUTES a planted filter — screen it the way the probe tree's creation
// is screened. The refusal reads as an unavailable A/B (infrastructure),
// never as a finding against the PR.
const filterRefusal = localFilterRefusal(
worktree,
"the base tree's creation checkout",
);
Comment on lines +269 to +272

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] R3-1 (same defect as the fetch-pr.ts comment): the base tree's creation checkout — git(worktree, 'worktree', 'add', '--detach', tree, baseSha) below — fires post-checkout from the shared common hooks dir, which the new screen cannot see (it matches only filter.* keys) and this spawn does not disable. Witness and full trace are on the R3-1 comment at fetch-pr.ts:979: a planted common-dir post-checkout FIRED on the worktree add shape (git 2.43) and was suppressed by -c core.hooksPath=/dev/null/no-hooks. Mirror ALL of INERT_GIT_ARGS (hooksPath AND the empty fsmonitor override, per R3-1/R3-4) on this spawn — the local git() helper needs the overrides at the call site or as a parameter.

中文说明

R3-1(与 fetch-pr.ts 评论同一缺陷):base 树的创建 checkout——下方的 git(worktree, 'worktree', 'add', '--detach', tree, baseSha)——会从共享的 common 钩子目录触发 post-checkout,新屏蔽看不见它(只匹配 filter.* 键),该 spawn 也没有禁用钩子。见证与完整追踪见 fetch-pr.ts:979 的 R3-1 评论:植入的 common dir post-checkout 在 worktree add 形态上触发(git 2.43),被 -c core.hooksPath=/dev/null/no-hooks 抑制。请把完整的 INERT_GIT_ARGS(hooksPath 与空 fsmonitor 覆盖,见 R3-1/R3-4)同步到该 spawn——本地 git() 辅助函数需要在调用点或以参数形式带上这些覆盖。

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

Comment on lines +269 to +272

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] R3-3: Check-then-use race at this call site: the screen reads the common-dir config here, then discardWorktree runs, then the creation git worktree add below re-reads config — with no re-check. runBaseTree's own lock exists precisely because probes run concurrently ("another probe is building the base tree right now"), the code under review documents that this pipeline runs discards and adds concurrently against one common dir, and the lock excludes only other base-tree builders — nothing excludes a shard running attacker code. The fetch-pr call site is NOT exposed (it precedes all probe execution); only this base-tree site overlaps live probes.

Failure path: during review of a malicious PR, probes start after fetch-pr's screened creation checkout; while a benign shard builds the base tree, the malicious probe watches the public lock dir and writes [filter "evil"] smudge = CMD into <common>/config plus * filter=evil into info/attributes the instant the stale base tree's directory vanishes (sweep finished — after this screen's read). The screen certified clean; the creation checkout executes the planted smudge over every file of the base commit.

Witness (race probe at HEAD, unmodified PR code, concurrent writer process):

race 6/6: [writer +589ms] stale base tree gone (sweep finished) - planting now
          -> {"available":true,"pwned":true}
control-clean: pwned:false | preplant: refused correctly
flip (re-screen right after discardWorktree, before the add): 3/3 {"available":false,"pwned":false}

Re-run localFilterRefusal immediately before the worktree add (after the sweep) and treat a hit as compromise — discard the just-created tree and return unavailable(refusal); the variant that closes the TOCTOU outright is refusing creation checkouts while any probe with shell access is concurrently alive.

中文说明

该调用点存在"先检查后使用"竞态:屏蔽在此读取 common dir 配置,随后 discardWorktree 运行,再下方创建用的 git worktree add 重新读取配置——中间没有复查。runBaseTree 自己的锁正是因为探针并发运行而存在("另一个探针正在构建 base 树"),受审代码自身也记载该管线对同一个 common dir 并发执行 discard 与 add,而该锁只排斥其他 base 树构建者——并不排斥正在运行攻击者代码的分片。fetch-pr 调用点不暴露(它先于一切探针执行);只有 base-tree 这个点与活着的探针重叠。

失败路径:审查恶意 PR 时,探针在 fetch-pr 已屏蔽的创建 checkout 之后启动;良性分片构建 base 树期间,恶意探针监视公开的锁目录,在旧 base 树目录消失(清扫完成——在本屏蔽读取之后)的瞬间向 <common>/config 写入 [filter "evil"] smudge = CMD 并向 info/attributes 追加 * filter=evil。屏蔽证明干净;创建 checkout 把被植入的 smudge 执行在 base 提交的每个文件上。

见证(在 HEAD 上用未改动的 PR 代码做竞态探针,并发写者进程):竞态 6/6:[写者 +589ms] 旧 base 树消失(清扫完成)——现在植入{"available":true,"pwned":true};对照-干净:pwned:false;预植入:正确拒绝;翻转(在 discardWorktree 之后、add 之前重新屏蔽):3/3 {"available":false,"pwned":false}

worktree add 之前(清扫之后)重新运行 localFilterRefusal,命中即按沦陷处理——丢弃刚创建的树并返回 unavailable(refusal);彻底关闭该 TOCTOU 的变体是:当任何拥有 shell 权限的探针并发存活时,拒绝创建 checkout。

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

Comment on lines +269 to +272

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] R3-4 (same defect as the fetch-pr.ts comment): the base tree's creation checkout below runs through the local git() helper with no -c core.fsmonitor= override, so a repo-local core.fsmonitor = CMD — invisible to the filter-only screen — executes during git worktree add --detach. Witness and full trace are on the R3-4 comment at fetch-pr.ts:979 (probe at HEAD: marker=FIRED on the --detach shape, not-fired with -c core.fsmonitor=). Fix both creation spawns together: mirror INERT_GIT_ARGS — '-c', 'core.hooksPath=/dev/null/no-hooks' (per R3-1) AND '-c', 'core.fsmonitor=' — or extend the screen to refuse on core.fsmonitor.

中文说明

R3-4(与 fetch-pr.ts 评论同一缺陷):下方 base 树的创建 checkout 经本地 git() 辅助函数运行、没有 -c core.fsmonitor= 覆盖,因此仓库本地的 core.fsmonitor = CMD——对仅匹配过滤器的屏蔽不可见——会在 git worktree add --detach 期间执行。见证与完整追踪见 fetch-pr.ts:979 的 R3-4 评论(在 HEAD 上探针:--detach 形态标记=触发,-c core.fsmonitor= 下未触发)。请一并修复两个创建 spawn:同步 INERT_GIT_ARGS——'-c', 'core.hooksPath=/dev/null/no-hooks'(按 R3-1)与 '-c', 'core.fsmonitor='——或把屏蔽扩展为对 core.fsmonitor 拒绝。

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

if (filterRefusal) return unavailable(filterRefusal);
let sweep: SweepResult | undefined;
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);
git(worktree, 'worktree', 'add', '--detach', tree, baseSha);
// Re-screen beside the add, after the sweep: this is the only screened
// checkout that overlaps LIVE probes — the lock excludes other base-tree
// builders, not shards running attacker code, and the sweep's completion
// is the public signal a watcher plants on. A filter landing between the
// screen above and the add below executed in the creation checkout the
// screen certified clean (measured: 6/6 race iterations pwned); the
// re-screen narrows that window to the gap between itself and the add.
const rescreenRefusal = localFilterRefusal(
worktree,
"the base tree's creation checkout",
);
if (rescreenRefusal) return unavailable(rescreenRefusal);
// INERT_GIT_ARGS: the screen reads filters only, while `worktree add`
// also fires `post-checkout` from the shared common hooks dir and runs
// a repo-local `core.fsmonitor` — both plantable, both measured live.
git(
worktree,
...INERT_GIT_ARGS,
'worktree',
Comment on lines +294 to +297

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] R11-3: Still stands from round 11 (witness upgraded this round). base-tree's git helpers (gitOut at base-tree.ts:99 and git at :112) carry no timeout/killSignal; the reuse fast path's gitOut(tree, 'rev-parse', 'HEAD') runs BEFORE any screen, the creation worktree add --detach spawn this diff edited to add INERT_GIT_ARGS stays on the timeout-less helper, and discardWorktree's worktree remove/unlock spawns between screen1 and screen2 share the shape — an incomplete closure of the unbounded-spawn class this same diff documents ('a screen spawn must still END against a config that blocks in open()').

A probe shard does one mkfifo+rename over a baseSha loose object — outside the screen's candidate set, which reads config/info files only; both screens pass, then git worktree add --detach blocks in open() with nothing that can end it. The FIFO persists in the never-wiped common dir: a permanent wedge of the base-tree facility; every later base-tree request re-hangs.

Witness (this round, probe through the real runBaseTree):

unpatched, FIFO'd baseSha object -> never returned; watchdog SIGKILL at 25s (EXIT=137)
flip: timeout+SIGKILL on the helper -> returned after 5092ms, available:false, 'spawnSync git ETIMEDOUT'

Fix: bound the helper (or at least the creation/sweep/fast-path spawns) with a checkout-sized ceiling plus killSignal: 'SIGKILL'; the existing catch already converts the kill into unavailable(...). A base-tree.test.ts case replacing one of baseSha's loose objects with a FIFO before run() should assert available === false with a bounded return; remove the timeout and the test hangs instead.

中文说明

自第 11 轮起仍然存在(本轮见证升级)。base-tree 的 git 助手(base-tree.ts:99 的 gitOut 与 :112 的 git)不带 timeout/killSignal;复用快路径的 gitOut(tree, 'rev-parse', 'HEAD') 在任何屏幕之前运行,本 diff 编辑来添加 INERT_GIT_ARGS 的创建 worktree add --detach spawn 仍然走无超时的助手,screen1 与 screen2 之间 discardWorktree 的 worktree remove/unlock spawn 也是同一形态——这是同一 diff 自己记录在案的无界 spawn 类别的不完整闭合。

探针分片对 baseSha 松散对象做一次 mkfifo+rename——在屏幕候选集之外(屏幕只读 config/info 文件);两个屏幕都通过,随后 git worktree add --detach 在 open() 阻塞,没有任何东西能终止它。FIFO 持久存在于永不清除的公共目录:base-tree 设施的永久楔子;之后每次 base-tree 请求都会再次挂起。

见证(本轮,通过真实 runBaseTree 的探针):未打补丁时永不返回(25s 看门狗 SIGKILL,EXIT=137);给助手加 timeout+SIGKILL 后 5092ms 返回 available:false、'spawnSync git ETIMEDOUT'。

修复:给助手(至少创建/清扫/快路径 spawn)加 checkout 量级的上限 + SIGKILL;请附带 FIFO 松散对象的测试与突变检查。

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

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 (budget-warning round). Fix stands: bound base-tree’s git helpers with a checkout-sized ceiling plus killSignal: 'SIGKILL', with the FIFO’d-loose-object witness. Batched with the other unbounded-spawn items.

中文说明

顺延到下一轮(预算警告轮)。修复不变:给 base-tree 的 git 助手加 checkout 量级上限与 killSignal: 'SIGKILL',附 FIFO 松散对象见证。与其他无界 spawn 项合并一批。

Comment on lines +295 to +297

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] R11-3: Still stands at round 15 (from round 11; witness upgraded in round 12, re-traced this round). base-tree's git helpers (gitOut :99, git :112) carry no timeout/killSignal; the reuse fast path's rev-parse runs BEFORE any screen, the creation worktree add this diff edited to add INERT_GIT_ARGS stays on the timeout-less helper, and discardWorktree's remove/unlock spawns between screen1 and screen2 share the shape — an incomplete closure of the unbounded-spawn class this same diff documents. Witness (round-12 probe through real runBaseTree): FIFO'd baseSha loose object → never returned, watchdog SIGKILL at 25s; flip with timeout+SIGKILL → returned after 5092ms available:false ETIMEDOUT. Helpers unchanged at this commit. Fix: bound the helpers (at least the creation/sweep/fast-path spawns) with a checkout-sized ceiling plus killSignal: 'SIGKILL'; the existing catch converts the kill into unavailable(...). A base-tree.test.ts case FIFO'ing one of baseSha's loose objects must assert a bounded available === false; remove the timeout and the test hangs.

中文说明

R11-3:第 15 轮仍然存在(第 11 轮提出;第 12 轮见证升级,本轮复核)。base-tree 的 git 助手(gitOut :99、git :112)不带 timeout/killSignal;复用快路径的 rev-parse 先于一切屏蔽,本 diff 刚加了 INERT_GIT_ARGS 的创建 worktree add 仍跑在无超时助手上,screen1 与 screen2 之间 discardWorktree 的 remove/unlock 同形——同一 diff 自己文档化的无界 spawn 类别的不完整闭合。见证(第 12 轮经真实 runBaseTree 的探针):FIFO 化的 baseSha 松散对象 → 永不返回、25s 看门狗 SIGKILL;加 timeout+SIGKILL 后 5092ms 返回 available:false。本提交助手未变。修复:给助手(至少创建/清扫/快路径 spawn)加 checkout 量级上限与 killSignal: 'SIGKILL'。请补 FIFO 松散对象测试并做突变验证。

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

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 (budget-warning round). Fix stands: bound base-tree’s git helpers with a checkout-sized ceiling plus killSignal: 'SIGKILL', with the FIFO’d-loose-object witness. Batched with the other unbounded-spawn items.

中文说明

顺延到下一轮(预算警告轮)。修复不变:给 base-tree 的 git 助手加 checkout 量级上限与 killSignal: 'SIGKILL',附 FIFO 松散对象见证。与其他无界 spawn 项合并一批。

'add',
Comment on lines +294 to +298

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] R11-3: Still stands from round 11 (base-tree.ts is interaction-only this round; the helpers are unchanged). base-tree's git helpers (gitOut at base-tree.ts:99 and git at :112) carry no timeout/killSignal, and the reuse fast path's gitOut(tree, 'rev-parse', 'HEAD') (~:184) runs BEFORE any screen — a FIFO at /config hangs the process instead of producing the refusal the screen exists for. The creation spawn anchored here (which this diff edited to add INERT_GIT_ARGS) stays on the timeout-less helper, and discardWorktree's worktree remove/unlock spawns between screen1 and screen2 share the shape — an incomplete closure of the unbounded-spawn class this same diff documents ('a screen spawn must still END against a config that blocks in open()').

Failure scenario: A probe (or a prior run's residue — the common dir is never wiped) mkfifos /config; any base-tree run — including the reuse fast path, which has no screen ahead of it at all — blocks in open() on the first config-reading spawn, permanently, with no refusal path; the event loop is blocked so no in-process bound can fire.

Witness: Round-11 trace stands; code unchanged at this commit (base-tree.ts:99-110 spawnSync carries only cwd/encoding/env).

Suggested fix: Give base-tree's git/gitOut helpers timeout: SCREEN_SPAWN_TIMEOUT_MS, killSignal: 'SIGKILL'; the existing unavailable(...) machinery already converts throws into named refusals.

中文说明

第 11 轮提出、仍然成立(base-tree.ts 本轮仅为交互文件,辅助函数未变)。base-tree 的 git 辅助(:99 的 gitOut 与 :112 的 git)不带 timeout/killSignal,而复用快速路径的 gitOut(tree, 'rev-parse', 'HEAD')(~:184)在任何 screen 之前运行——/config 的 FIFO 让进程挂起,而不是产生 screen 本要给出的拒绝。此处锚定的创建 spawn(本 diff 刚加了 INERT_GIT_ARGS)仍跑在无超时的辅助上,screen1 与 screen2 之间 discardWorktree 的 worktree remove/unlock spawn 同形——本 diff 自己文档化的无界 spawn 类别("screen spawn 面对阻塞在 open() 的配置也必须能结束")的不完整闭合。

**失败路径:**探针(或前次运行的残留——common dir 从不清理)把 /config 换成 FIFO;任何 base-tree 运行——包括前面根本没有 screen 的复用快速路径——在第一个读配置的 spawn 上永久阻塞于 open(),无拒绝路径;事件循环被阻塞,进程内界无法触发。

**修复建议:**给 base-tree 的 git/gitOut 辅助加 timeout: SCREEN_SPAWN_TIMEOUT_MS, killSignal: 'SIGKILL';现有 unavailable(...) 机制已把抛错转为命名拒绝。

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

Comment on lines +295 to +298

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] R11-3: Still stands at round 16 (from round 11; witness upgraded in round 12, re-traced in round 15). base-tree's git helpers (gitOut :99, git :112) carry no timeout/killSignal; the reuse fast path's rev-parse runs BEFORE any screen, the creation worktree add this diff edited to add INERT_GIT_ARGS stays on the timeout-less helper, and discardWorktree's remove/unlock spawns between screen1 and screen2 share the shape — an incomplete closure of the unbounded-spawn class this same diff documents.

Failure scenario: Round-12 probe through real runBaseTree: FIFO'd baseSha loose object → never returned, watchdog SIGKILL at 25s; flip with timeout+SIGKILL → returned after 5092ms available:false ETIMEDOUT. Helpers unchanged at this commit.

Witness: round-12 probe through real runBaseTree (quoted in the round-15 re-post): FIFO arm never returned until the 25s watchdog kill; bounded flip returned in 5092ms with available:false.

Suggested fix: Bound the helpers (at least the creation/sweep/fast-path spawns) with a checkout-sized ceiling plus killSignal: 'SIGKILL'; the existing catch converts the kill into unavailable(...).

Fix witness: please land this with the pinning test — A base-tree.test.ts case FIFO'ing one of baseSha's loose objects must assert a bounded available === false; remove the timeout and the test hangs.

中文说明

R11-3:第 16 轮仍然存在(第 11 轮提出;第 12 轮见证升级)。base-tree 的 git 助手无 timeout/killSignal;复用快路径的 rev-parse 先于屏蔽,创建 worktree add 仍在无超时助手上,screen1/screen2 之间的 discardWorktree spawn 同形。第 12 轮探针:FIFO 化松散对象 → 25s 看门狗杀;加超时翻转臂 5092ms 返回 available:false。

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

'--detach',
tree,
baseSha,
);
Comment on lines +294 to +302

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] R11-3: base-tree's git helpers (gitOut ~line 100 and git ~line 113) carry no timeout/killSignal, and the reuse fast path's gitOut(tree, 'rev-parse', 'HEAD') (~line 184) runs BEFORE any screen — a FIFO at /config hangs the process instead of producing the refusal the screen exists for. The creation spawn this diff edits (~here, adding INERT_GIT_ARGS) stays on the timeout-less helper, and discardWorktree's worktree remove/unlock spawns between screen1 and screen2 share the shape — an incomplete closure of the unbounded-spawn class this same diff documents ('a screen spawn must still END against a config that blocks in open()').

Witness:

linked worktree of a repo whose common config is a FIFO:
timeout 3 git -C <wt> rev-parse HEAD → exit 124 (the exact fast-path shape)
regular config → exit 0

Failure path: shard A builds the base tree (marker written); a concurrent writer mkfifo+renames the common config; shard B's base-tree call reaches the fast path and blocks forever in open() — no refusal note, because the hang precedes every screen.

Fix: give both helpers timeout + killSignal: 'SIGKILL' (SCREEN_SPAWN_TIMEOUT_MS for the reads, a larger constant for the checkout/add); the fast path's try/catch already falls through to the rebuild path, whose opening screen then refuses fail-closed.

中文说明

R11-6:base-tree 的两个 git 辅助函数(gitOut 约第 100 行、git 约第 113 行)不带 timeout/killSignal,而复用快路径的 gitOut(tree, 'rev-parse', 'HEAD')(约第 184 行)在任何 screen 之前运行——/config 是 FIFO 时进程直接挂起,而不是得到 screen 本应给出的拒绝。本 diff 编辑的创建 spawn(此处,加入 INERT_GIT_ARGS)仍然用无 timeout 的辅助函数,screen1 与 screen2 之间 discardWorktree 的 worktree remove/unlock spawn 同形——这是同一 diff 所记录的无界 spawn 类别的一次不完整闭合。见证:在 common config 为 FIFO 的仓库链接 worktree 中,timeout 3 git rev-parse HEAD → exit 124;普通配置 → exit 0。失败路径:分片 A 建好 base 树(标记已写);并发写者 mkfifo+rename 换掉 common config;分片 B 的 base-tree 调用走到快路径并在 open() 中永久阻塞——没有任何拒绝记录,因为挂起先于一切 screen。修复:给两个辅助函数加 timeout + killSignal: 'SIGKILL'(读用 SCREEN_SPAWN_TIMEOUT_MS,checkout/add 用更大的常量);快路径的 try/catch 本就会落入重建路径,其开头 screen 会失败关闭地拒绝。

— 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.

Still open — deferred to the next round. This round was a same-run verification repair: the previous commit was rejected because unrelated full-suite tests failed under CI load, and the round's single commit fixes exactly that rejection (see the round summary). No review-pipeline code changed, so this finding's fix stays queued for the next round's batch.

中文说明

仍然打开——顺延至下一轮。本轮是同轮验证修复:上一个提交因与本次改动无关的全套件测试在 CI 负载下失败而被门禁拒绝,本轮的单一提交精确修复该拒绝(见轮次总结)。review 管线代码未变动,该发现的修复继续排入下一轮批次。

Comment on lines +294 to +302

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] R11-3: Still stands from round 11 (the helpers are unchanged this round; verified at this commit). base-tree's git helpers (gitOut at :99 and git at :112) carry no timeout/killSignal, and the reuse fast path's gitOut(tree, 'rev-parse', 'HEAD') (:184) runs BEFORE any screen.

Failure: a FIFO at <common>/config hangs the rev-parse in open() — no refusal, no timeout, the process blocks. The creation spawn this diff edits (adding INERT_GIT_ARGS, anchored here) stays on the timeout-less helper, and discardWorktree's worktree remove/unlock spawns between screen1 and screen2 share the shape — an incomplete closure of the unbounded-spawn class this same diff documents ('a screen spawn must still END against a config that blocks in open()').

Fix: give the base-tree helpers SCREEN_SPAWN_TIMEOUT_MS + SIGKILL, matching the invariant the diff states for screen spawns.

中文说明

自第 11 轮起仍然存在(辅助函数本轮未改动;已在本提交上核实)。base-tree 的 git 辅助函数(:99 的 gitOut 与 :112 的 git)没有 timeout/killSignal,且复用快速路径的 gitOut(tree, 'rev-parse', 'HEAD')(:184)运行在任何屏幕之前。

失败场景:<common>/config 处的 FIFO 把 rev-parse 挂在 open() 中——无拒绝、无超时、进程阻塞。本 diff 编辑的创建 spawn(此处,加入 INERT_GIT_ARGS)仍然使用无超时的辅助函数;screen1 与 screen2 之间 discardWorktreeworktree remove/unlock spawn 同属该形态——这是同一 diff 所声明的无界 spawn 类别的不完全封闭。

修复:给 base-tree 辅助函数加上 SCREEN_SPAWN_TIMEOUT_MS + SIGKILL,与 diff 为屏幕 spawn 声明的不变量一致。

— 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 (budget-warning round). Fix stands: bound base-tree’s git helpers with a checkout-sized ceiling plus killSignal: 'SIGKILL', with the FIFO’d-loose-object witness. Batched with the other unbounded-spawn items.

中文说明

顺延到下一轮(预算警告轮)。修复不变:给 base-tree 的 git 助手加 checkout 量级上限与 killSignal: 'SIGKILL',附 FIFO 松散对象见证。与其他无界 spawn 项合并一批。

} catch (e) {
return unavailable(
worktreeCreateFailureDetail('base', e, String(sweep?.stderr ?? '')),
Expand Down
Loading
Loading