fix(cli): reject symlinked screenshot paths on win32 in capture_screen_context - #9847
Conversation
…n_context Windows silently ignores O_NOFOLLOW, so the symlink guard in readPrivatePng was a no-op on win32: a Host returning a symbolic link could have its target read as a screenshot, bypassing the private-directory containment check. Probe the path with lstat and reject symlinks (including NTFS junctions) before opening; POSIX keeps O_NOFOLLOW as a TOCTOU backstop. Before: 'rejects a symlink and deletes only the Host-provided link' failed on Windows (tool read through the link). After: rejected with an explicit error; only the link is removed and the target stays intact. Part of QwenLM#9481 (cluster 3).
|
Thanks for the PR! Re-run at the current head — gate findings refreshed below.
Moving on to code review. 🔍 中文说明感谢贡献!以下是在当前 head 上刷新的门禁结论。
进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewIndependent baseline first: for "the symlink guard is a no-op on win32 because Windows ignores Findings:
Since the previous pass reviewed Testing evidence — the PR's own CI, read via API (no PR code executed here)All checks on this head are completed and nothing is red. The ubuntu unit lane — the one that runs the new cross-platform test — is green; the earlier Two structural facts bound what this CI can prove:
A sponsored Sandboxed verification is the lane that would settle the remainder: CI results for
中文说明代码审查:先给独立基线——对"符号链接守卫因 Windows 忽略 结论:无阻断项。守卫位于 与上一次审查的 测试证据(通过 API 读取 PR 自身 CI,未执行任何 PR 代码):该 head 上所有检查已完成且无红。ubuntu 单元 lane(运行新跨平台用例的那条)为绿;此前提到的 — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — a clean, minimal fix for an observed, maintainer-documented failure; the standing reservation is infrastructure-shaped, not code-shaped: no lane this PR can trigger ever executes on Windows, so the win32 behaviour itself rests on the author's local evidence plus platform contract. Stepping back: the approach is exactly the one I'd have written — The honest reservation, kept on the record: since #9370 merged, the Windows and macOS lanes trigger only via Six months from now: six lines, one comment, one test, no new abstractions — thank them, not curse them. CI: every 中文说明置信度:4/5 —— 对一个已被维护者记录在案的观测到的失败,这是干净、最小的修复;唯一的保留意见属于基础设施层面而非代码层面:本 PR 能触发的所有 lane 都不会在 Windows 上执行,因此 win32 行为本身依托作者的本地证据与平台契约。 退一步看:方案与我会写的完全一致—— 如实保留的意见:#9370 合并后,Windows 与 macOS lane 仅由 六个月后维护这段代码:六行、一条注释、一个用例、没有新抽象——会感谢作者。 CI:该 head 上所有 — Qwen Code · qwen3.8-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally.
Not reviewed: reverse audit — stopped before round 3 by the review time budget.
Test Plan (not a blocker): src/acp-integration/live/capture-screen-context.test.ts — no such file or directory; 4 tests pass — this review observed 23597 passed; Tests 4 passed — this review observed 23597 passed; 3 passed — this review observed 23597 passed.
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally。
未审查:反向审计——评审时间预算不足,未能开始第 3 轮。
Test Plan(非阻断):src/acp-integration/live/capture-screen-context.test.ts — no such file or directory; 4 tests pass — this review observed 23597 passed; Tests 4 passed — this review observed 23597 passed; 3 passed — this review observed 23597 passed。
— qwen3.8-max via Qwen Code /review (v0.22.0)
| // Windows silently ignores O_NOFOLLOW, so a symlinked screenshot path | ||
| // would be followed and read on win32. Probe the link itself first. | ||
| if ((await lstat(path)).isSymbolicLink()) { | ||
| throw new Error('Host returned a symbolic link screenshot path.'); |
There was a problem hiding this comment.
[Suggestion] The new win32 symlink guard is not gated by any test: the pre-existing symlink test asserts only expect(result.error?.message).toBeTruthy(), and on the Linux PR-gate runners open(..., O_NOFOLLOW) throws ELOOP for a symlinked path anyway, so deleting the four added probe lines keeps the whole suite green. A future change that removes or breaks the win32 probe would pass PR CI silently and surface only in the post-approval Windows merge-queue job, re-opening the issue 9481 failure cluster this PR fixes.
Probe evidence (Linux scratch tree, this review):
- probe deleted → symlink test still green via the ELOOP fallback (
Test Files 3 passed (3)),error='ELOOP: too many symbolic links encountered', truthy assertion passes - strengthened assertion → red on the mutant:
expected 'ELOOP: too many symbolic links encoun…' to be 'Host returned a symbolic link screens…'
Strengthen the assertion in capture-screen-context.test.ts so removing the probe fails on every platform:
expect(result.error?.message).toBe(
'Host returned a symbolic link screenshot path.',
);中文说明
新的 win32 符号链接守卫没有任何测试把关:现有的符号链接测试只断言 expect(result.error?.message).toBeTruthy(),而在 Linux PR 门禁的 runner 上,无论是否有新增的探测代码,open(..., O_NOFOLLOW) 都会对符号链接路径抛出 ELOOP,因此删除新增的 4 行探测代码后整个测试套件依然是绿的。未来若有改动移除或破坏了 win32 探测,PR CI 会静默放行,只有批准后的 Windows 合并队列任务才会发现,等于重新打开本 PR 修复的 issue 9481 失败簇。
探测证据(Linux 草稿树,本次审查):
- 删除探测代码 → 符号链接测试经 ELOOP 兜底仍然通过(
Test Files 3 passed (3)),truthy 断言通过; - 加强断言后 → 突变体变红:
expected 'ELOOP: too many symbolic links encoun…' to be 'Host returned a symbolic link screens…'
在 capture-screen-context.test.ts 中加强断言,使删除探测代码在所有平台上都会让测试失败:
expect(result.error?.message).toBe(
'Host returned a symbolic link screenshot path.',
);— qwen3.8-max via Qwen Code /review (v0.22.0)
…context The lstat guard added for win32 had no test that fails when it is removed: on Windows dropping the guard makes the tool read through the link, but on POSIX CI O_NOFOLLOW still rejects the link with a generic ELOOP error, so the regression would be invisible there. Assert the exact 'Host returned a symbolic link screenshot path.' message so both removal paths turn the suite red. Addresses review finding R1-1 (Suggestion).
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally.
Test Plan (not a blocker): src/acp-integration/live/capture-screen-context.test.ts — no such file or directory.
中文说明
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally。
Test Plan(非阻断):src/acp-integration/live/capture-screen-context.test.ts — no such file or directory。
— qwen3.8-max via Qwen Code /review (v0.22.0)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): "agent 1a": did not execute capture-screen-context.test.ts — the review worktree has no node_modules and no built workspace dist/ packages, so running the spec would ….
Test Plan (not a blocker): src/acp-integration/live/capture-screen-context.test.ts — no such file or directory; 4 tests pass — this review observed 23601 passed; Tests 4 passed — this review observed 23601 passed; 3 passed — this review observed 23601 passed.
Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:
packages/cli/src/acp-integration/live/capture-screen-context.test.ts:96 — [review] New symlink test duplicates the adjacent test's setup; assertions can fold into one
中文说明
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):"agent 1a":did not execute capture-screen-context.test.ts — the review worktree has no node_modules and no built workspace dist/ packages, so running the spec would …。
Test Plan(非阻断):src/acp-integration/live/capture-screen-context.test.ts — no such file or directory; 4 tests pass — this review observed 23601 passed; Tests 4 passed — this review observed 23601 passed; 3 passed — this review observed 23601 passed。
收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.0)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally.
Test Plan (not a blocker): src/acp-integration/live/capture-screen-context.test.ts — no such file or directory.
Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:
packages/cli/src/acp-integration/live/capture-screen-context.test.ts:96 — [review] New symlink test duplicates the adjacent test's setup; assertions can fold into one
中文说明
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally。
Test Plan(非阻断):src/acp-integration/live/capture-screen-context.test.ts — no such file or directory。
收敛姿态下延后(第 4 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.0)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally.
Test Plan (not a blocker): src/acp-integration/live/capture-screen-context.test.ts — no such file or directory; 4 tests pass — this review observed 24761 passed; Tests 4 passed — this review observed 24761 passed; 3 passed — this review observed 24761 passed.
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally。
Test Plan(非阻断):src/acp-integration/live/capture-screen-context.test.ts — no such file or directory; 4 tests pass — this review observed 24761 passed; Tests 4 passed — this review observed 24761 passed; 3 passed — this review observed 24761 passed。
— qwen3.8-max via Qwen Code /review (v0.22.0)
| it('reports the dedicated symlink error on every platform', async () => { | ||
| const target = await captureFile(); | ||
| const link = join(target.directory, 'linked.png'); |
There was a problem hiding this comment.
[Suggestion] The sibling test above — rejects a symlink and deletes only the Host-provided link — is still gated it.skipIf(process.platform === 'win32') with the comment "The rejection relies on O_NOFOLLOW, which libuv ignores on win32". That rationale no longer holds: rejection now goes through the platform-agnostic lstat guard this PR adds, and this new test beside it runs the identical setup on every platform. The skip entered this branch through the merge with main (#9728, which silenced the then-failing test); with the guard in place the sibling test passes on win32 as written.
This matters because the sibling test is the only test asserting the cleanup semantics — the symlink's target survives and only the Host-provided link is unlinked. While it stays win32-skipped, nothing verifies those assertions on the very platform this PR hardens: a future change that regresses the finally-block unlink on win32 (resolving/realpath-ing the path before unlink and deleting the target instead of the link, or failing to remove the link) ships green on the Windows lane. The stale comment also invites a future maintainer to conclude win32 symlink rejection is untestable and re-introduce a skip or remove the guard.
Suggested fix: drop .skipIf(process.platform === 'win32') from the sibling test and rewrite its comment — e.g. "Rejection goes through the explicit lstat guard on every platform; O_NOFOLLOW is the POSIX TOCTOU backstop." Symlink creation is smoke-validated on this repo's self-hosted Windows pool (windows-runner-smoke.yml, "Verify symbolic links" step), and this PR's own new test already calls symlink() ungated. Alternatively, fold the sibling's two filesystem assertions into this test and delete the duplicate.
中文说明
上方相邻的用例 rejects a symlink and deletes only the Host-provided link 仍然被 it.skipIf(process.platform === 'win32') 跳过,且注释写着 "The rejection relies on O_NOFOLLOW, which libuv ignores on win32"。这个理由已经不再成立:拒绝逻辑现在走的是本 PR 新增的、与平台无关的 lstat 守卫,而旁边这个新用例已经在所有平台上运行完全相同的准备步骤。这个跳过是经由与 main 的合并(#9728,当时为了 silenced 失败的测试)进入本分支的;在守卫就位后,相邻用例在 win32 上按原样即可通过。
这一点很重要,因为相邻用例是唯一断言清理语义的测试——符号链接的目标文件保持完好、只有 Host 提供的链接本身被删除。只要它在 win32 上仍被跳过,在本 PR 所加固的这个平台上就没有任何测试验证这些断言:未来某个改动若在 win32 上破坏了 finally 块中的 unlink(例如先 resolve/realpath 再删除、从而删掉了目标文件而不是链接,或根本没有删掉链接),会在 Windows 流水线上绿灯通过。过时的注释还会误导后来的维护者,使其以为 win32 上无法测试符号链接拒绝,从而重新引入跳过或删除守卫。
建议修复:去掉相邻用例上的 .skipIf(process.platform === 'win32'),并改写其注释——例如 "Rejection goes through the explicit lstat guard on every platform; O_NOFOLLOW is the POSIX TOCTOU backstop."。本仓库自托管 Windows 资源池已通过冒烟流程验证可以创建符号链接(windows-runner-smoke.yml 的 "Verify symbolic links" 步骤),且本 PR 自己的新用例已经在不加门控的情况下调用 symlink()。也可以把相邻用例的两个文件系统断言合并进本用例,然后删除重复的用例。
— qwen3.8-max via Qwen Code /review (v0.22.0)
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 37 passed · 0 failed · 37 total Flakiness gate: ✅ 1 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:37 通过 · 0 失败 · 37 总计 抖动门:✅ 1 changed test file(s) x 5 identical rounds, no divergence Verification reportVerification report — PR #9847Verdict: 中文摘要
Central claim and A/BCentral claim: Secondary claims: (1) the new exact-message test pins the guard on POSIX CI, where the old test alone would stay green; (2) non-symlink behavior (success path, containment, cleanup) is unchanged. Harness:
The C2 cell is the security-relevant shape: the textual containment check passes (the link is inside the private dir), so on a platform where Reviewer Test Plan walkthrough
Corrections
FindingsF1 (Suggestion, pre-existing — not introduced by this PR): a FIFO planted as the screenshot hangs the tool on every platform. F2 (Nit): stale skip comment on the old symlink test. F3 (Informational): residual win32 TOCTOU between F4 (Informational, author-declared out of scope): sibling Mutation / vacuity matrixWitness:
Both mutations landed in the mutated file's own suite and failed the intended assertion with expected-vs-actual values — the positive control for the harness is the M1/M2 red itself. No survivors: the PR introduces exactly one guard and one pinning assertion, and each kills its mutant. Targeted gates (all at head)
Not covered
MethodologyEnvironment: CI verify container, Flakiness gate logEvidence imagesHarness scripts and raw logs are in the workflow run artifacts (7-day retention). — Qwen Code · sandboxed verification |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
Local verification on a real build — maintainer reviewI rebuilt this PR merged into Setup
The emulation is faithful, not a guess: libuv defines 1. The bug is real, and this PR closes itThe Host hands back a symlink whose target lives outside its private directory. On the Windows lane, On stock macOS both versions reject; only the error text changes ( 2. The repo's own suite reproduces the author's Windows failureReverting only the production hunk (keeping the PR's test file) and running Context worth recording: on today's 3. Full scenario matrix — no behaviour regressions8 Host-supplied path shapes × 2 platform lanes. Everything that used to succeed still succeeds (real PNG, hard link); everything that used to be rejected is still rejected, including the containment check. Exactly two rows change: the win32 symlink read (fixed) and the ENOENT text ( 4. Provenance, lint, types, bundle, regression sweep
Notes for whoever merges this
Verdict: LGTM — recommend merge. No blocking findings. Items 3 and 4 are pre-existing and belong in their own issue under #9481 rather than as scope creep here. How to reproduce the Windows lane locally (macOS)// win32_open_emu.c — strip O_NOFOLLOW the way libuv does on win32
#include <fcntl.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
typedef struct interpose_s { void *new_func; void *orig_func; } interpose_t;
static int in_scope(const char *path) {
const char *prefix = getenv("EMU_PREFIX");
return prefix && path && strncmp(path, prefix, strlen(prefix)) == 0;
}
int emu_open(const char *path, int flags, ...) {
mode_t mode = 0;
if (flags & O_CREAT) {
va_list ap; va_start(ap, flags); mode = (mode_t)va_arg(ap, int); va_end(ap);
}
if (in_scope(path)) flags &= ~O_NOFOLLOW;
return open(path, flags, mode);
}
__attribute__((used)) static const interpose_t interposers[]
__attribute__((section("__DATA,__interpose"))) = {
{(void *)emu_open, (void *)open},
};clang -O2 -dynamiclib -o win32_open_emu.dylib win32_open_emu.c
# Smoke test: this must FAIL with ELOOP without the dylib and SUCCEED with it.
ln -s target.txt link.txt
node -e "fs=require('fs');fs.openSync('link.txt',fs.constants.O_RDONLY|fs.constants.O_NOFOLLOW)"
# Then run the suite on the emulated lane. Launch vitest through node directly:
# /bin/sh is SIP-protected and would strip DYLD_INSERT_LIBRARIES from the child.
cd packages/cli
EMU_PREFIX="$(node -p 'require("os").tmpdir()')" \
DYLD_INSERT_LIBRARIES=/abs/path/win32_open_emu.dylib \
node ../../node_modules/vitest/vitest.mjs run \
src/acp-integration/live/capture-screen-context.test.ts --reporter=verboseWith 中文说明本地真实环境验证 — 维护者复核我在一个独立 clone 里把本 PR 合入 环境
这个模拟不是猜的:libuv 在 win32 上把 1. 缺陷真实存在,本 PR 确实堵住了它(图 1)Host 返回一个指向私有目录之外的符号链接。在 Windows 泳道上, 在原生 macOS 上两个版本都拒绝,只是错误文案不同( 2. 仓库自带的测试复现了作者报告的 Windows 失败(图 2)只回退生产代码那一段(保留本 PR 的测试文件),在 Windows 泳道跑 一点值得记录的上下文:当前 3. 完整场景矩阵——无行为回归(图 3)8 种 Host 给出的路径形态 × 2 个平台泳道。原本成功的仍然成功(真实 PNG、硬链接);原本被拒的仍然被拒,包含目录约束检查。只有两行发生变化:win32 上的符号链接读取(已修复)与 ENOENT 文案( 4. 溯源、lint、类型、打包与回归扫描(图 4)两个改动文件 给合并者的几点说明
结论:LGTM,建议合并。 没有阻塞项;第 3、4 条属于既有问题,应在 #9481 下另开 issue,而不是在本 PR 里扩范围。 |







What this PR does
The
capture_screen_contexttool now rejects a screenshot path that is a symbolic link (including NTFS junctions) before opening it. The private-directory containment check stays unchanged; only the read itself is hardened. POSIX behavior is identical to before, withO_NOFOLLOWkept as a TOCTOU backstop.Why it's needed
readPrivatePngrelied onO_NOFOLLOWto refuse symlinked screenshot paths, but Windows silently ignores that flag. On win32 the guard was therefore a no-op: a Host returning a symbolic link could have its target read as a screenshot, bypassing the containment check this tool promises. This is part of #9481 (cluster 3, the CaptureScreenContextTool symlink-rejection failure).Reviewer Test Plan
How to verify
Run
npx vitest run src/acp-integration/live/capture-screen-context.test.tsinsidepackages/cli. On Windows, before this change, the testrejects a symlink and deletes only the Host-provided linkfailed withexpected undefined to be truthyat line 91 — meaning the tool read through the link and returned success instead of rejecting it. After this change all 4 tests pass: the tool fails with an explicit error, only the link file is removed in cleanup, and the original PNG target stays intact.Evidence (Before & After)
Before (Windows 11, Node v22.14.0):
Tests 1 failed | 3 passed (4)withAssertionError: expected undefined to be truthyatcapture-screen-context.test.ts:91. After the same command reports✓ src/acp-integration/live/capture-screen-context.test.ts (4 tests)andTests 4 passed (4). Non-UI change, so no screenshots are attached.Tested on
Environment (optional)
Local
npx vitest runon Windows 11 with Node v22.14.0; Windows Developer Mode enabled so test fixtures can create real symlinks.Note on the failing check (
Test (ubuntu-latest, Node 22.x))The single red check on this commit is an infrastructure-side failure, not a code regression:
Test Files 65 passed (65)/Tests 1713 passed | 11 skipped (1724)— no assertion failed anywhere in the run.✓ src/acp-integration/live/capture-screen-context.test.ts (5 tests) 60ms.Error: [vitest-worker]: Timeout calling "onTaskUpdate", with the entire call stack insidenode_modules/vitest/dist/chunks/rpc.*.js(worker→parent RPC timeout), matching the known flaky pattern previously worked on in fix(tests): avoid blocking the Vitest worker during directory E2E #8685.A re-run of this job should clear it; no code change is needed or planned for it in this PR.
Risk & Scope
lstatper capture — negligible next to the file read it guards.O_NOFOLLOWcall sites elsewhere in the repository were not audited in this PR.Linked Issues
Part of #9481 (referenced without a closing keyword on purpose — the remaining clusters of that issue are being fixed separately).
中文说明
这个 PR 做了什么
capture_screen_context工具在打开截图路径之前,现在会先拒绝符号链接路径(包括 NTFS junction)。私有目录约束检查保持不变,仅加固读取这一步;POSIX 行为与之前完全一致,并保留O_NOFOLLOW作为 TOCTOU 兜底。为什么需要这个改动
readPrivatePng原本依赖O_NOFOLLOW来拒绝符号链接截图路径,但 Windows 会静默忽略该标志。因此在 win32 上这道防线形同虚设:当 Host 返回一个符号链接时,其目标文件会被当作截图读进来,绕过了该工具承诺的目录约束。本 PR 属于 #9481 的簇 3(CaptureScreenContextTool 的符号链接拒绝失败项)。审阅者测试计划
如何验证
在
packages/cli内运行npx vitest run src/acp-integration/live/capture-screen-context.test.ts。在 Windows 上、修改之前,用例rejects a symlink and deletes only the Host-provided link在第 91 行报expected undefined to be truthy——即工具读穿了链接并返回成功,而不是拒绝。修改后 4 条测试全部通过:工具以明确的错误拒绝请求,清理阶段只删除链接文件本身,原始 PNG 目标保持完好。证据(前后对比)
修改前(Windows 11,Node v22.14.0):
Tests 1 failed | 3 passed (4),错误为AssertionError: expected undefined to be truthy,位于capture-screen-context.test.ts:91。修改后同一命令报告✓ src/acp-integration/live/capture-screen-context.test.ts (4 tests)与Tests 4 passed (4)。本改动非 UI 变更,故未附截图。测试平台
macOS 未测;Windows 已测;Linux 未测(CI 会覆盖)。
环境(可选)
Windows 11 本地
npx vitest run,Node v22.14.0;已开启 Windows 开发者模式以便测试夹具创建真实符号链接。风险与范围
lstat,相对于它保护的文件读取可忽略不计。O_NOFOLLOW调用点不在本 PR 审查范围内。关联 issue
Part of #9481(有意不使用关闭关键词——该 issue 其余簇由其他改动分别修复)。