Skip to content

fix(core): track attached stdout fd redirects - #5317

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/shell-attached-stdout-redirects
Jun 18, 2026
Merged

wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/shell-attached-stdout-redirects

Conversation

@tt-a1i

@tt-a1i tt-a1i commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Fixes #5316

Summary

  • Track attached stdout fd redirects like and as write operations
  • Keep attached absolute fd redirects from being marked cwd-dependent after dynamic
  • Add regression coverage for both overwrite and append forms

Testing

  • npx vitest run src/permissions/shell-semantics.test.ts -t "combined stdout fd|does not mark absolute writes"
  • npx vitest run src/permissions/shell-semantics.test.ts
  • npm run typecheck --workspace=packages/core
  • npm run build --workspace=packages/core
  • npm run lint
  • npx prettier --experimental-cli --check packages/core/src/permissions/shell-semantics.ts packages/core/src/permissions/shell-semantics.test.ts
  • git diff --check

AI Assistance Disclosure

I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.

@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Independently verified this one — it's a real permission-gate bypass and the fix is correct. 👍

Repro (by construction): the tokenizer splits only on unquoted whitespace, so echo hi 1>.qwen/settings.json tokenizes to ['echo', 'hi', '1>.qwen/settings.json'] — the 1>… stays a single fused token. It isn't === any spaced operator, so it falls to the combined-form branch, where main's regex /^(<<-?|>>|>|2>>|2>|&>>|&>|<)(.+)$/ matches nothing that starts with 1 → the redirect is dropped → the write to .qwen/settings.json never becomes a write_file op and skips the permission check. The spaced form 1> file was already caught (tok === '1>'), so this closes exactly that no-space asymmetry. The added 1>>|1> (longest-first ordering is right) routes op !== '<' to writeFiles, no other change needed. ✔

Optional follow-up (non-blocking, low priority): the regex still ignores arbitrary fds, so echo x 3>.qwen/settings.json truncates the file undetected. But fd ≥ 3 only truncates without writing content and is never typed by accident, so 1> was the case worth fixing — a general \d+> handler can be a separate cleanup if you think it's worth it.

LGTM to merge once you flip it out of draft. Thanks for the catch!

中文说明

我独立复现验证过了 —— 这是个真实的权限闸门绕过,修复正确。👍

复现(从构造上证明):tokenizer 只按非引号空白切词,所以 echo hi 1>.qwen/settings.json 切成 ['echo', 'hi', '1>.qwen/settings.json'] —— 1>… 是一个融合 token。它不 === 任何带空格的算子,于是落到无空格的合并分支;而 main 的正则 /^(<<-?|>>|>|2>>|2>|&>>|&>|<)(.+)$/ 没有任何分支以 1 开头 → 重定向被丢弃 → 对 .qwen/settings.json 的写入不会变成 write_file 操作、绕过权限检查。带空格的 1> file 早已被 tok === '1>' 捕获,所以这个 PR 补的正是无空格的不对称缺口。新增的 1>>|1>(最长优先的顺序正确)让 op !== '<' 进入 writeFiles,无需改动其他地方。✔

可选后续(非阻塞、低优先级):正则仍然忽略任意 fd,所以 echo x 3>.qwen/settings.json 会在不被检测的情况下截断文件。但 fd ≥ 3 只会截断而不写入内容、也几乎不会被手动输入,所以 1> 才是值得修的那个 —— 通用的 \d+> 处理可以另开一个清理 PR,看你觉得是否值得。

翻出 draft 后即可合并,LGTM。感谢发现这个问题!

@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao
wenshao merged commit 7595723 into QwenLM:main Jun 18, 2026
33 checks passed
@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — local build + before/after semantics harness

Verified PR head 22bf5c4 against origin/main (f5761ac) on macOS (Darwin arm64, Node v22.22.2). This touches the shell permission-analysis boundary (extractShellOperations feeds the write/read-permission gate), so beyond the unit suite I ran a before/after harness driving the real extractShellOperations / extractShellOperationsAcrossCommand over an adversarial command corpus.

1. Build / tests / typecheck / lint (PR code)

Check Result
shell-semantics.test.ts ✅ 103 passed (4 new + 99 existing)
tsc --noEmit (core) ✅ exit 0, fully clean
npm run build (core) ✅ OK
eslint + prettier --check (changed files) ✅ clean
PR CI ✅ green on macOS / Windows / Linux

2. Before/after behavioral harness

✅ Intended fix — attached stdout-fd redirects are now tracked as writes (were silently missed before):

Command before after
echo hi 1>.qwen/settings.json [] (no op) write_file /repo/.qwen/settings.json
echo hi 1>>.qwen/settings.json [] write_file …/.qwen/settings.json
echo hi 1>/etc/passwd [] write_file /etc/passwd
cd "$X" && echo hi 1>/tmp/out.txt [] write_file /tmp/out.txt · pathMayDependOnCwd:false (correctly absolute)
cd "$X" && echo hi 1>rel.txt [] write_file /repo/rel.txt · pathMayDependOnCwd:true (correctly cwd-relative)

✅ No regressions — unchanged before↔after: spaced 1> file / 1>> file, attached >file / >>file, 2>file, 2>/dev/null (ignored), &>file, < file, plain echo hi.

3. One finding (non-blocking): fd-dup over-detection is extended to 1>&

The fix also makes fd-duplications on fd 1 match the redirect regex, and they fall through to a spurious write op:

Command before after
echo hi 1>&2 [] write_file /repo/&2 ⚠️
echo hi 1>&- [] write_file /repo/&- ⚠️

This is pre-existing behavior, not introduced by this PR. Root cause is looksLikePath('&2') === true (it only rejects $-vars, flags, pure ints, braces, URLs — not &N fd-dups). The same quirk already exists on main for the other operators and is unchanged here:

  • echo hi 2>&1 → write_file /repo/&1 (same before & after)
  • echo hi >&2 → write_file /repo/&2 (same before & after)

So the PR extends an existing looksLikePath limitation from >& / 2>& to 1>&. The direction is safe for a permission gate — it's over-detection on a nonsensical path, never under-detection, so it can't let a real write slip past; worst case is a spurious permission consideration on a 1>&2 command.

Suggested follow-up (separate, optional): skip &-prefixed fd-dup targets (&1, &2, &-) in extractRedirects / looksLikePath so the new 1>& and the pre-existing 2>& / >& cases are all cleaned up together.

Verdict

The core fix is correct and closes a real under-detection gap — attached 1>file / 1>>file writes were invisible to the permission analyzer while the spaced forms were tracked, so a command could write to a sensitive path (e.g. 1>.qwen/settings.json) without the write being seen. Zero regressions; the absolute-vs-relative cwd-dependency handling is correct. The only side effect (fd-dup → spurious write) is pre-existing behavior extended in the safe direction. ✅ Safe to merge. The fd-dup over-detection is worth a small follow-up but is not a blocker.

Verified by maintainer @wenshao: local build + unit suite + a before/after harness running the real semantics functions from both origin/main and the PR head (22bf5c4) over a 20-command corpus.

中文版(点击展开)

维护者验证 —— 本地构建 + 前后对比语义 harness

在 macOS(Darwin arm64,Node v22.22.2)上,针对 origin/main(f5761ac)验证了 PR head 22bf5c4。这块改动落在 shell 权限分析边界上(extractShellOperations 给写/读权限门控供数),所以除单元测试外,我还跑了一个前后对比 harness——用真实的 extractShellOperations / extractShellOperationsAcrossCommand 过一份对抗性命令语料。

1. 构建 / 测试 / 类型检查 / lint(PR 代码)

检查 结果
shell-semantics.test.ts ✅ 103 通过(4 新 + 99 既有)
tsc --noEmit(core) ✅ exit 0,完全干净
npm run build(core) ✅ 通过
eslint + prettier --check(改动文件) ✅ 干净
PR CI ✅ macOS / Windows / Linux 全绿

2. 前后对比行为 harness

✅ 目标修复 —— 紧贴的 stdout-fd 重定向现在被识别为写操作(此前会被静默漏掉):

命令 before after
echo hi 1>.qwen/settings.json [](无 op) write_file /repo/.qwen/settings.json
echo hi 1>>.qwen/settings.json [] write_file …/.qwen/settings.json
echo hi 1>/etc/passwd [] write_file /etc/passwd
cd "$X" && echo hi 1>/tmp/out.txt [] write_file /tmp/out.txt · pathMayDependOnCwd:false(正确判为绝对路径)
cd "$X" && echo hi 1>rel.txt [] write_file /repo/rel.txt · pathMayDependOnCwd:true(正确判为相对 cwd)

✅ 无回归 —— before↔after 一致:带空格的 1> file / 1>> file、紧贴的 >file / >>file、2>file、2>/dev/null(忽略)、&>file、< file、纯 echo hi。

3. 一个发现(非阻塞):fd-dup 的过度识别被扩展到了 1>&

修复让 fd 1 上的 fd 复制也匹配了重定向正则,进而落到一个虚假的写 op:

命令 before after
echo hi 1>&2 [] write_file /repo/&2 ⚠️
echo hi 1>&- [] write_file /repo/&- ⚠️

这是既有行为,并非本 PR 引入。 根因是 looksLikePath('&2') === true(它只排除 $ 变量、flag、纯整数、花括号、URL,不排除 &N 这种 fd 复制)。同样的怪癖在 main 上对其他操作符早已存在,本 PR 未改变:

  • echo hi 2>&1 → write_file /repo/&1(前后一致)
  • echo hi >&2 → write_file /repo/&2(前后一致)

所以本 PR 只是把既有的 looksLikePath 局限从 >& / 2>& 扩展到了 1>&。方向对权限门控而言是安全的——属于过度识别(在一个无意义路径上多报一个写),绝不会漏报,因此不会让真实的写绕过;最坏情况只是对 1>&2 这类命令多做一次权限判断。

建议的后续(独立、可选): 在 extractRedirects / looksLikePath 里跳过 & 开头的 fd-dup 目标(&1、&2、&-),把新的 1>& 和既有的 2>& / >& 一起清理掉。

结论

核心修复正确,且堵上了一个真实的漏报缺口——紧贴的 1>file / 1>>file 写此前对权限分析器不可见,而带空格的形式却被识别,于是一条命令可以写入敏感路径(如 1>.qwen/settings.json)而该写操作不被看见。无回归;绝对 vs 相对的 cwd 依赖处理正确。唯一的副作用(fd-dup → 虚假写)是既有行为在安全方向上的扩展。✅ 可以合并。 fd-dup 过度识别值得一个小的后续修复,但不构成本 PR 的阻塞项。

维护者 @wenshao 验证:本地构建 + 单元测试 + 一个前后对比 harness(分别从 origin/main 与 PR head 22bf5c4 加载真实语义函数)跑 20 条命令的对抗性语料。

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.

bug(core): shell semantics miss attached stdout fd redirects

2 participants