Skip to content

fix(core): reject unverifiable validated read inodes - #9857

Merged
wenshao merged 5 commits into
QwenLM:mainfrom
AaronZ345:fix/fail-zero-inode-validated-read
Aug 25, 2026
Merged

fix(core): reject unverifiable validated read inodes#9857
wenshao merged 5 commits into
QwenLM:mainfrom
AaronZ345:fix/fail-zero-inode-validated-read

Conversation

@AaronZ345

@AaronZ345 AaronZ345 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Rejects validated @-file reads when the approved or reopened file identity has an unverifiable inode value such as 0, instead of treating { dev, ino: 0 } as a usable identity match.

Why it's needed

Issue #8227 points out that validated file reads rely on (dev, ino) identity checks after approval. On filesystems where inode identity is unavailable or reported as 0, matching on inode 0 is vacuous: multiple files can share the same placeholder identity, so the validation step cannot prove the reopened file is the one the user approved. The new guard fails closed for unverifiable inode identities while keeping the existing identity checks for normal filesystems.

Reviewer Test Plan

How to verify

Run the targeted read-many-files tests, the core typecheck, and Prettier on the touched files.

Evidence (Before & After)

Before: a regression test with mocked fs.stat() / file-handle stat() returning ino: 0 would still include the validated file content. After: the same path is omitted from successful file content and returned as a visible read error explaining that the validated identity is unavailable on this filesystem.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested locally; covered by CI
🐧 Linux ⚠️ not tested locally; covered by CI

Environment (optional)

Local Node/npm workspace on macOS.

Risk & Scope

  • Main risk or tradeoff: Low to moderate; filesystems that cannot provide verifiable inode identity now fail closed for validated reads rather than allowing an unverifiable match.
  • Not validated / out of scope: Windows native filesystem manual testing; the inode-zero behavior is covered with mocked stat results.
  • Breaking changes / migration notes: None expected; this only affects validated reads whose identity cannot be verified.

Linked Issues

Refs #8227. This handles the inode-zero fail-closed behavior; Windows runner coverage and broader platform policy remain separate follow-up scope.

中文说明

What this PR does

当已批准或重新打开的文件身份里 inode 不可验证(例如 0)时,拒绝 validated @-file read,而不是把 { dev, ino: 0 } 当作可用身份继续匹配。

Why it's needed

#8227 指出 validated file read 在用户批准后依赖 (dev, ino) 身份校验。在某些文件系统上,inode 身份不可用或会被报告为 0;此时用 inode 0 匹配是空洞的,因为多个文件可能共享同一个占位身份,验证步骤无法证明重新打开的文件就是用户批准的文件。新逻辑对不可验证 inode fail closed,同时保留普通文件系统上的现有身份校验。

Reviewer Test Plan

How to verify

运行目标 read-many-files 测试、core typecheck,并对改动文件跑 Prettier。

Evidence (Before & After)

Before:mock fs.stat() / file-handle stat() 返回 ino: 0 的回归测试仍会包含 validated 文件内容。After:同一路径不会进入成功文件内容,而是作为可见读取错误返回,并说明当前文件系统无法提供 validated identity。

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested locally; covered by CI
🐧 Linux ⚠️ not tested locally; covered by CI

Environment (optional)

macOS 本地 Node/npm 工作区。

Risk & Scope

  • Main risk or tradeoff: 中低;无法提供可验证 inode 身份的文件系统现在会在 validated read 中 fail closed,而不是允许不可验证匹配。
  • Not validated / out of scope: 未做 Windows 原生文件系统手工测试;inode 为 0 的行为通过 mocked stat 覆盖。
  • Breaking changes / migration notes: 预期无;只影响身份不可验证的 validated read。

Linked Issues

Refs #8227. This handles the inode-zero fail-closed behavior; Windows runner coverage and broader platform policy remain separate follow-up scope.

Fail closed when validated @-file reads encounter zero inode identities so dev/ino comparisons cannot silently accept unrelated files on filesystems that do not expose stable inode numbers.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot qwen-code-ci-bot left a comment

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.

Thanks for the fix, @AaronZ345 — the direction matches #8227, but the PR description doesn't follow this repo's template, so it can't enter review as-is.

  • Missing required sections — the body needs the headings from the PR template: What this PR does, Why it's needed, Reviewer Test Plan (with How to verify, Evidence (Before & After), Tested on), Risk & Scope, Linked Issues, plus the Chinese translation in <details>.
  • Current body uses a generic Summary / Test plan structure instead. Please edit the PR description to fill in the template — the test commands you already listed fit directly under How to verify, and Fixes #8227 belongs under Linked Issues.
  • This is a formatting gate, not a verdict on the code. Once the description follows the template, re-trigger with @qwen-code /triage and the run will pick it up from the top.
中文说明

感谢这个修复,@AaronZ345 —— 方向与 #8227 一致,但 PR 描述没有遵循本仓库的模板,暂时无法进入评审。

  • 缺少必需章节 —— 正文需要使用 PR 模板 中的标题:What this PR doesWhy it's neededReviewer Test Plan(含 How to verifyEvidence (Before & After)Tested on)、Risk & ScopeLinked Issues,以及 <details> 中的中文翻译。
  • 当前正文使用的是通用的 Summary / Test plan 结构。请编辑 PR 描述按模板填写——已列出的测试命令可以直接放进 How to verifyFixes #8227 应放在 Linked Issues 下。
  • 这只是格式关卡,不是对代码的结论。描述按模板更新后,用 @qwen-code /triage 重新触发即可从头评审。

Qwen Code · qwen3.8-max

@AaronZ345

Copy link
Copy Markdown
Contributor Author

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

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.

Partially reviewed — gaps disclosed. Suggestions are inline.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not explored to full depth (tool budget reached): "agent 6a": executing readManyFiles.test.ts under vitest — the worktree has no node_modules / dist , so running requires a full npm ci + npm run build ; I relied on s….

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

@@ -159,6 +160,9 @@ export async function readManyFiles(
const displayPath = displayPaths?.get(fullPath) ?? fullPath;
const validatedIdentity = validatedPathIdentities?.get(fullPath);
if (validatedPathIdentities && !validatedIdentity) continue;
if (validatedIdentity && !hasVerifiableInode(validatedIdentity.ino)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Fixes #8227 overclaims the issue's scope. Issue #8227's Proposal has three items: (1) run the atCommandProcessor / Session / readManyFiles validated-read suites on a Windows runner (junctions + NTFS symlinks), (2) decide the posture for identity checks on ino: 0 filesystems — fail closed or document the residual risk, (3) un-skip whichever regression tests can run under Windows semantics. This diff implements only proposal 2 (the fail-closed branch); the O_NOFOLLOW collapse named in the issue title, the adjacent ino: 0 sites named in the issue thread (FileReadCache.inodeKey, workspace-file-system.ts assertSameFile, session-transcript-reader.ts), and proposals 1 and 3 are untouched — the PR body itself concedes "Not validated / out of scope: Windows native filesystem manual testing".

The cost is tracking: merging with Fixes auto-closes #8227, and the still-open Windows-runner validation and un-skipping work (both explicitly listed in the issue body) lose their tracking issue and remain tracked nowhere. Consider changing the keyword to Part of #8227 and opening follow-up issues for proposals 1 and 3, or expanding this PR to cover them before closing the issue. Note the ino: 0 fail-closed posture was recorded as a maintainer call in the issue thread, so a maintainer confirmation is worth obtaining either way.

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

Comment on lines +305 to +308
const stats = await source.stat();
if (
!stats.isFile() ||
!hasVerifiableInode(stats.ino) ||

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] This handle-bound validated-identity predicate is duplicated verbatim in snapshotValidatedFile (~lines 373-379), and this PR pasted the same new hasVerifiableInode clause into both copies in lockstep; a third variant lives in matchesValidatedPathIdentity (~line 342). The drift cost is demonstrated by this very change: the next change to identity semantics (another unverifiable-inode shape, a new comparison rule) must find and edit every copy, and missing one silently leaves either the text-handle read path or the binary snapshot path with a weaker check than its sibling — precisely the fail-closed asymmetry this change exists to eliminate. (matchesValidatedPathIdentity cannot be the shared helper for the two fd-stat sites — it re-resolves via realpath and re-stats by path instead of trusting the already-opened handle's stats.)

One small leaf predicate would close it:

function statsMatchValidatedIdentity(
  stats: fs.Stats,
  expected: ReadManyFilesPathIdentity,
): boolean {
  return (
    stats.isFile() &&
    hasVerifiableInode(stats.ino) &&
    stats.dev === expected.dev &&
    stats.ino === expected.ino
  );
}

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

@@ -159,6 +160,9 @@ export async function readManyFiles(
const displayPath = displayPaths?.get(fullPath) ?? fullPath;
const validatedIdentity = validatedPathIdentities?.get(fullPath);
if (validatedPathIdentities && !validatedIdentity) continue;
if (validatedIdentity && !hasVerifiableInode(validatedIdentity.ino)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] On inode-0 filesystems (FAT/exFAT, some SMB mounts — the class file-identity.ts documents), this guard silently drops every validated @-read: a bare continue, no log, no error entry in result.files, no user-visible reason. Before this PR those reads succeeded (0 === 0 matched — verified against HEAD~1), so this is a newly reachable silent failure with zero diagnostics. Both production callers (atCommandProcessor.ts, Session.ts) unconditionally record { dev, ino } from stat and always pass the map, so there is no fallback; on such mounts 100% of validated reads are dropped, permanently disabling @-mentions.

The user experience is an attachment vanishing: result.files stays empty, the TUI renders no "Read File" card, and the model only receives the generic "No files matching the criteria were found or all were skipped." — indistinguishable from a workspace-bounds bug, an ignore-rule bug, or a path-resolution bug. atCommandProcessor already renders FileReadInfo.error entries as visible failed reads, but a dropped path produces no FileReadInfo at all (and Session.ts consumes only contentParts, so the ACP path has no per-file rendering). The fail-closed rationale justifies the drop, not the silence. Surfacing a skip reason — e.g. pushing a FileReadInfo with error: 'inode identity unverifiable on this filesystem (ino=0)', or exposing skipped paths on the result — would make the failure self-describing. (Confirmed by running the new regression test: it asserts content excludes the file and result.files has length 0.)

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

Report validated reads with unverifiable inode identity as explicit file errors and share the file identity predicate across validated read paths.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

@qwen-code-ci-bot qwen-code-ci-bot left a comment

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.

Partially reviewed — gaps disclosed. Suggestions are inline.

1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not explored to full depth (tool budget reached): "agent 1a": running the packages/core vitest suite / typecheck for readManyFiles.test.ts — the review worktree has no node_modules installed, so compile/run verification co….

中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未探索到全部深度(达到工具调用预算):"agent 1a"running the packages/core vitest suite / typecheck for readManyFiles.test.ts — the review worktree has no node_modules installed, so compile/run verification co…

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

Comment on lines +168 to +169
contentParts.push(...errorParts);
files.push(info);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] This new error branch emits before the seenFiles dedup, while the success-read branch sits behind it, so repeated @-mentions of the same path emit one error block per mention. atCommandProcessor pushes one path entry per @-mention with no dedup (the ACP session dedups via a Set), so duplicate mentions flow through to readManyFiles. Before this commit, duplicate mentions on inode-0 volumes matched vacuously and were read once with seenFiles absorbing the duplicate — the duplication was invisible; the new branch emits per iteration.

Concrete trigger: submitting @notes.txt @notes.txt in the interactive TUI on a FAT/exFAT/SMB workspace (every stat reports ino: 0 — the exact filesystem class this PR targets) makes both loop iterations hit this guard, so contentParts receives two identical "Validated file identity is unavailable" blocks and files gets two entries, rendering two identical failed-read tool cards.

Verified by probe on the PR source: paths: ['dup.txt', 'dup.txt'] with identity { dev, ino: 0 } produces {"errorBlocksInContent":2,"filesLength":2,"filesWithError":2}; gating the emission on seenFiles in a scratch tree yields {"errorBlocksInContent":1,"filesLength":1} with all 53 tests still green.

Suggested change
contentParts.push(...errorParts);
files.push(info);
if (!seenFiles.has(fullPath)) {
seenFiles.add(fullPath);
contentParts.push(...errorParts);
files.push(info);
}
中文说明

[建议] 这个新的错误分支在 seenFiles 去重之前就输出结果,而成功读取分支在去重之后,因此对同一路径的重复 @ 提及会为每次提及输出一个错误块。atCommandProcessor 按每个 @ 提及推入一个路径条目且不去重(ACP session 用 Set 去重),所以重复提及会直接进入 readManyFiles。在此提交之前,inode-0 卷上的重复提及会空洞匹配并被读取一次,由 seenFiles 吸收重复——重复是不可见的;新分支则按迭代次数输出。

具体触发:在 FAT/exFAT/SMB 工作区(每次 stat 都报告 ino: 0——正是本 PR 针对的文件系统类别)的交互式 TUI 中提交 @notes.txt @notes.txt,两次循环迭代都会命中这个守卫,contentParts 会收到两个相同的 "Validated file identity is unavailable" 块,files 得到两个条目,渲染出两张相同的读取失败工具卡片。

已通过探针在 PR 源码上验证:paths: ['dup.txt', 'dup.txt'] 且身份为 { dev, ino: 0 } 时产生 {"errorBlocksInContent":2,"filesLength":2,"filesWithError":2};在临时树中用 seenFiles 门控输出后得到 {"errorBlocksInContent":1,"filesLength":1},53 个测试全部仍然通过。

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

Comment on lines +491 to +494
function createFileReadErrorResult(
displayPath: string,
errorMessage: string,
): { contentParts: Part[]; info: FileReadInfo } {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The new helper duplicates byte-for-byte the error-result block already constructed inline in this same file (~lines 236-247, the readValidatedTextFileContent catch block): same \nContent from ${displayPath}:\n prefix part, same Error reading ${displayPath}: ${errorMessage} text, same FileReadInfo with isDirectory: false and error. The cost is future drift: a change to the failed-read shape (rewording the prefix, adding a FileReadInfo field for error entries) reaches this helper — which the new regression test pins — but not the inline catch block, so the "identity unverifiable" error and the "read threw" error then render differently for the same class of failure, invisibly to tests that exercise only one path (atCommandProcessor's file.error tool-card rendering would diverge between the two paths).

Route the catch block through the helper too (keeping the abort rethrow and the errorMessage binding above it):

readResult = createFileReadErrorResult(displayPath, errorMessage);
中文说明

[建议] 新 helper 与同一文件中已有的内联错误结果构造(约 236-247 行,readValidatedTextFileContent 的 catch 块)逐字节重复:相同的 \nContent from ${displayPath}:\n 前缀部分、相同的 Error reading ${displayPath}: ${errorMessage} 文本、相同的带 isDirectory: falseerrorFileReadInfo。代价是未来的漂移:对读取失败形状的修改(改写前缀、为错误条目新增 FileReadInfo 字段)会到达这个被新回归测试钉住的 helper,但到不了内联 catch 块,于是 "identity unverifiable" 错误和 "read threw" 错误会对同一类失败呈现不同的渲染,而只覆盖其中一条路径的测试无法发现(atCommandProcessorfile.error 工具卡片渲染会在两条路径之间出现分歧)。

建议把 catch 块也改为调用该 helper(保留前面的 abort 重新抛出和 errorMessage 绑定):

readResult = createFileReadErrorResult(displayPath, errorMessage);

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

zhangyu.34 added 2 commits August 24, 2026 17:43
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com
@AaronZ345

Copy link
Copy Markdown
Contributor Author

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

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.

Partially reviewed — gaps disclosed. Suggestions are inline.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) is merge_group-only (never runs at the PR stage) and its E2E suite did not run locally.

Not explored to full depth (tool budget reached): "agent 5": executing packages/core/src/tools/readManyFiles.test.ts to confirm the new tests are green — the review worktree has no node_modules or built dist/ , and a….

中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) is merge_group-only (never runs at the PR stage) and its E2E suite did not run locally。

未探索到全部深度(达到工具调用预算):"agent 5"executing packages/core/src/tools/readManyFiles.test.ts to confirm the new tests are green — the review worktree has no node_modules or built dist/ , and a…

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

Comment on lines +240 to +242
it('deduplicates unverifiable validated inode errors for repeated paths', async () => {
const { relativePath, absolutePath } = await createTestFile(
'zero-inode-duplicate.txt',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The two new zero-inode tests paste the same ~28-line fs.stat/fs.open mocking block verbatim — this test's setup is byte-identical to the one in 'surfaces an error when validated inode identity is unverifiable' (lines ~191-214).

The cost is maintenance drift, and it is measurable today: these spies target exactly the object production calls (fs.promises), but they are never invoked, because the new early guard in readManyFiles.ts fires on the map's ino: 0 before any fs access. So one copy can silently lose its precondition with nothing noticing — deleting this test's mocking block in a scratch tree left the whole file green (52/52), including this test's toHaveLength(1) dedup assertion. Any future edit to the zero-inode precondition (e.g. also mocking lstat, handling bigint stats) must now be made in two places; if one copy rots, the test blesses the very regression it was written to catch.

Extract a shared helper next to createTestFile and call it from both tests, e.g.:

function mockZeroInodeForPath(absolutePath: string): { restore(): void } {
  const statSpy = vi.spyOn(fs, 'stat');
  const openSpy = vi.spyOn(fs, 'open');
  // move the shared mockImplementation blocks here
  return {
    restore: () => {
      statSpy.mockRestore();
      openSpy.mockRestore();
    },
  };
}

(checked in a scratch tree: with the helper extracted, all 52 tests still pass)

中文说明

两个新的 zero-inode 测试逐字粘贴了同一段约 28 行的 fs.stat/fs.open mock 代码块——本测试的 setup 与 'surfaces an error when validated inode identity is unverifiable'(约 191-214 行)中的完全相同。

代价是维护上的漂移,而且现在就可以度量:这些 spy 恰好挂在生产代码调用的对象(fs.promises)上,但从未被调用,因为 readManyFiles.ts 里新的早期守卫会在任何 fs 访问之前,基于 map 中的 ino: 0 提前触发。因此其中一个副本即使悄悄丢失前置条件,也不会有任何测试察觉——在临时树中删掉本测试的 mock 代码块后,整个文件仍然 52/52 全绿,包括本测试的 toHaveLength(1) 去重断言。将来任何对 zero-inode 前置条件的修改(例如同时 mock lstat、处理 bigint stats)都必须在两处同步;若其中一个副本腐化,测试反而会放行它本应捕获的回归。

createTestFile 旁提取一个共享 helper 并让两个测试都调用它(示例见上方英文代码块)。已在临时树中验证:提取 helper 后 52 个测试仍全部通过。

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

Consolidate duplicated zero-inode test setup so validated read diagnostics stay covered without copy-pasted filesystem spies.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

@qwen-code-ci-bot qwen-code-ci-bot left a comment

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.

No issues found. LGTM! ✅

Not explored to full depth (tool budget reached): "agent 2": executing readManyFiles.test.ts at the reviewed commit — the worktree has no node_modules and npm ci + monorepo build to satisfy vitest's globalSetup prereq….

中文说明

未发现问题。LGTM!✅

未探索到全部深度(达到工具调用预算):"agent 2"executing readManyFiles.test.ts at the reviewed commit — the worktree has no node_modules and npm ci + monorepo build to satisfy vitest's globalSetup prereq…

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

@wenshao

wenshao commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Local maintainer verification round — merge-ready

153/153 scripted assertions passed, 0 failed. Verified head 43108655b465a8926e971913d05c0e44909396fb against base 6a21c436d66f2cdaf45919048dce073e2b4f399d (both resolved from PR metadata, each in its own worktree with its own npm ci + packages/core build).

中文摘要

结论:建议合并(merge-ready)。153/153 条脚本化断言全部通过。

  • A/B 主张已证明:在模拟的 inode=0 文件系统上(node:fs/promises 边界拦截,忠实复现 Windows: validated @-file reads lose O_NOFOLLOW and may have vacuous dev/ino identity checks (follow-up to #7206) #8227 的身份形状),base 构建的 validated read 因 0===0 空洞匹配放行文件内容;head 构建则排除内容并返回可见错误 "Validated file identity is unavailable on this filesystem (inode is 0).",重复路径去重为恰好一条错误。
  • 零附带损伤:正常 APFS 读取、身份不匹配丢弃、stats 侧 inode=0、真实 EACCES 读错误四个对照细胞输出与 base 字节级一致(含错误结果构造重构的等价性)。
  • 测试非空洞:突变矩阵证明两个新测试各自钉住 pre-check 与去重;阳性对照使 3 个既有测试变红。
  • 门禁:readManyFiles 套件 head 52/52(base 50/50,净增 2、双侧零失败);core typecheck 通过;改动文件 Prettier 全过(含活性探针)。
  • 完整性记录(非阻塞)statsMatchValidatedIdentityhasVerifiableInode(stats.ino) 子句删除后 52/52 仍全绿——纵深防御,非缺陷。
  • 未覆盖:无真实 FAT/exFAT/SMB 卷(hdiutil 被沙箱拒绝),零 inode 为边界模拟;Windows 原生路径未测;Windows: validated @-file reads lose O_NOFOLLOW and may have vacuous dev/ino identity checks (follow-up to #7206) #8227 的 O_NOFOLLOW-on-Windows 半边不在本 PR 范围。

Central claim + A/B

Validated @-file reads must fail closed when the approved or reopened identity carries an unverifiable inode (0), instead of a vacuous 0===0 match (issue #8227). The harness drives each tree's compiled packages/core dist through the real producer order from atCommandProcessor.ts (realpathstat{dev, ino} capture) and the real readManyFiles. The zero-inode filesystem is simulated at the node:fs/promises boundary — it reproduces the identity shape the issue reports, not the filesystem itself (no FAT volume could be attached in this session).

Cell base (6a21c43) head (4310865)
zero — identity {dev, ino:0}, read-time stat also 0 content included (vacuous match), no error content excluded, visible error Validated file identity is unavailable on this filesystem (inode is 0).
dup — same path twice marker ×1, no error error ×exactly 1, content excluded
normal — real APFS identity content ✓ byte-identical to base
mismatch — file swapped before read silent drop byte-identical
statszero — expected ≠ 0, current stat 0 silent drop byte-identical
openerror — chmod 000 before read EACCES surfaced byte-identical (refactor proven behavior-preserving)

Cell totals: base 19/19, head 25/25 (head includes 4 cross-tree byte-identity assertions). The failure is not silent on the TUI path: files[].error maps to ToolCallStatus.Error with a Failed to read … display.

A/B: base vacuous match vs head fail-closed

Mutation matrix (vacuity + positive control)

Mutation Result Verdict
M1 — delete the zero-inode pre-check 50/52, the 2 new tests red pinned — both load-bearing
M2 — fine: delete only seenFiles.add in the pre-check 51/52, dedup test red pinned precisely
M3 — positive control: drop stats.ino === expected.ino 49/52, 3 drift tests red pinned — suite can go red
M4 — drop hasVerifiableInode(stats.ino) 52/52 green survivor → defense-in-depth, not load-bearing

Mutation matrix

Reviewer Test Plan + gates

  1. read-many-files tests: head 52/52 (base 50/50 — delta +2, zero failures either side).
  2. Core typecheck (tsc --noEmit): exit 0.
  3. Prettier on the 5 touched files: all pass; gate proven live by a planted violation that was flagged and removed.

Findings

No blocking findings. Completeness notes: (1) the hasVerifiableInode(stats.ino) clause inside statsMatchValidatedIdentity cannot change any observable outcome today (all callers sit behind the expected-side pre-check) — M4 survivor, reasonable defense-in-depth, keep-or-drop is a style call; (2) other dev:ino consumers in the codebase (sessionArtifacts.ts, conversation-runtime-ownership.ts, discovery.ts, same-file.ts, settings-cache.ts) are change-detection, not approval gates, and remain vacuous-on-zero-inode by design — out of scope here, listed to avoid over-reading this PR's coverage.

Not covered

No real FAT/exFAT/SMB volume (boundary simulation only); Windows native behavior incl. the O_NOFOLLOW half of #8227 (declared follow-up); other dev:ino consumers read but not driven; ACP Session.ts producer verified by code read (same accepting end as the driven path); repo-wide suites not run.

Full report, harnesses, and raw logs: tmp/pr9857-verify-20260825-0545/ (report.md, verdict.txt, assertions.json, harnesses/, logs/, evidence/).

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.22.2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants