Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ed2ef50
fix(review): keep host-trusted state out of the container's writable …
wenshao Aug 25, 2026
4ae341e
fix(review): close the shapes the first cut of this gate left open
wenshao Aug 25, 2026
9213610
fix(review): gate the third write, and stop breaking the lane that pi…
wenshao Aug 25, 2026
895462e
test(review): gate the new cases off the platform where containment c…
wenshao Aug 25, 2026
3d33de5
test(review): assert the property, not the sentence that explains it
wenshao Aug 25, 2026
a5fbb49
Merge branch 'main' into fix/review-host-state-out-of-mount
wenshao Aug 25, 2026
b3ff0b1
fix(review): ask git where the repository is, and gate the entrances …
wenshao Aug 25, 2026
226c1fb
Merge remote-tracking branch 'origin/fix/review-host-state-out-of-mou…
wenshao Aug 25, 2026
e89cdd0
fix(review): the entrances round 6 found, and two limits stated rathe…
wenshao Aug 25, 2026
36fde42
fix(review): stop editing git's answer, and gate the pointer that is …
wenshao Aug 26, 2026
78bb556
fix(review): let the launch directory resolve the way git does
wenshao Aug 26, 2026
548887a
fix(review): three failures of my own, and the block gate that stops …
wenshao Aug 26, 2026
ae424f1
Merge remote-tracking branch 'origin/main' into fix/review-host-state…
wenshao Aug 29, 2026
9b89f6a
Merge branch 'main' into fix/review-host-state-out-of-mount
wenshao Aug 30, 2026
1cba501
Merge branch 'main' into fix/review-host-state-out-of-mount
wenshao Aug 30, 2026
7455996
Merge remote-tracking branch 'origin/main' into fix/review-host-state…
wenshao Aug 31, 2026
a94be7b
fix(review): gate the reads and the launch directories, not only the …
wenshao Sep 2, 2026
049d6bb
fix(review): gate the base tree's reuse fast path too
wenshao Sep 2, 2026
3f10c3f
fix(review): ask the launch-directory question where all of it goes t…
wenshao Sep 2, 2026
29b6aae
refactor(review): let the gates default to the mount root everywhere
wenshao Sep 2, 2026
6346aea
fix(review): gate revert-hunk's tree reads, and reflow a comment
wenshao Sep 2, 2026
8ac40a1
Merge remote-tracking branch 'origin/main' into fix/review-host-state…
wenshao Sep 2, 2026
4fba588
Merge remote-tracking branch 'origin/main' into fix/review-host-state…
wenshao Sep 2, 2026
5b0b78c
fix(review): let a refusal say it refused, and re-ask where git resolves
wenshao Sep 2, 2026
5b8744c
fix(review): ask a pointer where it resolves to, and stamp the run th…
wenshao Sep 2, 2026
c6a5ac8
Merge branch 'main' into fix/review-host-state-out-of-mount
qwen-code-dev-bot Sep 3, 2026
8f881f0
fix(review): fail closed where a gate could not be run, and stop edit…
wenshao Sep 3, 2026
ca37195
Merge branch 'main' into fix/review-host-state-out-of-mount
qwen-code-dev-bot Sep 3, 2026
f4fcaa8
Merge branch 'main' into fix/review-host-state-out-of-mount
qwen-code-dev-bot Sep 3, 2026
f477327
Merge branch 'main' into fix/review-host-state-out-of-mount
qwen-code-dev-bot Sep 4, 2026
da52bea
Merge branch 'main' into fix/review-host-state-out-of-mount
qwen-code-dev-bot Sep 4, 2026
28da51e
Merge branch 'main' into fix/review-host-state-out-of-mount
qwen-code-dev-bot Sep 4, 2026
ac2e3ee
Merge branch 'main' into fix/review-host-state-out-of-mount
wenshao Sep 5, 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
9 changes: 8 additions & 1 deletion .github/workflows/qwen-code-pr-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2276,7 +2276,14 @@ jobs:
"${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
# Both locations: leases moved to `.qwen/review-leases` (out of the
# directory the review sandbox mounts read-write), and a runner whose
# workspace persists can still be holding one written by an older
# build in the old place. `-r`, because the old path is inside that
# mounted directory and a reviewed PR can leave a DIRECTORY at the
# lease's name, which plain `rm -f` cannot remove.
rm -rf .qwen/review-leases/qwen-review-lease-pr-*.json 2>/dev/null || true
rm -rf .qwen/tmp/qwen-review-lease-pr-*.json 2>/dev/null || true
Comment on lines +2285 to +2286

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 lease-sweep rewrite breaks the sync test that pins this exact workflow line. scripts/tests/review-worktree-cleanup-workflow.test.js (test: 'keeps the review-job cleanup sweep pinned to paths.ts') asserts expect(reviewCleanStep).toContain(rm -f ${toPosix(REVIEW_TMP_DIR)}/${LEASE_PREFIX}pr-*.json) — the literal substring rm -f .qwen/tmp/qwen-review-lease-pr-*.json. This diff replaced that line with two rm -rf lines; rm -f .qwen/tmp/... is not a substring of rm -rf .qwen/tmp/... (the inserted -r breaks the match), and these are the only occurrences of the lease glob in the step — so the spec suite is red at HEAD. The lane is npm run test:scripts, the trailing clause of root test:ci, run by ci.yml's required test job on every pull_request and merge_group: this PR's own required check fails on this. Secondarily, nothing pins the new .qwen/review-leases glob to REVIEW_LEASE_DIR/LEASE_PREFIX, so a later rename of the lease dir would silently no-op the new-location sweep — exactly the drift this spec exists to catch (its header cites a rename that once broke a sweeper).

Witness (live run at this commit):

npx vitest run --config ./scripts/tests/vitest.config.ts review-worktree-cleanup-workflow
Test Files 1 failed (1); Tests 1 failed | 14 passed (15)
AssertionError: expected 'set -uo pipefail…' to contain 'rm -f .qwen/tmp/qwen-review-lease-pr-…'

Fix (in the spec file, same PR): import REVIEW_LEASE_DIR alongside REVIEW_TMP_DIR and pin both new lines:

expect(reviewCleanStep).toContain(
  `rm -rf ${toPosix(REVIEW_LEASE_DIR)}/${LEASE_PREFIX}pr-*.json`,
);
expect(reviewCleanStep).toContain(
  `rm -rf ${toPosix(REVIEW_TMP_DIR)}/${LEASE_PREFIX}pr-*.json`,
);
中文说明

lease 清扫的改写打破了钉住这条工作流行的同步测试。scripts/tests/review-worktree-cleanup-workflow.test.js(测试「keeps the review-job cleanup sweep pinned to paths.ts」)断言 expect(reviewCleanStep).toContain(rm -f ${toPosix(REVIEW_TMP_DIR)}/${LEASE_PREFIX}pr-*.json)——即字面子串 rm -f .qwen/tmp/qwen-review-lease-pr-*.json。本 diff 把那一行换成了两条 rm -rf 行;rm -f .qwen/tmp/... 不是 rm -rf .qwen/tmp/... 的子串(插入的 -r 破坏了匹配),而这两行是该步骤中 lease 通配符仅有的出现处——因此该规格套件在 HEAD 上是红的。该测试所在通道是 npm run test:scripts,即根目录 test:ci 的收尾环节,由 ci.yml 的必需 test 作业在每个 pull_request 与 merge_group 上运行:本 PR 自己的必需检查会因此失败。其次,新的 .qwen/review-leases 通配符没有任何东西钉到 REVIEW_LEASE_DIR/LEASE_PREFIX,日后重命名 lease 目录会悄无声息地让新位置的清扫失效——正是这份规格文件存在所要防的漂移(其开头就记录过一次重命名曾弄坏清扫器)。

证据(在本提交上实际运行):npx vitest run --config ./scripts/tests/vitest.config.ts review-worktree-cleanup-workflowTests 1 failed | 14 passed——AssertionError: expected 'set -uo pipefail…' to contain 'rm -f .qwen/tmp/qwen-review-lease-pr-…'

修复(在同一 PR 的规格文件中):把 REVIEW_LEASE_DIRREVIEW_TMP_DIR 一并导入,钉住两条新行(见上方英文部分的代码块)。

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

echo "review worktrees cleaned"

# A review job that dies abnormally — runner crash, host loss, or the
Expand Down
126 changes: 123 additions & 3 deletions packages/cli/src/commands/review/base-tree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ 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 { adminEntryOf, plantAdminEntry } from './lib/test-utils.js';
import { runEpochMs } from './lib/prompt-record.js';
import type { BuildTestReport } from './build-test.js';

const okBuild = {
Expand All @@ -43,6 +45,13 @@ const failedBuild = {
build: [{ command: 'npm run build', exitCode: 2 }],
} as unknown as BuildTestReport;

// Skipped on win32 for the same reason as the sibling suites: `mountRootFor`
// refuses every absolute Windows path (a drive letter is a colon), so
// containment is unavailable there by design and this gate never speaks. The
// assertion would fail for that reason and nothing else — first inside the
// merge queue, where that lane actually runs.
const itWhereContainmentExists = it.skipIf(process.platform === 'win32');

describe('runBaseTree', () => {
let repo: string;
let worktree: string;
Expand All @@ -61,13 +70,19 @@ describe('runBaseTree', () => {
return p;
};

// Captured ONCE per test, the way `fetch-pr` captures it once per run: the
// reuse marker is fenced on the plan's epoch, so a helper that re-captured on
// every call would simulate a new run each time and the fast path — the
// concurrent-shard guard the reuse test below pins — could never speak.
let planPath = '';
const run = (
over: { plan?: Record<string, unknown>; worktree?: string } = {},
build: (w: string) => BuildTestReport = () => okBuild,
): BaseTreeReport => {
const { plan: planOver, ...rest } = over;
if (planOver !== undefined || !planPath) planPath = writePlan(planOver);
return runBaseTree({
plan: writePlan(planOver),
plan: planPath,
worktree,
timeout: 60,
install: false,
Expand All @@ -77,6 +92,7 @@ describe('runBaseTree', () => {
};

beforeEach(() => {
planPath = '';
repo = mkdtempSync(join(tmpdir(), 'qwen-base-tree-'));
git(repo, 'init', '-q', '-b', 'main');
git(repo, 'config', 'user.email', 't@t.t');
Expand All @@ -96,6 +112,105 @@ describe('runBaseTree', () => {

afterEach(() => rmSync(repo, { recursive: true, force: true }));

itWhereContainmentExists(
'does not REUSE a base tree whose tracked files were rewritten in the mount',
() => {
// `rev-parse HEAD` does not move when working files change, and this tree
// is a direct child of the directory the sandbox mounts read-write — so
// the reviewed PR's own build, which runs before the verifier shards get
// here, can overwrite the base checkout's tracked sources with a plain
// copy while every pointer and ref condition still passes. Reused, the
// A/B compares the PR against a copy of itself: a test the PR breaks
// fails identically on both sides, `test-delta` files the regression as
// pre-existing, and a real finding against the PR is suppressed.
const tree = baseWorktreePath(worktree);
const firstBuilds: string[] = [];
expect(
run({}, (w) => {
firstBuilds.push(w);
return okBuild;
}).available,
).toBe(true);
writeFileSync(join(tree, 'a.txt'), 'after\n');

// Untracked output is what the pipeline's own build leaves here, and it
// must not disable reuse: that reintroduces the concurrent-shard clobber
// the fast path exists to prevent.
mkdirSync(join(tree, 'dist'), { recursive: true });
writeFileSync(join(tree, 'dist', 'cli.js'), 'built');

const rebuilds: string[] = [];
const second = run({}, (w) => {
rebuilds.push(w);
return okBuild;
});
expect(rebuilds).toEqual([tree]);
expect(second.note).not.toContain('reusing it');
},
);

itWhereContainmentExists(
'does not REUSE a base tree an EARLIER RUN built, whose untracked plants the dirt check cannot see',
() => {
// `cleanStale` releases the review worktree and its branch but never
// `-base`, so this tree stands into the next round with a whole
// containerized build/test phase in between — and inside the mount the
// reviewed code writes where it likes. What it can drop there is
// untracked executable content, `dist/cli.js` and `node_modules/.bin/`
// being exactly what a host-side A/B measurement runs, and
// `--untracked-files=no` cannot see it: a blanket untracked refusal
// would disable every correctly-built tree's reuse and bring back the
// concurrent-shard clobber the fast path exists to prevent. So the
// marker carries the run that built it, and a stamp from another run is
// not a tree this run may certify.
const tree = baseWorktreePath(worktree);
const builds: string[] = [];
const build = (w: string) => {
builds.push(w);
return okBuild;
};
expect(run({}, build).available).toBe(true);
mkdirSync(join(tree, 'dist'), { recursive: true });
writeFileSync(
join(tree, 'dist', 'cli.js'),
'planted by the reviewed build',
);

// The next run captures its own plan, and the plan's mtime IS the epoch.
const later = new Date(Date.now() + 60_000);
utimesSync(planPath, later, later);

const second = run({}, build);
expect(second.note).not.toContain('reusing it');
expect(builds).toEqual([tree, tree]);
// The plant went with the tree it was standing in.
expect(existsSync(join(tree, 'dist', 'cli.js'))).toBe(false);
},
);

itWhereContainmentExists(
'refuses to build through a rewritten review-worktree gitfile',
() => {
// `worktree add` resolves the repository through the REVIEW worktree's own
// gitfile, which lives in the directory the sandbox mounts read-write and
// which the build/test phase already ran the PR's code against. It checks
// files out, so it runs whatever that pointer leads to, on the host.
plantAdminEntry(
join(repo, '.qwen', 'tmp', '.evil-git'),
adminEntryOf(worktree),
worktree,
join(repo, '.git'),
);

const r = run();
expect(r.available).toBe(false);
expect(JSON.stringify(r)).toContain('review temp dir');
// The tree was never created, which is what says the spawn never ran —
// the note alone reads the same whichever side of it the gate fires on.
expect(existsSync(baseWorktreePath(worktree))).toBe(false);
},
);

it('creates a sibling worktree holding the BASE commit, not the head', () => {
const r = run();
expect(r.available).toBe(true);
Expand Down Expand Up @@ -140,8 +255,13 @@ describe('runBaseTree', () => {
expect(second.path).toBe(first.path);
expect(second.note).toContain('reusing');
expect(builds).toHaveLength(1); // one install+build, not two
// A marker for a DIFFERENT sha (rebase between runs) does not shortcut.
writeFileSync(join(first.path!, '.qwen-review-base-ok'), 'f'.repeat(40));
// A marker for a DIFFERENT sha (rebase between runs) does not shortcut —
// stamped with this run's epoch, so the sha arm is what answers and not the
// epoch fence standing in front of it.
writeFileSync(
join(first.path!, '.qwen-review-base-ok'),
`${'f'.repeat(40)}\n${runEpochMs(planPath)}\n`,
);
expect(run({}, build).note).not.toContain('reusing');
});

Expand Down
64 changes: 60 additions & 4 deletions packages/cli/src/commands/review/base-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,14 @@ import { dirname, join, resolve } from 'node:path';
import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js';
import { baseWorktreePath } from './lib/paths.js';
import {
untrustedGitfile,
discardWorktree,
sanitizedGitEnv,
worktreeCreateFailureDetail,
type SweepResult,
} from './lib/worktree.js';
import { runBuildTest, type BuildTestReport } from './build-test.js';
import { runEpochMs } from './lib/prompt-record.js';

export interface BaseTreeReport {
/**
Expand Down Expand Up @@ -175,11 +177,55 @@ export function runBaseTree(args: BaseTreeArgs): BaseTreeReport {
// rebase between runs — falls through to the rebuild below.)
const marker = () => join(tree, '.qwen-review-base-ok');
const failedMarker = () => join(tree, '.qwen-review-base-failed');
// The run's epoch, the fence the deadline stamps and the session ledger
// already key on. A run captures its plan once, so this is stable for the
// whole run and different between two.
const runEpoch = String(runEpochMs(args.plan));
try {
const stamp = readFileSync(marker(), 'utf8').trim().split('\n');
if (
existsSync(tree) &&
readFileSync(marker(), 'utf8').trim() === baseSha &&
gitOut(tree, 'rev-parse', 'HEAD') === baseSha
stamp[0] === baseSha &&
// ...and THIS run built it. `cleanStale` never releases `-base`, so an
// earlier round's tree stands into this one with a whole containerized
// build/test phase in between — long enough for the reviewed code to drop
// untracked executables in here, and `dist/cli.js` is what a host-side A/B
// runs. `--untracked-files=no` below cannot see them, and refusing any
// untracked file at all would disable every legitimate reuse; the stamp is
// the provenance that separates the two.
stamp[1] === runEpoch &&
Comment on lines +195 to +196

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] R15-4: [certifies-falsely] Still standing — the epoch fence does not close this class: every input the base-tree provenance fence keys on is attacker-writable from the sandbox read-write mount, so the reviewed code can make the pipeline certify an attacker-chosen commit as the A/B base — no marker forge needed. The stamp's epoch is statSync(plan).mtimeMs - 2000 (lib/prompt-record.ts:60-67), and the plan lives at .qwen/tmp/qwen-review-pr-<n>-fetch.json inside the mounted dir (paths.ts:48). Two entrances demonstrated by probe this round. (1) Epoch forge: the reviewed code stats the plan, computes the epoch, reads mergeBaseSha from the same plan JSON, drops untracked executables into <repo>/.qwen/tmp/review-pr-N-base (dist/cli.js is what a host-side A/B runs; cleanStale never releases -base), rewrites .qwen-review-base-ok, and runBaseTree answers "reusing it" with the plant riding into the certified base. (2) Plan-content rewrite: the reviewed code writes "mergeBaseSha": "<headSha>" into the plan; the pipeline itself then runs git worktree add --detach tree <headSha> (base-tree.ts:325) behind a gate that judges only the pointer, never the sha, and stamps the marker with genuine provenance — every fence passes because the pipeline built the tree itself. The A/B then compares the PR against a behaviourally identical tree: a test the PR breaks fails identically on both sides, lands in shared, and is reported "pre-existing by measurement" — a real regression suppressed with measurement-grade certification. The same root produced two more probe-confirmed entrances beside it: the sibling .qwen-review-base-failed marker is still compared sha-only (base-tree.ts:246-248) — and this round's epoch fence is what now routes every standing tree onto it deterministically (reverting the fence hunks makes the plant inert) — and the build lock is a plantable bare directory (base-tree.ts:270-297) whose mere presence returns BUSY for the whole run.

Witness:

Epoch forge (intact PR code, scratch tree):
F1-ARM-A note="base tree already built at 9b0ac2829 … (reusing it)" available=true plantSurvived=true
F1-ARM-B (identical staleness, no marker rewrite) rebuilt — the fence's answer is controlled by a value the reviewed code computes
Plan rewrite (intact PR code):
first:  { available: true, baseSha: c1fca5ce… }   (true base)
second: { available: true, baseSha: fc5be377…, note: "base tree built at fc5be377b…" }
treeHolds: fc5be377…  head: fc5be377…  trueBase: c1fca5ce…
Failed marker: {"available":false,"note":"… already failed to build (an earlier probe measured it)…"}
  reverting the epoch-fence hunks flips it to reuse — the fence opened the route
Lock: mkdir the lock → {"available":false,"note":"another probe is building the base tree right now…"}

Fix direction — move the fence's provenance out of the mount, one structural change closing all four entrances: generate a random per-run nonce at plan capture and persist it beside REVIEW_LEASE_DIR (paths.ts:65 — the existing "nothing mounts it" precedent); stamp .qwen-review-base-ok AND .qwen-review-base-failed with it and compare the nonce instead of the plan mtime; pin mergeBaseSha outside the mount (fetch-pr resolves it on the host before any sandbox phase exists — record it beside the nonce and refuse when the plan's copy disagrees, or re-resolve at shard time from the refs git holds outside the mount in <repo>/.git); and require the same provenance on the build lock, sweeping a lock that lacks it the way the 30-minute corpse is swept.

The fix must keep same-run reuse alive and cheap — base-tree.ts:172-174 ("a tree that exists, holds the right commit, and carries the marker a successful build wrote is returned as-is") exists so shard B's opening sweep cannot destroy the tree shard A is mid-A/B in — and must respect base-tree.ts:151-157, where baseFetchFailed means the merge base may be stale, so a re-resolved base must not silently substitute a different commit; the nonce store itself must sit outside REVIEW_TMP_DIR = join('.qwen', 'tmp') (lib/paths.ts:48).

Please add arms to base-tree.test.ts that (a) forge the marker with an epoch computed from the plan file's mtime plus untracked-only planting and assert the report does not contain 'reusing it', (b) rewrite the plan's mergeBaseSha to headSha and rerun, asserting no base is certified at headSha, and (c) plant the failed marker into a mkdir'd base path, asserting the rebuild proceeds — all red at this HEAD — and confirm by mutation that removing the nonce comparison or the pin reds each respective arm again.

中文说明

仍然成立——epoch 围栏没有关闭这一类问题:base-tree 来源围栏所依赖的每一个输入都可以被被审代码从沙箱读写挂载内改写,因此被审代码可以让流水线把攻击者选定的提交认证为 A/B 基线——完全不需要伪造 marker。stamp 的 epoch 是 statSync(plan).mtimeMs - 2000(lib/prompt-record.ts:60-67),而 plan 文件就放在挂载目录内的 .qwen/tmp/qwen-review-pr-<n>-fetch.json(paths.ts:48)。本轮探针证明了两个入口:(1)伪造 epoch:被审代码对 plan 做一次 stat 算出 epoch、从同一份 plan JSON 读出 mergeBaseSha、向 <repo>/.qwen/tmp/review-pr-N-base 丢入未跟踪的可执行文件(宿主侧 A/B 跑的正是 dist/cli.jscleanStale 从不释放 -base)、改写 .qwen-review-base-ok,于是 runBaseTree 返回 "reusing it",植入物随被认证的基线一起进入后续流程。(2)改写 plan 内容:被审代码把 "mergeBaseSha": "<headSha>" 写进 plan,流水线自己就会执行 git worktree add --detach tree <headSha>(base-tree.ts:325)——创建路径上的门只判指针、从不判 sha——然后用真实的来源信息盖上 marker:因为树是流水线本轮自己建的,所有围栏全部通过。于是 A/B 把 PR 与一棵行为完全相同的树对比:PR 弄坏的测试在两侧同样失败、落入 shared、被报告为「测量意义上的既有问题」——真实的回归被以测量级认证压制掉。同一根因还在旁边产生了两个探针确认的入口:兄弟 marker .qwen-review-base-failed 仍按纯 sha 比较(base-tree.ts:246-248)——而本轮新增的 epoch 围栏恰好把每一棵残留的树确定性地路由到它上面(回退围栏的 hunk 后植入物即失效)——以及构建锁是一个可植入的裸目录(base-tree.ts:270-297),仅凭存在就能让整个运行返回 BUSY。

修复方向——把围栏的来源信息移出挂载,一处结构化修改关闭全部四个入口:在 plan 捕获时生成每运行一次的随机 nonce,存放在 REVIEW_LEASE_DIR 旁(paths.ts:65——现有「没有任何东西挂载它」的先例);两个 marker 都用它盖章、用它替代 plan mtime 做比较;把 mergeBaseSha 钉在挂载之外(fetch-pr 在任何沙箱阶段存在之前就在宿主上解析它——把它记录在 nonce 旁,plan 中的副本不一致时拒绝;或在 shard 时从 <repo>/.git 里 git 持有的引用重新解析);构建锁也要求同样的来源信息,缺少来源的锁按 30 分钟尸体锁的方式清扫。

约束:修复必须保住同运行复用的存活与廉价——base-tree.ts:172-174(「树存在、提交正确、携带成功构建写下的 marker 就原样返回」)正是为了防止 shard B 的开场清扫摧毁 shard A 正在 A/B 的树——并且必须尊重 base-tree.ts:151-157:baseFetchFailed 时 merge base 可能已陈旧,重新解析的基线不得静默替换成另一个提交;nonce 存储本身必须位于 REVIEW_TMP_DIR = join('.qwen', 'tmp')(lib/paths.ts:48)之外。

请向 base-tree.test.ts 添加三个手臂:(a) 用从 plan 文件 mtime 计算的 epoch 伪造 marker 并只做未跟踪植入,断言报告不含 'reusing it';(b) 把 plan 的 mergeBaseSha 改写为 headSha 后重跑,断言没有以 headSha 认证的基线;(c) 向 mkdir 出来的基线路径植入 failed marker,断言走重建——三者在本 HEAD 上都应为红——并用变异确认:移除 nonce 比较或钉住值后对应手臂再次变红。

— qwen3.8-max via Qwen Code /review (v0.22.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] R15-4: [certifies-falsely] [new-surface] Still standing (rounds 15/17, re-derived at this commit): the base-tree reuse fence keys on inputs the sandbox's read-write mount exposes — the epoch is statSync(plan).mtimeMs - 2000 (lib/prompt-record.ts:60) and the plan lives at .qwen/tmp/qwen-review-pr-<n>-fetch.json inside the mounted dir, so the reviewed code can make the pipeline certify an attacker-chosen commit as the A/B base. A leftover -base tree is GENUINE (cleanStale never releases -base), so every pointer/shape check passes and the forgeable epoch is the only line left. Concretely: the reviewed code stats the plan during the containerized build/test phase, computes the epoch with the same public constant, drops untracked dist/cli.js into the tree, rewrites .qwen-review-base-ok line 2 — every reuse condition passes, runBaseTree answers "reusing it", and the A/B's BASE side runs against the attacker-shaped tree: a test the PR breaks fails identically on both sides, lands in shared, and a real regression is reported "pre-existing by measurement". The sibling entrances named in the round-15/16 bodies stand too: the plan's mergeBaseSha is rewritable (the pipeline then builds the tree itself behind a pointer-only gate), the .qwen-review-base-failed marker is still compared sha-only (base-tree.ts:214), and the build lock is a plantable bare directory.

Witness:

Probe (round 16, carried): epoch-forge and plan-rewrite entrances both demonstrated.
Re-derived at this commit (round 18 reverse audit): runEpochMs = statSync(plan).mtimeMs - 2000;
the reused tree is genuine, so every pointer/shape check passes and the forgeable epoch
is the only line left.

Fix: move the trust artifacts out of the writable surface — the round-17 thread's direction: a random per-run nonce persisted beside REVIEW_LEASE_DIR (paths.ts:65, the existing "nothing mounts it" precedent), stamping both markers, and pin mergeBaseSha outside the mount — so a mount-local writer can neither refresh the epoch nor forge it; a leftover tree then always fails the fence and is rebuilt, taking any plants with it. The fix must keep same-run reuse alive and cheap (base-tree.ts:172-174 exists so shard B's opening sweep cannot destroy the tree shard A is mid-A/B in) and must respect base-tree.ts:151-157 (baseFetchFailed means the merge base may be stale — a re-resolved base must not silently substitute a different commit).

Fix witness: a new base-tree.test.ts case with production geometry (plan INSIDE <repo>/.qwen/tmp): round 1 builds legitimately, then rewrite the marker epoch to runEpochMs(plan) of a fresh-mtime plan and plant dist/cli.js; assert the note does NOT contain "reusing it" and the plant is gone — removing the fence arm must turn it red.

中文说明

依然成立(第 15/17 轮提出,本轮在该提交上重新推导):base-tree 复用栅栏依赖的输入暴露在沙箱读写挂载之下——epoch 是 statSync(plan).mtimeMs - 2000(lib/prompt-record.ts:60),而 plan 就在挂载目录内的 .qwen/tmp/qwen-review-pr-<n>-fetch.json,因此被审代码可以让流水线把攻击者选定的提交认证为 A/B 基线。遗留的 -base 树是真实的(cleanStale 从不释放 -base),所有指针/形状检查都通过,可伪造的 epoch 成为唯一防线。具体地:被审代码在容器化构建/测试阶段 stat plan、用同一公开常量算出 epoch、向树中放入未跟踪的 dist/cli.js、改写 .qwen-review-base-ok 第二行——所有复用条件通过,runBaseTree 回答「reusing it」,A/B 的 BASE 侧在攻击者塑造的树上运行:PR 弄坏的测试在两侧同样失败、落入 shared,真实回归被报告为「测量意义上的既有问题」。第 15/16 轮正文点名的兄弟入口同样成立:plan 的 mergeBaseSha 可改写(流水线随后仅凭指针门自己建树)、.qwen-review-base-failed 仍只按 sha 比较(base-tree.ts:214)、构建锁是可植入的裸目录。

修复:把信任凭据移出可写面——第 17 轮讨论的方向:在 REVIEW_LEASE_DIR 旁(paths.ts:65,现有「无人挂载」先例)持久化每轮随机 nonce 并戳进两个标记,mergeBaseSha 钉在挂载之外——挂载内的写者既不能刷新 epoch 也不能伪造它;遗留树将始终无法过栅栏并被重建(连同植入物一起丢弃)。修复必须保持同轮复用的廉价与可用(base-tree.ts:172-174 的存在就是为了分片 B 的开场清扫不能毁掉分片 A 正在 A/B 的树),并尊重 base-tree.ts:151-157(baseFetchFailed 意味着合并基可能过期——重新解析的基不得静默替换为不同提交)。

修复见证:在 base-tree.test.ts 新增生产几何用例(plan 位于 <repo>/.qwen/tmp 内):第 1 轮正常构建,然后把标记 epoch 改写为新 mtime plan 的 runEpochMs(plan) 并植入 dist/cli.js;断言 note 不含 "reusing it" 且植入物被清除——移除栅栏臂该测试应变红。

— qwen3.8-max via Qwen Code /review (v0.22.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.

Escalated for a maintainer's decision — thread left open.

Re-derived at this head and it stands: the reuse fence's epoch is statSync(plan).mtimeMs - 2000 and the plan lives inside the mounted directory, so the value the fence trusts is one the reviewed code can write. The previous round's epoch stamp (commit 5b8744c824) moved what is recorded, not where it is trusted from, which is why the finding came back this round.

This is not a decline — the finding is verified real. It is left open because the call is a maintainer's, not mine: the author put the same question in round 8 (keep gating entrance by entrance in this pull request, or split the gitfile half out and land the lease migration, which has been stable and Critical-free for many rounds), round 18's own review independently reached land-with-residual-risk, and @qqqys has now said three times that this scope decision is the only gate left. Closing it by adding another gate would be answering that question silently.

The prescribed fix is a per-run random nonce persisted beside REVIEW_LEASE_DIR and stamped into both markers, with mergeBaseSha pinned outside the mount. That is a new trust artifact with a real design surface — which process writes it, when it is cleared, and how the verifier shards that base-tree.ts:172-174 exists to protect agree on one value — rather than a corrected guard. It is exactly the additive gating the pending scope decision covers, so it waits for that decision instead of growing this diff.

Earlier re-posts of this same finding carry the same answer: rc:3913328133 (round 15), rc:3917123743 (round 16), rc:3920055333 (round 17).

中文说明

升级给维护者决定 —— 线程保持开放。

在当前 head 上重新推导,仍然成立:复用栅栏的 epoch 是 statSync(plan).mtimeMs - 2000,而 plan 就在被挂载的目录里,所以栅栏所信任的那个值正是被审代码能写的东西。上一轮的 epoch 戳(提交 5b8744c824)改变的是记录了什么,而不是从哪里取得信任,这正是该发现本轮再次出现的原因。

这不是驳回 —— 该发现已验证为真。保持开放是因为这个判断属于维护者,不属于我:作者在第 8 轮就摆出了同一个问题(是继续在本 PR 里按入口逐个设门,还是把 gitfile 那一半拆出去、先落地已经连续多轮稳定且零 Critical 的 lease 迁移),第 18 轮评审自己也独立走到了 land-with-residual-risk,而 @qqqys 已经三次说明这个范围决定是唯一剩下的卡点。用再加一道门的方式关掉它,等于悄悄替这个问题作了答。

给出的修法是在 REVIEW_LEASE_DIR 旁持久化一个每轮随机 nonce、戳进两个标记,并把 mergeBaseSha 钉在挂载之外。那是一个新的信任凭据,有真实的设计面 —— 由哪个进程写、何时清除、以及 base-tree.ts:172-174 所要保护的那些 verifier 分片如何就同一个值达成一致 —— 而不是一道被改正的门。它恰恰属于那个待决范围决定所涵盖的加法式设门,因此等该决定,而不是长进这个 diff。

同一发现的更早重复发布给出同样的答复:rc:3913328133(第 15 轮)、rc:3917123743(第 16 轮)、rc:3920055333(第 17 轮)。

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] R15-4: [certifies-falsely] [new-surface] Still standing (rounds 15/17/18; re-derived at the round-19 head — base-tree.ts is byte-identical to the round-18 anchor): the base-tree reuse fence keys on inputs the sandbox's read-write mount exposes — the epoch is statSync(plan).mtimeMs - 2000 (lib/prompt-record.ts:60) and the plan lives at .qwen/tmp/qwen-review-pr-<n>-fetch.json inside the mounted dir, so the reviewed code can make the pipeline certify an attacker-chosen commit as the A/B base. A leftover -base tree is GENUINE (cleanStale never releases -base), so every pointer/shape check passes and the forgeable epoch is the only line left: the reviewed code stats the plan during the containerized phase, computes the epoch with the same public constant, drops untracked dist/cli.js into the tree, rewrites .qwen-review-base-ok line 2 — every reuse condition passes, runBaseTree answers "reusing it", and the A/B's BASE side runs against the attacker-shaped tree: a test the PR breaks fails identically on both sides, lands in shared, and a real regression is reported "pre-existing by measurement". The sibling entrances stand too: the plan's mergeBaseSha is rewritable, the .qwen-review-base-failed marker is still compared sha-only (base-tree.ts:214), and the build lock is a plantable bare directory.

Witness:

Re-derived at this head (round 19): runEpochMs = statSync(plan).mtimeMs - 2000 (lib/prompt-record.ts:60);
the reused tree is genuine, so every pointer/shape check passes and the forgeable epoch is the only line left.
Round-16 probe (carried): epoch-forge and plan-rewrite entrances both demonstrated.

Fix direction (round-17 thread): move the trust artifacts out of the writable surface — a random per-run nonce persisted beside REVIEW_LEASE_DIR (paths.ts:65, the existing "nothing mounts it" precedent), stamping both markers, and pin mergeBaseSha outside the mount — so a mount-local writer can neither refresh the epoch nor forge it; a leftover tree then always fails the fence and is rebuilt, taking any plants with it. The fix must keep same-run reuse alive and cheap (base-tree.ts:172-174 exists so shard B's opening sweep cannot destroy the tree shard A is mid-A/B in) and must respect base-tree.ts:151-157 (baseFetchFailed means the merge base may be stale — a re-resolved base must not silently substitute a different commit). Escalated for a maintainer's decision in round 18 — the thread stays open; this re-post carries the finding into this round's work list.

Fix witness owed: a new base-tree.test.ts case with production geometry (plan INSIDE <repo>/.qwen/tmp): round 1 builds legitimately, then rewrite the marker epoch to runEpochMs(plan) of a fresh-mtime plan and plant dist/cli.js; assert the note does NOT contain "reusing it" and the plant is gone — removing the fence arm must turn it red.

中文说明

依然成立(第 15/17/18 轮提出;在第 19 轮的 head 上重新推导——base-tree.ts 与第 18 轮锚点逐字节一致):base-tree 复用栅栏依赖的输入暴露在沙箱读写挂载之下——epoch 是 statSync(plan).mtimeMs - 2000(lib/prompt-record.ts:60),而 plan 就在挂载目录内的 .qwen/tmp/qwen-review-pr-<n>-fetch.json,因此被审代码可以让流水线把攻击者选定的提交认证为 A/B 基线。遗留的 -base 树是真实的(cleanStale 从不释放 -base),所以一切指针/形状检查都通过,可伪造的 epoch 成为唯一防线:被审代码在容器化阶段 stat plan、用同一公开常量算出 epoch、向树中放入未跟踪的 dist/cli.js、改写 .qwen-review-base-ok 第二行——所有复用条件通过,runBaseTree 回答 "reusing it",A/B 的 BASE 侧在攻击者塑造的树上运行:PR 弄坏的测试在两侧同样失败、落入 shared,真实回归被报告为「测量意义上的既有问题」。兄弟入口同样成立:plan 的 mergeBaseSha 可改写、.qwen-review-base-failed 仍只按 sha 比较(base-tree.ts:214)、构建锁是可植入的裸目录。

证据:本轮在该 head 上重新推导:runEpochMs = statSync(plan).mtimeMs - 2000;复用的树是真实的,故所有指针/形状检查通过,可伪造的 epoch 是唯一防线。第 16 轮探针(沿用):epoch 伪造与 plan 改写两个入口均已演示。

修复方向(第 17 轮讨论):把信任凭据移出可写面——在 REVIEW_LEASE_DIR 旁(paths.ts:65,现有「无人挂载」先例)持久化每轮随机 nonce 并戳进两个标记,mergeBaseSha 钉在挂载之外——挂载内的写者既不能刷新也不能伪造 epoch;遗留树将始终无法过栅栏并被重建(连同植入物一起丢弃)。修复必须保持同轮复用的廉价与可用(base-tree.ts:172-174 的存在就是为了分片 B 的开场清扫不能毁掉分片 A 正在 A/B 的树),并尊重 base-tree.ts:151-157(baseFetchFailed 意味着合并基可能过期——重新解析的基不得静默替换为不同提交)。已在第 18 轮升级等待 maintainer 决定——线程保持打开;本次重发把该发现带入本轮工作清单。

欠交的修复见证:在 base-tree.test.ts 新增生产几何用例(plan 位于 <repo>/.qwen/tmp 内):第 1 轮正常构建,然后把标记 epoch 改写为新 mtime plan 的 runEpochMs(plan) 并植入 dist/cli.js;断言 note 不含 "reusing it" 且植入物被清除——移除栅栏臂该测试应变红。

— qwen3.8-max via Qwen Code /review (v0.22.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] R15-4: [certifies-falsely] [new-surface] Still standing (rounds 15/17/18/19; re-derived at the round-20 head — base-tree.ts is byte-identical): the base-tree reuse fence keys on inputs the sandbox's read-write mount exposes — the epoch is statSync(plan).mtimeMs - 2000 (lib/prompt-record.ts:60) and the plan lives at .qwen/tmp/qwen-review-pr-<n>-fetch.json inside the mounted dir, so the reviewed code can make the pipeline certify an attacker-chosen commit as the A/B base. A leftover -base tree is GENUINE (cleanStale never releases -base), so every pointer/shape check passes and the forgeable epoch is the only line left. The sibling entrances named in the carried bodies stand too: the plan's mergeBaseSha is rewritable (the pipeline then builds the tree itself behind a pointer-only gate), the .qwen-review-base-failed marker is still compared sha-only — the epoch fence this PR adds covers only the OK marker, not the failed-marker fast path — and the build lock is a plantable bare directory. If the fence is defeated, runBaseTree answers "reusing it" and the A/B's BASE side runs against the attacker-shaped tree: a test the PR breaks fails identically on both sides, lands in shared, and a real regression is reported "pre-existing by measurement".

Witness:

Probe (round 16, carried): epoch-forge and plan-rewrite entrances both demonstrated.
Re-derived at this head (rounds 18/19/20): runEpochMs = statSync(plan).mtimeMs - 2000;
the reused tree is genuine, so every pointer/shape check passes and the forgeable epoch
is the only line left.

Fix direction (carried from the round-17 thread): move the trust artifacts out of the writable surface — a random per-run nonce persisted beside REVIEW_LEASE_DIR (paths.ts:65, the existing "nothing mounts it" precedent), stamping both markers, and pin mergeBaseSha outside the mount — so a mount-local writer can neither refresh the epoch nor forge it; a leftover tree then always fails the fence and is rebuilt, taking any plants with it. The fix must keep same-run reuse alive and cheap (base-tree.ts:172-174 exists so shard B's opening sweep cannot destroy the tree shard A is mid-A/B in) and must respect base-tree.ts:151-157 (baseFetchFailed means the merge base may be stale — a re-resolved base must not silently substitute a different commit).

中文说明

依然成立(第 15/17/18/19 轮提出;在第 20 轮的 head 上重新推导——base-tree.ts 逐字节未变):base-tree 复用栅栏依赖的输入暴露在沙箱读写挂载之下——epoch 是 statSync(plan).mtimeMs - 2000(lib/prompt-record.ts:60),而 plan 就在挂载目录内的 .qwen/tmp/qwen-review-pr-<n>-fetch.json,因此被审代码可以让流水线把攻击者选定的提交认证为 A/B 基线。遗留的 -base 树是真实的(cleanStale 从不释放 -base),所有指针/形状检查都通过,可伪造的 epoch 成为唯一防线。既有正文点名的兄弟入口同样成立:plan 的 mergeBaseSha 可改写(流水线随后仅凭指针门自己建树)、.qwen-review-base-failed 标记仍只按 sha 比较——本 PR 新增的 epoch 栅栏只覆盖了 OK 标记,未覆盖失败标记快路径——构建锁是可植入的裸目录。栅栏一旦被绕过,runBaseTree 回答「reusing it」,A/B 的 BASE 侧便在攻击者塑造的树上运行:PR 弄坏的测试在两侧同样失败、落入 shared,真实回归被报告为「测量意义上的既有问题」。
修复方向(沿第 17 轮讨论):把信任凭据移出可写面——在 REVIEW_LEASE_DIR 旁(paths.ts:65,现有「无人挂载」先例)持久化每轮随机 nonce 并戳进两个标记,mergeBaseSha 钉在挂载之外——挂载内的写者既不能刷新 epoch 也不能伪造它;遗留树将始终无法过栅栏并被重建(连同植入物一起丢弃)。修复必须保持同轮复用的廉价与可用(base-tree.ts:172-174),并尊重 base-tree.ts:151-157(baseFetchFailed 意味着合并基可能过期——重新解析的基不得静默替换为不同提交)。

— qwen3.8-max via Qwen Code /review (v0.23.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.

[Critical] R15-4: [certifies-falsely] [new-surface] Still standing (rounds 15/17/18/19/20; re-derived at the round-21 head): the base-tree reuse fence keys on inputs the sandbox's read-write mount exposes — the epoch is statSync(plan).mtimeMs - 2000 (lib/prompt-record.ts:60) and the plan lives at .qwen/tmp/qwen-review-pr-<n>-fetch.json inside the mounted dir, so the reviewed code can make the pipeline certify an attacker-chosen commit as the A/B base. A leftover -base tree is genuine (cleanStale never releases it), so during the containerized build/test phase the reviewed code stats the plan, computes the epoch with the same public constant, drops untracked executables into the tree (--untracked-files=no cannot see them, and dist/cli.js is exactly what a host-side A/B runs), and re-stamps the marker — every reuse arm passes, runBaseTree answers "reusing it", and the A/B's BASE side runs attacker-shaped content: a test the PR breaks fails identically on both sides, lands in shared, and a real regression is reported "pre-existing by measurement". The sibling entrances named in earlier rounds stand too: the plan's mergeBaseSha is rewritable, the .qwen-review-base-failed marker is compared sha-only, and the build lock is a plantable bare directory.

Witness:

Round-21 re-derivation at head f4fcaa8e: plan still lives at .qwen/tmp/qwen-review-pr-<n>-fetch.json inside the RW mount; epoch is still statSync(plan).mtimeMs - 2000; cleanStale still never releases -base; the hunk's own comment concedes 'dist/cli.js is what a host-side A/B runs' and '--untracked-files=no below cannot see them'.
中文说明

仍然成立(第 15/17/18/19/20 轮;在第 21 轮 head 再次推出):base-tree 复用栅栏依赖的输入暴露在沙箱读写挂载内 —— epoch 是 statSync(plan).mtimeMs - 2000,而 plan 就在被挂载目录里的 .qwen/tmp/qwen-review-pr-<n>-fetch.json,因此被审代码可以让流水线把攻击者选定的提交认证为 A/B 基线。残留的 -base 树是真实的(cleanStale 从不释放它),被审代码在容器化 build/test 阶段 stat 该 plan、用同一公开常量算出 epoch、向树中投放未跟踪的可执行文件(--untracked-files=no 看不见,而 dist/cli.js 恰是宿主侧 A/B 实际运行的东西)、改写 marker —— 复用条件全部通过,A/B 的基线一侧运行攻击者塑造的内容:PR 弄坏的测试在两侧同样失败,落入 shared,真实回归被报告为「测量上预先存在」。早先轮次点名的兄弟入口同样成立:plan 的 mergeBaseSha 可改写、.qwen-review-base-failed marker 仍按 sha 比较、build lock 是可植入的裸目录。

— qwen3.8-max via Qwen Code /review (v0.23.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.

[Critical] R15-4: [certifies-falsely] [new-surface] Still standing (rounds 15/17/18/19/20/21; probe-confirmed again at this head): the base-tree reuse fence keys on inputs the sandbox's read-write mount exposes — the epoch is statSync(plan).mtimeMs - 2000 (lib/prompt-record.ts:60) and the plan lives at .qwen/tmp/qwen-review-pr-<n>-fetch.json inside the mounted dir, so the reviewed code can make the pipeline certify an attacker-chosen commit as the A/B base. A leftover -base tree is genuine (cleanStale never releases -base), so every pointer/shape check passes and the forgeable epoch is the only line left. Concretely: the attacker sets the plan's mtime to T, rewrites the marker to <baseSha>\n(T-2000)\n, and drops untracked dist/cli.js into the tree; every reuse condition passes, runBaseTree answers "reusing it", and the A/B BASE side runs against the attacker-shaped tree — a test the PR breaks fails identically on both sides and a real regression is reported "pre-existing by measurement".

Witness:

Probe at this commit (scratch tree, real runBaseTree):
ATTACK arm (marker forged to runEpochMs(plan), plan mtime set, untracked dist/cli.js planted):
  secondNote = "base tree already built at 450939897 in /tmp/qwen-probe-bt-HcdrH8/.qwen/tmp/review-pr-1-base (reusing it — a concurrent or earlier probe built it)"
  builds = 1, plantStillThere: true
CONTROL arm (old stamp, advanced mtime — the case the PR's new test covers):
  secondNote = "base tree built at 450939897 …" (rebuild)

Fix direction (the open thread's): move the trust anchor out of the writable surface — a random per-run nonce persisted beside REVIEW_LEASE_DIR (the "nothing mounts it" precedent this same diff establishes, lib/paths.ts:63), stamping both markers, and pin mergeBaseSha outside the mount; a leftover tree then always fails the fence and is rebuilt with its plants.

Fix constraint: base-tree.ts:167-171 — "without this, shard B's opening sweep destroys the tree shard A is mid-A/B in", so the nonce must be stable within one run; base-tree.ts:155-160 — baseFetchFailed means the merge base may be stale, and a re-resolved base must not silently substitute a different commit.

Fix witness: a new base-tree.test.ts case with production geometry (plan inside <repo>/.qwen/tmp): round 1 builds legitimately, then rewrite the marker epoch to runEpochMs(plan) of a fresh-mtime plan and plant dist/cli.js; assert the note does NOT contain "reusing it" and the plant is gone — removing the fence arm must turn it red.

中文说明

依然成立(第 15/17/18/19/20/21 轮;本轮在该提交上再次以探针确认):base-tree 复用栅栏依赖的输入暴露在沙箱读写挂载之下——epoch 是 statSync(plan).mtimeMs - 2000(lib/prompt-record.ts:60),而 plan 就在挂载目录内的 .qwen/tmp/qwen-review-pr-<n>-fetch.json,因此被审代码可以让流水线把攻击者选定的提交认证为 A/B 基线。遗留的 -base 树是真实的(cleanStale 从不释放 -base),所有指针/形状检查都通过,可伪造的 epoch 成为唯一防线。具体地:攻击者把 plan 的 mtime 设为 T、把标记改写为 <baseSha>\n(T-2000)\n、向树中放入未跟踪的 dist/cli.js;所有复用条件通过,runBaseTree 回答「reusing it」,A/B 的 BASE 侧在攻击者塑造的树上运行——PR 弄坏的测试在两侧同样失败,真实回归被报告为「测量意义上的既有问题」。

修复方向(开放讨论串中的):把信任凭据移出可写面——在 REVIEW_LEASE_DIR 旁(本 diff 自己建立的「无人挂载」先例,lib/paths.ts:63)持久化每轮随机 nonce 并戳进两个标记,mergeBaseSha 钉在挂载之外;遗留树将始终无法过栅栏并被重建(连同植入物一起丢弃)。

修复约束:base-tree.ts:167-171——「否则分片 B 的开场清扫会毁掉分片 A 正在 A/B 的树」,因此 nonce 必须在同一轮内保持稳定;base-tree.ts:155-160——baseFetchFailed 意味着合并基可能过期,重新解析的基不得静默替换为不同提交。

修复见证:在 base-tree.test.ts 新增生产几何用例(plan 位于 <repo>/.qwen/tmp 内):第 1 轮正常构建,然后把标记 epoch 改写为新 mtime plan 的 runEpochMs(plan) 并植入 dist/cli.js;断言 note 不含 "reusing it" 且植入物被清除——移除栅栏臂该测试应变红。

— qwen3.8-max via Qwen Code /review (v0.23.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.

[Critical] R15-4: [certifies-falsely] [new-surface] Still standing (rounds 15-22; probe-confirmed again at this head) — the base-tree reuse fence keys on inputs the sandbox's read-write mount exposes: the epoch is statSync(plan).mtimeMs - 2000 with the constant exported (prompt-record.ts:45), and the plan lives at .qwen/tmp/qwen-review-pr-<n>-fetch.json inside the mounted dir. Reviewed code running during the containerized build/test phase can compute the exact stamp value, rewrite .qwen-review-base-ok, and drop untracked content (e.g. dist/cli.js) into a genuine leftover -base tree (cleanStale never releases -base). Every reuse arm then passes — the tree is genuine, rev-parse HEAD matches, tracked status is clean because the plant is untracked — and runBaseTree answers "reusing it" with build: null, so the A/B base side runs the attacker-shaped tree and test-delta attributes the PR's regressions as pre-existing by measurement.

Witness:

Probe at da52bea4: FORGED arm (marker epoch = statSync(plan).mtimeMs - 2000, untracked plant) → {"note":"base tree already built at 866c0fe5b ... (reusing it ...)","available":true,"buildNull":true,"rebuilds":[],"plantSurvived":true}. STALE control arm (epoch 60s older) → fence fires: {"rebuilds":[".../review-pr-1-base"],"plantSurvived":false} — the fence speaks, and the forged epoch defeats it.

Move the trust artifacts out of the writable surface, per the round-17 direction: a random per-run nonce persisted beside REVIEW_LEASE_DIR (the existing "nothing mounts it" precedent), stamping both markers, and pin mergeBaseSha outside the mount — so a mount-local writer can neither refresh the epoch nor forge it; a leftover tree then always fails the fence and is rebuilt, taking any plants with it.

The fix must keep same-run reuse alive and cheap — the fast path is the concurrency guard (base-tree.ts:167-174) — and must not silently substitute a re-resolved base when the base branch could not be fetched (base-tree.ts:151-157). Please add the production-geometry test the fix implies — plan inside <repo>/.qwen/tmp, forge the marker's second line to runEpochMs(plan) of the same plan, plant dist/cli.js — and confirm that removing the new provenance arm turns it red.

中文说明

仍然成立(第 15-22 轮;本轮再次经探针确认)——base-tree 复用栅栏依赖沙箱读写挂载暴露的输入:epoch 是 statSync(plan).mtimeMs - 2000,且该常量已导出(prompt-record.ts:45),而 plan 文件位于挂载目录内的 .qwen/tmp/qwen-review-pr-<n>-fetch.json。在容器化 build/test 阶段运行的被审代码可以算出完全相同的 stamp 值、改写 .qwen-review-base-ok,并把未跟踪内容(如 dist/cli.js)放进一个真实遗留的 -base 树(cleanStale 从不释放 -base)。此时复用的每个条件都会通过——树是真实的、rev-parse HEAD 匹配、已跟踪状态干净(植入物是未跟踪文件)——runBaseTree 返回 "reusing it" 且 build: null,于是 A/B 的 base 侧运行在被攻击者整形的树上,test-delta 会把本 PR 的回归判成「测量上早已存在」。

修复建议:Move the trust artifacts out of the writable surface, per the round-17 direction: a random per-run nonce persisted beside REVIEW_LEASE_DIR (the existing "nothing mounts it" precedent), stamping both markers, and pin mergeBaseSha outside the mount — so a mount-local writer can neither refresh the epoch nor forge it; a leftover tree then always fails the fence and is rebuilt, taking any plants with it.

— qwen3.8-max via Qwen Code /review (v0.23.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.

[Critical] R15-4: [certifies-falsely] [new-surface] Still standing (rounds 15-24; probe re-confirmed at this head) — the reuse fence keys on the plan's mtime, and the plan file lives inside the very directory the sandbox mounts read-write, so the adversary the fence exists to keep out can read the key and forge the stamp. A run that dies abnormally leaves the -base tree (cleanStale never releases it); the next run's plan lands at .qwen/tmp/qwen-review-pr-<n>-fetch.json inside the mount; its containerized build/test phase stats the plan, computes String(mtimeMs - RUN_EPOCH_SLACK_MS) (an exported public constant), rewrites .qwen-review-base-ok with sha+epoch, and plants untracked executables (node_modules/.bin/*, dist/cli.js) that the --untracked-files=no dirt check cannot see while leaving the gitfile untouched — every fast-path arm passes and runBaseTree returns "reusing it", handing the host-side A/B a tree whose build output the PR author chose. Under --resume the plan is deliberately untouched ("its mtime is the run epoch"), so the epoch is identical and no forgery is even needed.

Witness:

[probe] INTACT: forged stamp -> note contains 'reusing it', builds: 1, planted dist/cli.js survives
CONTROL (plan mtime +60s, no forge): rebuild

Fix: park the run-epoch's source outside the writable mount — e.g. write the plan (or a small epoch sidecar) beside the leases this PR already moved to .qwen/review-leases/, so stamp[1] is compared against a value the container cannot read — or stamp both markers with a per-run nonce persisted outside the mount (the round-17 thread's direction).

The fix must keep same-run reuse alive and cheap (the opening sweep must not destroy the tree a sibling shard is mid-A/B in, base-tree.ts's own rationale), and a baseFetchFailed re-resolved base must not silently substitute a different commit. Please add a test asserting the epoch source resolves outside mountRootFor's root, and that a forged-stamp plant is rebuilt; removing the move must turn it red.

中文说明

仍然成立(第 15-24 轮;本轮探针再次确认)——复用围栏以 plan 文件的 mtime 为键,而 plan 文件就在沙箱以读写方式挂载的目录里,因此围栏要防的对手可以读到键并伪造戳记。异常退出的运行会留下 -base 树(cleanStale 从不释放它);下一次运行的 plan 落在挂载内的 .qwen/tmp/qwen-review-pr-<n>-fetch.json;其容器化的构建/测试阶段可以 stat 该 plan、算出 String(mtimeMs - RUN_EPOCH_SLACK_MS)(导出的公共常量)、用 sha+epoch 改写 .qwen-review-base-ok,并植入 --untracked-files=no 的脏检查按设计看不到的未跟踪可执行文件(node_modules/.bin/*dist/cli.js),同时不动 gitfile——快速通道的每一臂都通过,runBaseTree 返回 "reusing it",把宿主侧 A/B 交给一棵构建产物由 PR 作者选定的树。在 --resume 下 plan 刻意不被触碰("它的 mtime 就是运行纪元"),epoch 完全相同,连伪造都不需要。

证据:探针——原样代码:伪造戳记 → note 含 'reusing it',builds: 1,植入的 dist/cli.js 存活;对照臂(plan mtime +60s、不伪造)→ 重建。

修复:把运行纪元的来源挪出可写挂载——例如把 plan(或一个小的 epoch 旁车文件)写到本 PR 已经迁移的 .qwen/review-leases/ 旁,使 stamp[1] 比较的是容器读不到的值——或按第 17 轮线程的方向,用持久化在挂载外的按运行随机 nonce 给两个 marker 打戳。

修复必须保持同一运行内的复用仍然可用且廉价(开场清扫不能毁掉兄弟分片正在 A/B 的树,见 base-tree.ts 自身的理由),且 baseFetchFailed 时重新解析出的 base 不得静默替换为不同的提交。请新增测试断言 epoch 来源解析在 mountRootFor 的根之外、伪造戳记的植入会被重建;撤销该迁移后测试应变红。

— qwen3.8-max via Qwen Code /review (v0.23.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.

[Critical] R15-4: [certifies-falsely] [new-surface] Still standing (rounds 15-25; probe re-confirmed rounds 15-24, mechanism re-verified at this head, independently re-derived this round) — the reuse fence keys on the plan's mtime, and the plan file lives inside the very directory the sandbox mounts read-write, so the adversary the fence exists to keep out can read the key and forge the stamp. A run that dies abnormally leaves the -base tree (cleanStale never releases it); the next run's plan lands at .qwen/tmp/qwen-review-pr-<n>-fetch.json inside the mount; its containerized build/test phase stats the plan, computes String(mtimeMs - RUN_EPOCH_SLACK_MS) (an exported public constant), rewrites .qwen-review-base-ok with sha+epoch, and plants untracked executables (node_modules/.bin/*, dist/cli.js) that the --untracked-files=no dirt check cannot see while leaving the gitfile untouched — every fast-path arm passes and runBaseTree returns "reusing it", handing the host-side A/B a tree whose build output the PR author chose. Under --resume the plan is deliberately untouched ("its mtime is the run epoch"), so the epoch is identical and no forgery is even needed. A sibling state of the same fence — an untracked plant in a tree THIS run built, needing no forgery at all — is posted separately as R25-1.

Witness:

[probe, round 24] INTACT: forged stamp -> note contains 'reusing it', builds: 1, planted dist/cli.js survives
CONTROL (plan mtime +60s, no forge): rebuild
Round 25: mechanism re-verified at this head (runEpochMs = statSync(plan).mtimeMs - RUN_EPOCH_SLACK_MS, prompt-record.ts:60; plan written under REVIEW_TMP_DIR); independently re-derived by this round's chunk-1 finder and confirmed by its verifier trace.

Fix: park the run-epoch's source outside the writable mount — e.g. write the plan (or a small epoch sidecar) beside the leases this PR already moved to .qwen/review-leases/, so stamp[1] is compared against a value the container cannot read — or stamp both markers with a per-run nonce persisted outside the mount (the round-17 thread's direction).

The fix must keep same-run reuse alive and cheap (the opening sweep must not destroy the tree a sibling shard is mid-A/B in, base-tree.ts's own rationale), and a baseFetchFailed re-resolved base must not silently substitute a different commit. Please add a test asserting the epoch source resolves outside mountRootFor's root, and that a forged-stamp plant is rebuilt; removing the move must turn it red.

中文说明

仍然成立(第 15-25 轮;第 15-24 轮探针确认,本轮机制复查通过,且本轮独立重新推导)——复用围栏以 plan 文件的 mtime 为键,而 plan 文件就在沙箱以读写方式挂载的目录里,因此围栏要防的对手可以读到键并伪造戳记。异常退出的运行会留下 -base 树(cleanStale 从不释放它);下一次运行的 plan 落在挂载内的 .qwen/tmp/qwen-review-pr-<n>-fetch.json;其容器化的构建/测试阶段可以 stat 该 plan、算出 String(mtimeMs - RUN_EPOCH_SLACK_MS)(导出的公共常量)、用 sha+epoch 改写 .qwen-review-base-ok,并植入 --untracked-files=no 的脏检查按设计看不到的未跟踪可执行文件(node_modules/.bin/*dist/cli.js),同时不动 gitfile——快速通道的每一臂都通过,runBaseTree 返回 "reusing it",把宿主侧 A/B 交给一棵构建产物由 PR 作者选定的树。在 --resume 下 plan 刻意不被触碰("它的 mtime 就是运行纪元"),epoch 完全相同,连伪造都不需要。同一围栏的兄弟形态——本轮构建的树中被植入未跟踪可执行文件、完全无需伪造——已作为 R25-1 单独发布。

证据:第 24 轮探针——原样代码:伪造戳记 → note 含 'reusing it',builds: 1,植入的 dist/cli.js 存活;对照臂(plan mtime +60s、不伪造)→ 重建。第 25 轮:机制在本 head 复查确认;本轮 chunk-1 审查员独立重新推导,其验证者以代码追踪确认。

修复:把运行纪元的来源挪出可写挂载——例如把 plan(或一个小的 epoch 旁车文件)写到本 PR 已经迁移的 .qwen/review-leases/ 旁,使 stamp[1] 比较的是容器读不到的值——或按第 17 轮线程的方向,用持久化在挂载外的按运行随机 nonce 给两个 marker 打戳。

修复必须保持同一运行内的复用仍然可用且廉价(开场清扫不能毁掉兄弟分片正在 A/B 的树,见 base-tree.ts 自身的理由),且 baseFetchFailed 时重新解析出的 base 不得静默替换为不同的提交。请新增测试断言 epoch 来源解析在 mountRootFor 的根之外、伪造戳记的植入会被重建;撤销该迁移后测试应变红。

— qwen3.8-max via Qwen Code /review (v0.23.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.

[Critical] R15-4: [certifies-falsely] [new-surface] Still standing (rounds 15-26; probe re-confirmed at this head) — the base-tree reuse fence keys on inputs the sandbox's read-write mount exposes: the epoch is statSync(plan).mtimeMs - 2000 (lib/prompt-record.ts:60, formula and slack exported) and the plan lives at .qwen/tmp/qwen-review-pr-<n>-fetch.json inside the mounted dir, so the reviewed code can forge the stamp and make the pipeline certify an attacker-shaped tree as the A/B base. A leftover -base tree is genuine (cleanStale never releases it), so every pointer/shape check passes and the forgeable epoch is the only line left: during the containerized phase the reviewed code stats the plan, rewrites .qwen-review-base-ok line 2 with the computed epoch, drops untracked dist/cli.js into the tree — every reuse condition passes, runBaseTree answers "reusing it", and the A/B's BASE side runs against the attacker-shaped tree, so a test the PR breaks fails identically on both sides and a real regression is reported "pre-existing by measurement". The sibling entrances stand too: the failed marker is compared sha-only (R22-1), and the ok-marker itself is attacker-writable inside the mount.

Witness:

PROBE-B (honest stale stamp): fence speaks, rebuilt, plant swept
PROBE-A (forged stamp = runEpochMs of the live plan): note "...(reusing it — a concurrent or earlier probe built it)"
PROBE-A builds called: 0
PROBE-A plant survived: true
PROBE-C (failed marker, sha only): "already failed to build" (available: false)
— three arms on the real runBaseTree at ac2e3eee

Fix: move the trust artifacts out of the writable surface — a random per-run nonce persisted beside REVIEW_LEASE_DIR (paths.ts:65, the existing "nothing mounts it" precedent), stamping both markers, and mergeBaseSha pinned outside the mount, so a mount-local writer can neither refresh nor forge the epoch.

The fix must not violate the escalated thread's own constraints: keep same-run reuse alive and cheap (base-tree.ts:172-174 exists so shard B's opening sweep cannot destroy the tree shard A is mid-A/B in), and respect base-tree.ts:149-157 (baseFetchFailed means the merge base may be stale — a re-resolved base must not silently substitute a different commit). Fix witness: a base-tree.test.ts case with production geometry (plan INSIDE <repo>/.qwen/tmp) — round 1 builds legitimately, then the marker epoch is rewritten to runEpochMs(plan) of a fresh-mtime plan and dist/cli.js is planted; assert the note does NOT contain "reusing it" and the plant is gone, and prove it by removing the fence arm and watching that test go red.

中文说明

依然成立(第 15-26 轮;本轮在该 head 上再次探针确认)——base-tree 复用栅栏依赖的输入暴露在沙箱读写挂载之下:epoch 是 statSync(plan).mtimeMs - 2000lib/prompt-record.ts:60,公式与 slack 均已导出),而 plan 就在挂载目录内的 .qwen/tmp/qwen-review-pr-<n>-fetch.json,因此被审代码可以伪造戳记,让流水线把攻击者塑造的树认证为 A/B 基线。遗留的 -base 树是真实的(cleanStale 从不释放它),所有指针/形状检查都通过,可伪造的 epoch 成为唯一防线:容器化阶段被审代码 stat plan、把 .qwen-review-base-ok 第二行改写为计算出的 epoch、向树中放入未跟踪的 dist/cli.js——所有复用条件通过,runBaseTree 回答「reusing it」,A/B 的 BASE 侧在攻击者塑造的树上运行,PR 弄坏的测试在两侧同样失败,真实回归被报告为「测量意义上的既有问题」。兄弟入口同样成立:failed 标记仍只按 sha 比较(R22-1),ok 标记本身也在挂载内可被改写。

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

// The reuse path RETURNS, so the gate below the rebuild never runs for
// it — and both facts it reuses on come from inside the mount: the
// marker is a file in the base tree, and `rev-parse HEAD` resolves
// through that tree's own `.git`. A planted repository answers the right
// sha for a working tree holding anything at all, and the A/B's BASE
// side is then the reviewed code's own — every "the base behaves
// differently" verdict the shard reports would be its author's.
//
// Not a refusal: an unusable leftover is what the rebuild exists for.
// Falling through discards the tree (removing the plant with it) and
// creates a fresh one through the review worktree's pointer, which the
// gate before `worktree add` checks. Same shape as `scratch-tree`'s
// reuse path, for the same reason.
untrustedGitfile(tree) === null &&
Comment thread
qwen-code-dev-bot marked this conversation as resolved.
gitOut(tree, 'rev-parse', 'HEAD') === 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] R25-1: [certifies-falsely] The epoch fence excludes only trees built by a DIFFERENT run — it is blind to an untracked executable plant dropped into a tree THIS run built, while reviewed code still holds the read-write mount, and the pipeline's own documented flow guarantees that window. agent-briefs welds base-tree immediately before test-delta, which requires the built base tree and reruns the PR's FAILED test commands containerised with the rw mount (build-test.ts's run) or host-spawned when the sandbox is off (refuseUnsandboxedPhase gates, but the containerised lane writes the mount by design); Step-4 verifier probes additionally execute PR test code in scratch trees under the same directory. So reviewed code is guaranteed write access to the built base tree within the same run, after its marker already carries this run's epoch. dist and node_modules are gitignored in this repo, so the --untracked-files=no dirt check (the anchored condition's sibling arm) cannot see a planted dist/cli.js; the epoch arm answers (same run — no forgery needed), the sha arm answers, the gitfile is the pipeline's own, and a later same-run caller — a sibling shard, the fast path's stated purpose — reuses the planted tree: runBaseTree certifies "reusing it" and the host-side A/B executes the PR author's content, so a regression the PR introduces measures as pre-existing with deterministic provenance. This is the sibling state of R15-4's cross-run forge; R15-4's fix (an epoch source outside the mount) does not close this one — here the epoch is legitimately this run's.

Witness:

[probe, round 25] git status --porcelain --untracked-files=no on a base-shaped worktree holding a planted dist/cli.js -> output: [EMPTY] -> CLEAN: reuse certified despite the plant; HEAD matches baseSha; the untracked-inclusive variant shows only ?? .qwen-review-base-ok, not the gitignored plant
Ordering traced at HEAD: agent-briefs welds base-tree -> test-delta (gated on refuseUnsandboxedPhase, containerised via build-test's run or host-spawned with sandbox off)
vitest probe of runBaseTree itself not run — the rebuilt review worktree has no node_modules (an external sweep destroyed the first worktree mid-run)

Fix: do not let provenance alone vouch for untracked content — build/hold the base tree outside the directory the sandbox mounts read-write, or record a manifest of the pipeline build's own outputs at marker-write time and compare it at reuse; or at minimum document and pin with a test that the base tree is only ever built after the run's final containerized exposure of reviewed code — and close the post-build exposure that contradicts it.

Same-run reuse must stay alive and cheap — the fast path exists so a concurrent shard does not clobber a tree a sibling is mid-A/B in (base-tree.ts's own rationale) — and the dirt check must stay --untracked-files=no: "an untracked-inclusive check would call every correctly-built tree dirty and disable reuse outright" (comment this diff adds at this site). Please add a test planting an untracked gitignored executable in a same-run-built base tree (whose marker carries this run's epoch) and asserting reuse is declined or the plant is swept; removing the new check must turn it red.

中文说明

epoch 围栏只排除由「不同运行」构建的树——它对「本轮运行自己构建的树」中被植入的未跟踪可执行文件是盲的,而被审查代码此时仍持有读写挂载,且流水线自己的文档化流程保证了这个窗口存在。agent-briefsbase-tree 紧接在 test-delta 之前焊接,后者需要已构建的 base 树,并以读写挂载容器化地重跑 PR 的失败测试命令(build-test.ts 的 run;沙箱关闭时则宿主直跑,refuseUnsandboxedPhase 有门,但容器化通道按设计就写这个挂载);此外第 4 步验证者的探针还会在同一目录下的 scratch 树中执行 PR 测试代码。因此被审查代码在同一运行内、在 marker 已带上本轮 epoch 之后,必然对已构建的 base 树有写权限。本仓库中 distnode_modules 都被 gitignore,所以 --untracked-files=no 的脏检查(锚定条件的兄弟臂)看不到植入的 dist/cli.js;epoch 臂答对(同一运行——无需伪造)、sha 臂答对、gitfile 是流水线自己的,于是同一运行内后来的调用者——兄弟分片,正是快速通道存在的目的——会复用被植入的树:runBaseTree 认证 "reusing it",宿主侧 A/B 执行 PR 作者选定内容,PR 引入的回归被测量为「先前已存在」,且带确定性来源标签。这是 R15-4 跨运行伪造的兄弟形态;R15-4 的修复(把 epoch 来源挪出挂载)关不上这一形态——这里 epoch 合法地就是本轮的。

证据:第 25 轮探针——对一棵持有植入 dist/cli.js 的 base 形态工作树运行 git status --porcelain --untracked-files=no → 输出:[EMPTY] → CLEAN:植入存活而复用被认证;HEAD 与 baseSha 匹配;含未跟踪的变体只显示 ?? .qwen-review-base-ok,看不到被 gitignore 的植入物。顺序在本 head 追踪确认:agent-briefs 把 base-tree 焊接在 test-delta 之前(经 refuseUnsandboxedPhase 门,容器化经 build-test 的 run,或沙箱关闭时宿主直跑)。对 runBaseTree 本体的 vitest 探针未运行——重建后的审查工作树没有 node_modules(第一轮工作树在运行中途被外部清扫销毁)。

修复:不要让「来源证明」独自为未跟踪内容背书——把 base 树构建/保存在沙箱读写挂载目录之外,或在写 marker 时记录流水线自身构建产物的清单并在复用时比对;或至少用测试文档化并钉住「base 树只在本轮对被审查代码的最后一次容器化暴露之后构建」——并关闭与之矛盾的构建后暴露。

同一运行内的复用必须保持可用且廉价——快速通道的存在是为了让并发分片不毁掉兄弟正在 A/B 的树(base-tree.ts 自己的理由)——且脏检查必须保持 --untracked-files=no:"含未跟踪的检查会把每棵正确构建的树都判为脏、彻底禁用复用"(本 diff 在此处新增的注释)。请新增测试:在本轮构建的 base 树(marker 带本轮 epoch)中植入一个未跟踪且被 gitignore 的可执行文件,断言复用被拒绝或植入物被清扫;移除新检查后测试应变红。

— qwen3.8-max via Qwen Code /review (v0.23.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.

[Critical] R25-1: [certifies-falsely] Still standing (rounds 25-26; probe re-confirmed at this head) — the epoch fence excludes only trees built by a DIFFERENT run: it is blind to an untracked executable plant dropped into a tree THIS run built, while reviewed code still holds the read-write mount, and the pipeline's own documented flow guarantees that window (the sandboxed build/test phase overlaps the other agents, and the base tree "is a direct child of the directory the sandbox mounts read-write"). A sandboxed PR-side command writes ../<review>-base/dist/cli.js or node_modules/.bin/<x> after the marker is stamped; the next runBaseTree call passes every arm — epoch genuine (same run, plan untouched), pointer intact (the plant is content, not a pointer), HEAD correct, --untracked-files=no blind — and certifies "reusing it". The host-side A/B then executes the attacker-written dist/cli.js as the BASE side: a test the PR breaks "fails identically on both sides" (finding suppressed) or the plant fabricates a difference — the outcome the fence's own comment says it exists to prevent, which never speaks because the stamp is genuine and the run never ended.

Witness:

P3 tracked status = ""
P3 untracked status = "?? .qwen-review-base-ok\n?? dist/\n?? node_modules/"
P3 second.available = true
P3 buildsB (a rebuild would appear here) = []
P3 note says REUSING = true
P3 plant still standing = true
— the plant was dist/cli.js and node_modules/.bin/vitest, written after the marker with the plan untouched

Fix: fence reuse on the tree's untracked surface, not just its run identity — at marker write, record HOST-SIDE (outside the mount; the marker itself is attacker-writable per R15-4's class) a digest of the untracked path set the build legitimately left; at reuse, require the current untracked set to be a subset of the recorded one, rebuilding otherwise.

base-tree.ts fast-path comment: "--untracked-files=no: the pipeline's own build leaves node_modules/ and dist/ here, so an untracked-inclusive check would call every correctly-built tree dirty and disable reuse outright" — the fix must keep same-run reuse working for legitimate build residue. Fix witness: a case that builds once, then plants ONLY untracked dist/cli.js without touching the plan mtime, must not reuse (note lacks "reusing it", the plant is gone, the build callback fired again); removing the subset check must turn it red.

中文说明

依然成立(第 25-26 轮;本轮探针再确认)——epoch 栅栏只排除由另一次运行构建的树:对被审代码仍持有读写挂载时放进本次运行所建树中的未跟踪可执行植入物,它是盲的,而流水线自己文档化的流程保证了这个窗口(沙箱化构建/测试阶段与其他代理重叠,且 base 树「是沙箱以读写挂载的目录的直接子目录」)。沙箱化的 PR 侧命令在标记写入后写入 ../<review>-base/dist/cli.jsnode_modules/.bin/<x>;下一次 runBaseTree 调用通过所有分支——epoch 真实(同一运行、plan 未动)、指针完好(植入物是内容而非指针)、HEAD 正确、--untracked-files=no 看不见——并认证「reusing it」。宿主侧 A/B 随后把攻击者写入的 dist/cli.js 当作 BASE 侧执行:PR 弄坏的测试「在两侧同样失败」(发现被压制),或植入物伪造差异——这正是栅栏自身注释声称要防止的结果,而它永远不会发声,因为戳记是真实的、运行也从未结束。

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

// ...and its CONTENTS must still be the base's. `rev-parse HEAD` does not
// move when working files change, and this tree is a direct child of the
// directory the sandbox mounts read-write — which is why the pointer gate
// above speaks at all. So the reviewed PR's own build, which runs before
// the verifier shards get here, can overwrite the base checkout's tracked
// sources with a plain copy: every condition above still passes, and the
// A/B then compares the PR against a copy of itself. A test the PR breaks
// fails identically on both sides, `test-delta` classifies the regression
// as pre-existing, and a real finding against the PR is suppressed.
//
// `--untracked-files=no`: the pipeline's own build leaves `node_modules/`
// and `dist/` here, so an untracked-inclusive check would call every
// correctly-built tree dirty and disable reuse outright — reintroducing
// the concurrent-shard clobber this fast path exists to prevent. Tracked
// dirt is what a rewrite leaves, and `npm run build` in this repository
// modifies no tracked file, so a legitimate build still reuses.

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] R26-1: [certifies-falsely] [regression] New this round — the tracked-dirt arm's premise is repo-specific, and the failure mode it chooses is destructive. This comment's own concession holds only for this repository: for any OTHER repo this CLI reviews, a build that modifies tracked files (codegen, lockfile rewrites — common) makes the arm fire on a tree THIS run built, and even here a concurrent shard's A/B writing one tracked file (a snapshot --update, format/lint:fix) does the same. Every fence failure falls through to discardWorktree — sweeping a live tree another shard is mid-A/B in and reintroducing exactly the concurrent-shard clobber the fast path exists to prevent: shard B takes the free build lock, rmSync's shard A's tree, and A's base side reads empty output — a fabricated difference with a deterministic source tag, the outcome the fast-path comment at :170-176 names. At the merge base the same tree was reused and survived (measured).

Witness:

P1.A tracked status after build = "M generated.txt"
P1.A buildsB = [".../review-pr-1-base"]  ← shard B REBUILT
P1.A shard A sentinel survives = false  ← A's tree was rmSync'd
P1.B (clean build; the A/B writes ONE tracked file) buildsB = [...], sentinel survives = false
Flip (same-run marker → decline instead of fall through): buildsB = [], sentinel survives = true, note "built by THIS run but no longer passes a reuse check; declining rather than discarding"
BASE arm (dirt arm neutralised at e3d2628's gate): buildsB = [], sentinel survives = true, note "(reusing it)"

Fix: when stamp[0] === baseSha && stamp[1] === runEpoch (this run's own tree) but a state arm fails, return the busy/unavailable retry shape (as the lock's EEXIST arm at :291-298 does) instead of falling through to rebuild; keep discard-and-rebuild for cross-run leftovers and the absent-marker case.

The fence comment's own requirement — "Falling through discards the tree (removing the plant with it)" — must still hold for cross-run/planted trees: the busy arm may only engage when the epoch arm matched; and the epoch arm itself is the open R15-4 escalation, so this fix must not be argued as closing the plant path. Fix witness: a base-tree.test.ts case — first run stamps the marker, then ONE tracked file is rewritten, a second run (same cached plan → same epoch) must leave the dirtied file on disk and report busy/unavailable rather than rebuild; removing the busy arm must turn it red (the existing 'does not REUSE … tracked files were rewritten' case flips from rebuild to busy for the same-run shape it exercises).

中文说明

本轮新发现——被跟踪脏文件分支的前提是仓库特定的,而它选择的失败模式是破坏性的。这条注释自己的让步只对本仓库成立:对本 CLI 审查的任何其他仓库,修改被跟踪文件的构建(代码生成、锁文件重写——很常见)会让该分支在一次由本次运行构建的树上触发;即使在本仓库,并发分片的 A/B 写入一个被跟踪文件(快照 --updateformat/lint:fix)也一样。每次栅栏失败都落入 discardWorktree——清扫另一个分片正在 A/B 中的活树,恰好重新引入快路径存在要防止的并发分片摧毁:分片 B 拿到空闲的构建锁,rmSync 掉分片 A 的树,A 的 base 侧读到空输出——一个带确定性来源标签的伪造差异,即 :170-176 处快路径注释点名的结果。在合并基线上,同一棵树会被复用并存活(已测量)。

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

gitOut(tree, 'status', '--porcelain', '--untracked-files=no') === ''
Comment thread
qwen-code-dev-bot marked this conversation as resolved.
) {
return {
available: true,
Expand Down Expand Up @@ -265,6 +311,16 @@ export function runBaseTree(args: BaseTreeArgs): BaseTreeReport {
// 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);
// The same question the probe phase asks before its own `worktree add`:
// this resolves the repository through the REVIEW worktree's gitfile,
// which lives inside the directory the sandbox mounts read-write and
// which the build/test phase already gave the reviewed code a chance to
// rewrite. `worktree add` checks files out, so it runs whatever that
// pointer leads to, on the host. See `untrustedGitfile`.
const untrusted = untrustedGitfile(worktree);
if (untrusted !== null) {
throw new Error(`refusing to create a base tree: ${untrusted}`);
}
git(worktree, 'worktree', 'add', '--detach', tree, baseSha);
} catch (e) {
return unavailable(
Expand Down Expand Up @@ -335,9 +391,9 @@ export function runBaseTree(args: BaseTreeArgs): BaseTreeReport {
}

// The marker is what the fast path above trusts, so it is written only after
// a build that succeeded, and it records the SHA it vouches for.
// a build that succeeded, and it records the SHA and the run it vouches for.
try {
writeFileSync(marker(), `${baseSha}\n`);
writeFileSync(marker(), `${baseSha}\n${runEpoch}\n`);

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] R22-1: [fails-closed] The epoch fence this diff adds to the reuse fast path is missing from the sibling failed-marker fast path ten lines above (base-tree.ts:246-249, unchanged): existsSync(tree) && readFileSync(failedMarker(), 'utf8').trim() === baseSha returns a settled "already failed to build" verdict with no epoch check, before the lock and before the rebuild that would discard a plant. The base tree location is a direct child of the mounted .qwen/tmp, and the merge-base sha the plant needs is readable from the plan JSON in the same mount — so one mkdir plus one file write by the reviewed code settles every later base-tree request of the whole review (every verifier shard, every later round until cleanup) as "A/B unavailable (infrastructure)", mislabeled as a measured failure, with the plant never swept.

Witness:

Probe at this commit (scratch tree, real runBaseTree, flip-checked):
PR arm — plant <wt>-base holding ONLY .qwen-review-base-failed with the plan's merge-base sha:
  available=false, note "…already failed to build (an earlier probe measured it)…",
  builds=[]           # zero builds — the settled answer precedes the rebuild
FIX arm — with the implied epoch fence on read+write:
  available=true, note "base tree built at 74a4dd9cf …", plant discarded, rebuild runs

Fix: mirror the fence — write ${baseSha}\n${runEpoch}\n at the failed-marker write site and require the second line to equal this run's epoch at the read site, so a failed marker from another run — or a plant with no epoch at all — falls through to the rebuild that discards it.

Fix constraint: base-tree.ts:241-243 — "The marker makes the failure a SETTLED answer for every later shard": a genuine build failure must still answer settled for every later shard of the SAME run (same-run shards share the plan epoch), so the fix must key on the run epoch, not delete the branch.

Fix witness: a base-tree.test.ts case in the shape of "does not REUSE a base tree an EARLIER RUN built": first run returns failedBuild (failed marker written), then utimesSync advances the plan (next run), and a second run({}, build) must attempt a rebuild (builds gains an entry) instead of the settled note — deleting the epoch condition from the failed-marker branch reds it; its same-run twin (no plan advance, settled note, one build) pins the constraint.

中文说明

本 diff 给复用快路径加的 epoch 栅栏,漏掉了上方十行处的孪生失败标记快路径(base-tree.ts:246-249,未改动):existsSync(tree) && readFileSync(failedMarker(), 'utf8').trim() === baseSha 在锁之前、在会丢弃植入物的重建之前,就以零 epoch 检查返回「已经构建失败」的定论。base 树位置是挂载的 .qwen/tmp 的直接子目录,植入所需的合并基 sha 又可以从同一挂载内的 plan JSON 读到——于是被审代码只需一次 mkdir 加一次文件写入,就能让整个审查后续每一次 base-tree 请求(每个验证分片、直到 cleanup 的每一后续轮次)都定论为「A/B 不可用(基础设施)」,被伪装成一次已测量的失败,且植入物永不被清扫。

修复:把栅栏镜像过去——在失败标记写入点写 ${baseSha}\n${runEpoch}\n,并在读取点要求第二行等于本轮的 epoch,使来自其他轮次的失败标记(或根本没有 epoch 的植入物)落入会丢弃它的重建路径。

修复约束:base-tree.ts:241-243——「该标记让失败成为每个后续分片的定论」:真实的构建失败对同一轮次的每个后续分片仍必须定论(同轮分片共享 plan epoch),因此修复必须以轮次 epoch 为键,而不是删除该分支。

修复见证:在 base-tree.test.ts 按「不复用更早一轮构建的 base 树」形状加用例:第一轮返回 failedBuild(写失败标记),然后 utimesSync 前移 plan(下一轮),第二次 run({}, build) 必须尝试重建(builds 增加一项)而不是返回定论——从失败标记分支删除 epoch 条件该测试应变红;其同轮孪生用例(不前移 plan、定论、一次构建)钉住上述约束。

— qwen3.8-max via Qwen Code /review (v0.23.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.

[Critical] R22-1: [certifies-falsely] Still standing (round 22; re-derived independently by both reverse auditors this round and probe-confirmed) — the epoch fence this diff adds to the ok-marker reuse fast path is missing from the sibling failed-marker fast path ten lines below (base-tree.ts:241-263, unchanged): existsSync(tree) && readFileSync(failedMarker(), 'utf8').trim() === baseSha returns a settled "already failed to build" verdict with no epoch check, before the lock and before the rebuild that would discard a plant. The merge-base sha is public knowledge and cleanStale never releases -base, so reviewed code can plant .qwen-review-base-failed (or pre-create the bare -base directory) during the containerized phase — and because the new epoch condition now makes every cross-round ok-marker reuse fall through, the planted failed marker is the first check that answers: A/B measurement is permanently suppressed while the pipeline reports a false "infrastructure result, never a finding against the PR".

Witness:

Probe at da52bea4: pre-created <wt>-base/ holding .qwen-review-base-failed with the base sha → ROUND1 available:false, note contains 'already failed to build', build seam never called (builds: [] — the discarding rebuild is unreachable); ROUND2 after advancing the plan's mtime → identical, the wedge self-perpetuates. FLIP: epoch-fencing the failed marker → probe fails with 'expected true to be false': available:true, the planted tree discarded by the rebuild.

Mirror the ok-marker fence: at the failed-marker write site (buildBaseTree) write ${baseSha}\n${runEpoch}\n, and in the fast path require stamp[0] === baseSha && stamp[1] === runEpoch — same shape as the reuse gate above.

The failed-marker WRITE site must gain the epoch line too, or genuine same-run failures never pass the fenced read; and do not special-case runEpochMs's -Infinity as a wildcard. Please add the base-tree.test.ts case planting a sha-only failed marker across a plan-mtime advance and asserting a rebuild — removing the epoch arm of the failed-marker check turns it red, and the existing 'a FAILED build is a settled answer' test must stay green.

中文说明

仍然成立(第 22 轮;本轮两位反向审计员独立重新推导并经探针确认)——本 diff 给 ok-marker 复用快路径加上的 epoch 栅栏,在下方十行的姊妹 failed-marker 快路径(base-tree.ts:241-263,未改动)上缺失:existsSync(tree) && readFileSync(failedMarker(), 'utf8').trim() === baseSha 没有任何 epoch 检查,就在锁之前、在会丢弃植入物的重建之前返回「早已构建失败」的定论。merge-base sha 是公开信息,且 cleanStale 从不释放 -base,所以被审代码可以在容器化阶段植入 .qwen-review-base-failed(或预先创建空的 -base 目录)——而新的 epoch 条件现在让每一轮跨轮的 ok-marker 复用都落空,于是植入的 failed marker 成为第一个回答的检查:A/B 测量被永久抑制,流水线却报告一个虚假的「基础设施问题,绝不构成本 PR 的发现」。

修复建议:Mirror the ok-marker fence: at the failed-marker write site (buildBaseTree) write ${baseSha}\n${runEpoch}\n, and in the fast path require stamp[0] === baseSha && stamp[1] === runEpoch — same shape as the reuse gate above.

— qwen3.8-max via Qwen Code /review (v0.23.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.

[Critical] R22-1: [certifies-falsely] Still standing (rounds 22-24; probe re-confirmed at this head) — the run-epoch fence this diff adds to the ok-marker reuse fast path is missing from the sibling FAILED-marker fast path ten lines above (base-tree.ts:246-249, unchanged): existsSync(tree) && readFileSync(failedMarker(), 'utf8').trim() === baseSha returns a settled "already failed to build" verdict with no epoch check and no gitfile arm, before the lock and before the rebuild that would discardWorktree a plant. Both the -base tree and the marker are direct children of .qwen/tmp (the read-write mount), and the plan JSON carrying mergeBaseSha lives in the same dir — so the containerized build/test phase can mkdir <worktree>-base and write .qwen-review-base-failed containing that sha (no fake gitdir needed; the failed-marker path runs no git), and every later base-tree call falls through the reuse fast path, hits the unfenced check, and returns "already failed to build (an earlier probe measured it) … infrastructure, never a finding against the PR": the review loses its only A/B measurement capability — including the test-delta measurement that turns "fails in an untouched file" into a Critical — and the refusal arrives certified as infrastructure. The diff's own rationale condemns this path: it hardens the sibling marker precisely because "this tree is a direct child of the directory the sandbox mounts read-write".

Witness:

[probe] INTACT: planted failed marker served as a settled answer — note 'already failed to build (an earlier probe measured it); an A/B is not available for this review (infrastructure, never a finding against the PR)', builds: []
FIXED (epoch-fenced failed marker): rebuild sweeps the plant — note 'base tree built at …'

Fix: fence the failed marker the way the success marker is fenced — write ${baseSha}\n${runEpoch}\n in the build-failure arm and require stamp[1] === runEpoch in the fast-path read; a mismatch then falls through to the rebuild that sweeps the plant.

The fence must still accept a legitimate same-run failure marker as a settled answer — base-tree.ts's own rationale: "every shard that asks re-sweeps and re-pays the install+build to relearn the same 'unavailable' — and the sweep destroys the evidence tree the failure deliberately leaves standing." Please add a test planting <tree> + .qwen-review-base-failed holding baseSha with no prior run, asserting the note is NOT "already failed to build" and a rebuild happens; removing the epoch arm from the failed-marker check must turn it red.

中文说明

仍然成立(第 22-24 轮;本轮探针再次确认)——本 diff 给 ok-marker 复用快速路径加上的运行纪元围栏,在上方十行的兄弟 FAILED-marker 快速路径(base-tree.ts:246-249,未改动)上是缺失的:existsSync(tree) && readFileSync(failedMarker(), 'utf8').trim() === baseSha 返回一个既定的「已经构建失败」判定——没有 epoch 检查、没有 gitfile 臂——发生在锁之前、也发生在会 discardWorktree 掉植入物的重建之前。-base 树与 marker 都是 .qwen/tmp(读写挂载)的直接子项,携带 mergeBaseSha 的 plan JSON 也在同一目录——因此容器化的构建/测试阶段可以 mkdir <worktree>-base 并写入含该 sha 的 .qwen-review-base-failed(不需要伪造 gitdir;失败 marker 路径不运行任何 git),之后每一次 base-tree 调用都会穿过复用快速路径、命中这个无围栏的检查,返回「已经构建失败(早先的探针测到过)……基础设施问题,绝不作为针对 PR 的发现」:审查失去唯一的 A/B 测量能力——包括把「在未触碰文件中失败」升级为 Critical 的 test-delta 测量——而这个拒绝以「基础设施」的名义被认证送达。本 diff 自己的理由恰恰谴责这条路径:它强化兄弟 marker 正是因为「这棵树是沙箱以读写挂载的目录的直接子项」。

证据:探针——原样代码:植入的失败 marker 被当作既定答案——note「已经构建失败(早先的探针测到过);本次审查无可用 A/B(基础设施问题,绝不作为针对 PR 的发现)」,builds: [];修复(给失败 marker 加 epoch 围栏):重建扫除植入物——note「base tree built at …」。

修复:像给成功 marker 加围栏那样给失败 marker 加围栏——在构建失败臂写入 ${baseSha}\n${runEpoch}\n,并在快速路径读取中要求 stamp[1] === runEpoch;不匹配则落入会扫除植入物的重建。

围栏仍必须把同一运行内合法的失败 marker 当作既定答案接受——base-tree.ts 自己的理由:「每个来询问的分片都会重新清扫、重新支付 install+build 去重新得知同样的『不可用』——而清扫会毁掉失败刻意留下的证据树」。请新增测试:植入 <tree> + 含 baseSha.qwen-review-base-failed(无先前运行),断言 note 不是「已经构建失败」且发生重建;删除失败 marker 检查中的 epoch 臂后测试应变红。

— qwen3.8-max via Qwen Code /review (v0.23.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.

[Critical] R22-1: [certifies-falsely] Still standing (rounds 22-25; probe re-confirmed rounds 22-24, mechanism re-verified at this head, independently re-derived this round) — the run-epoch fence this diff adds to the ok-marker reuse fast path (anchored here, at the fenced marker write) is missing from the sibling FAILED-marker fast path (existsSync(tree) && readFileSync(failedMarker(), 'utf8').trim() === baseSha, unchanged code between hunks): it returns a settled "already failed to build" verdict with no epoch check and no gitfile arm, before the lock and before the rebuild that would discardWorktree a plant. Both the -base tree and the marker are direct children of .qwen/tmp (the read-write mount), and the plan JSON carrying mergeBaseSha lives in the same dir — so the containerized build/test phase can mkdir <worktree>-base and write .qwen-review-base-failed containing that sha (no fake gitdir needed; the failed-marker path runs no git), and every later base-tree call falls through the reuse fast path, hits the unfenced check, and returns "already failed to build (an earlier probe measured it) … infrastructure, never a finding against the PR": the review loses its only A/B measurement capability — including the test-delta measurement that turns "fails in an untouched file" into a Critical — and the refusal arrives certified as infrastructure. The diff's own rationale condemns this path: it hardens the sibling marker precisely because "this tree is a direct child of the directory the sandbox mounts read-write".

Witness:

[probe, round 24] INTACT: planted failed marker served as a settled answer — note 'already failed to build (an earlier probe measured it); an A/B is not available for this review (infrastructure, never a finding against the PR)', builds: []
FIXED (epoch-fenced failed marker): rebuild sweeps the plant — note 'base tree built at …'
Round 25: mechanism verbatim at this head; independently re-derived by this round's round-2 verifier as an incidental finding.

Fix: fence the failed marker the way the success marker is fenced — write ${baseSha}\n${runEpoch}\n in the build-failure arm and require stamp[1] === runEpoch in the fast-path read; a mismatch then falls through to the rebuild that sweeps the plant.

The fence must still accept a legitimate same-run failure marker as a settled answer — base-tree.ts's own rationale: "every shard that asks re-sweeps and re-pays the install+build to relearn the same 'unavailable' — and the sweep destroys the evidence tree the failure deliberately leaves standing." Please add a test planting <tree> + .qwen-review-base-failed holding baseSha with no prior run, asserting the note is NOT "already failed to build" and a rebuild happens; removing the epoch arm from the failed-marker check must turn it red.

中文说明

仍然成立(第 22-25 轮;第 22-24 轮探针确认,本轮机制复查通过,且本轮独立重新推导)——本 diff 给 ok-marker 复用快速路径加上的运行纪元围栏(锚定于此,即加了围栏的 marker 写入处),在兄弟 FAILED-marker 快速路径(existsSync(tree) && readFileSync(failedMarker(), 'utf8').trim() === baseSha,位于 hunk 之间未改动的代码)上是缺失的:它返回一个既定的「已经构建失败」判定——没有 epoch 检查、没有 gitfile 臂——发生在锁之前、也发生在会 discardWorktree 掉植入物的重建之前。-base 树与 marker 都是 .qwen/tmp(读写挂载)的直接子项,携带 mergeBaseSha 的 plan JSON 也在同一目录——因此容器化的构建/测试阶段可以 mkdir <worktree>-base 并写入含该 sha 的 .qwen-review-base-failed(不需要伪造 gitdir;失败 marker 路径不运行任何 git),之后每一次 base-tree 调用都会穿过复用快速路径、命中这个无围栏的检查,返回「已经构建失败(早先的探针测到过)……基础设施问题,绝不作为针对 PR 的发现」:审查失去唯一的 A/B 测量能力——包括把「在未触碰文件中失败」升级为 Critical 的 test-delta 测量——而这个拒绝以「基础设施」的名义被认证送达。本 diff 自己的理由恰恰谴责这条路径:它强化兄弟 marker 正是因为「这棵树是沙箱以读写挂载的目录的直接子项」。

证据:第 24 轮探针——原样代码:植入的失败 marker 被当作既定答案——note「已经构建失败(早先的探针测到过);本次审查无可用 A/B(基础设施问题,绝不作为针对 PR 的发现)」,builds: [];修复(给失败 marker 加 epoch 围栏):重建扫除植入物——note「base tree built at …」。第 25 轮:机制在本 head 原样存在;本轮第 2 批验证者作为附带发现独立重新推导。

修复:像给成功 marker 加围栏那样给失败 marker 加围栏——在构建失败臂写入 ${baseSha}\n${runEpoch}\n,并在快速路径读取中要求 stamp[1] === runEpoch;不匹配则落入会扫除植入物的重建。

围栏仍必须把同一运行内合法的失败 marker 当作既定答案接受——base-tree.ts 自己的理由:「每个来询问的分片都会重新清扫、重新支付 install+build 去重新得知同样的『不可用』——而清扫会毁掉失败刻意留下的证据树」。请新增测试:植入 <tree> + 含 baseSha.qwen-review-base-failed(无先前运行),断言 note 不是「已经构建失败」且发生重建;删除失败 marker 检查中的 epoch 臂后测试应变红。

— qwen3.8-max via Qwen Code /review (v0.23.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.

[Critical] R22-1: [certifies-falsely] Still standing (rounds 22-26; probe re-confirmed at this head) — the run-epoch fence this diff adds to the ok-marker reuse fast path (anchored here, at the fenced marker write) is missing from the sibling FAILED-marker fast path: existsSync(tree) && readFileSync(failedMarker(), 'utf8').trim() === baseSha returns a settled "already failed to build … never a finding against the PR" verdict with no epoch check, before the lock and before the rebuild that would discard a plant. The failed marker is a file in the base tree, inside the read-write mount: reviewed code plants .qwen-review-base-failed containing the base sha during the containerized phase, and every later runBaseTree call this run answers "already failed to build" without attempting anything — the A/B lane is suppressed outright for the whole round, and the suppression reads as infrastructure rather than as an attack.

Witness:

PROBE-C (failed marker, sha only — no epoch arm): note "...already failed to build..." (available: false)
— the planted marker settles the verdict at this head, before the lock and before any rebuild

Fix: give the failed marker the same epoch fence the ok marker now carries (stamp baseSha + runEpoch, compare both), as part of the R15-4 trust-artifact move out of the writable surface.

Fix witness: a case planting a sha-only failed marker and then running with a fresh epoch must NOT settle on "already failed" (the rebuild is attempted); removing the epoch arm must turn it red.

中文说明

依然成立(第 22-26 轮;本轮探针再确认)——本 diff 给 ok 标记复用快路径加上的运行 epoch 栅栏(锚点在此,即被栅栏保护的标记写入处),在兄弟 FAILED 标记快路径上缺失:existsSync(tree) && readFileSync(failedMarker(), 'utf8').trim() === baseSha 会在锁之前、在会丢弃植入物的重建之前,直接返回「已经构建失败……绝不算 PR 的问题」的定论,且没有任何 epoch 检查。failed 标记是 base 树内的文件,位于读写挂载之内:被审代码在容器化阶段植入内容为 base sha 的 .qwen-review-base-failed,本轮之后每次 runBaseTree 调用都不做任何尝试而直接回答「已经构建失败」——A/B 通道在整轮中被彻底压制,而且这种压制看起来像基础设施故障而非攻击。

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

} catch {
// The tree may be too broken to hold a marker; the next shard rebuilds.
}
Expand Down
Loading
Loading