Skip to content

fix(cosh-ng): block path traversal in readonly - #2708

Open
jfeng18 wants to merge 1 commit into
mainfrom
fix/cosh-ng/readonly-path-traversal
Open

fix(cosh-ng): block path traversal in readonly#2708
jfeng18 wants to merge 1 commit into
mainfrom
fix/cosh-ng/readonly-path-traversal

Conversation

@jfeng18

@jfeng18 jfeng18 commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Why

is_blocked_special_path matched /proc, /dev and /sys by raw prefix only, so spellings like /../proc/version bypassed the blocklist and were allowed through the readonly auto-execution chain — a real bypass of the readonly safety check.

What changed

  • Paths are normalized lexically (no filesystem access) before matching the special-path blocklist.
  • The raw-prefix pass is kept first so the check stays fail-closed for anything normalization cannot resolve.
  • $-quoting forms remain intercepted upstream by the exec layer; noted in a code comment.

Related issue

closes #2184

User / Agent impact

Commands whose path arguments lexically resolve into /proc, /dev or /sys — previously executed via the readonly chain — are now blocked. Legitimate paths are unaffected.

Risk and compatibility

  • Public CLI, API, configuration, or documented behavior changed
  • Privileged or security-sensitive behavior changed
  • Cross-component contract changed
  • Migration or rollback guidance is needed

Security tightening: the newly intercepted set is exactly the spellings that lexically resolve into /proc, /dev or /sys, all of which were genuine bypasses before the fix. An anti-false-positive matrix pins legitimate paths that must keep passing.

Validation

  • ECS discriminative two-direction test: bypass reproduces (allowed) on pre-fix code, blocked on fixed code.
  • ECS cargo test: 1343 passed (baseline 1340 + 3 new).
  • cargo fmt and clippy clean with zero warnings.

Documentation and rollback

Single commit — revert it directly to restore the previous matching behavior if ever needed.

@jfeng18
jfeng18 requested a review from SunnyQjm as a code owner August 20, 2026 08:09
@github-actions github-actions Bot added the component:cosh-ng src/cosh-ng label Aug 20, 2026

@qoderai qoderai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

本次审查范围内未发现需要修改的问题。


🤖 Generated by QoderView workflow run

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2f5d3c168e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/cosh-ng/crates/cosh-shell/src/tools/readonly_rules/evaluator.rs Outdated
@SunnyQjm

SunnyQjm commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

PR number: #2708
head_sha: 2f5d3c1
reviewed_at: 2026-08-20T08:36:11Z

Findings

  • [P1] 相对路径 .. 穿越仍然放行,且被新测试固化为预期行为evaluator.rs:422-445normalize_path_lexically 对相对路径保留前导 ..("cannot be resolved without the working directory"),于是 has_blocked_special_prefix 永远不命中相对拼写;tests.rs:469-471 显式断言 ../proc/versionis_safe_readonly_path。但 readonly 自动执行链是在真实 shell cwd 下执行的:当 cwd 是 / 的子目录(如 /tmp)时,cat ../proc/version 在 OS 层解析为 /proc/version,穿越绕过对本类拼写仍然敞开。本 PR 的修复目标(closes [cosh-ng] bug: is_blocked_special_path 纯字符串前缀匹配,路径穿越与 $'...' ANSI-C quoting 可绕过 readonly_rules 黑名单 #2184,阻断进入 /proc//dev//sys 的穿越)只覆盖了绝对路径拼写。建议:在执行点用真实 cwd 解析相对路径后再过 blocklist,或在无法解析时 fail-closed(退回人工审批),而不是放行并把不安全结果钉成测试合同。
  • [P2] 当前 head 的 CI 为失败状态,与 PR 描述的自证不符 — PR state 中 Test cosh-ngTest cosh-ng fast checks 在该 head_sha 上均为 FAILURE,而 PR body 声称 "1343 passed、fmt/clippy clean"。合并前必须让这两项转绿并确认失败原因与本次改动无关(或已修复)。

结构评审确认项(无 blocking 组织问题)

  • Owner 归属正确:改动落在 tools/readonly_rules/,readonly 路径策略属 tools/ owner(tool classification / readonly policy),符合 standard.md §2。
  • Public API 未扩大:不触碰 lib.rs,无新增 pub mod/pub use;新增 has_blocked_special_prefixnormalize_path_lexically 均为 private fnis_blocked_special_path 既有 pub 签名不变,无需更新 public-api-inventory。
  • 无 forbidden dependency / self-crate path:仅用 std 字符串操作,未引入 crate::agent/runtime/ui/...,无 cosh_shell::...
  • 范围收敛:单 owner、两个文件、单 commit,无跨 crate 配套改动;未新增 root src/*.rs
  • Fail-closed 设计正确:raw prefix 先行、/proc/../etc 类拼写保持阻断;anti-false-positive 矩阵覆盖 /home/../usr/bin/ls 等合法路径。
  • 测试位置:用例放入既有 tools/readonly_rules/tests.rs,与现状一致,测试组织细节归测试 SDD。

剩余风险

  • Symlink 别名与 ~ 展开不在 lexical normalization 覆盖范围,doc comment 已声明无 filesystem access 契约,属设计内取舍。
  • $ 引用/变量展开依赖上游 broker is_shell_meta 与 compound executor rule 5 拦截,本层不构成完整 sandbox,doc comment 已明确分工;后续改上游拦截逻辑时不得破坏该前提。

未跑验证(以 CI 实测为准)

  • 本评审不执行工具;evaluator.rs 新增约 49 行(diff 推算),该文件未登记在 large-file-inventory,是否触及 700 行阈值以 check-layout.sh 实测为准。
  • PR body 的测试结果与 fmt/clippy 结论未由本评审复核,且与当前 CI FAILURE 状态冲突,见 P2。

@jfeng18
jfeng18 force-pushed the fix/cosh-ng/readonly-path-traversal branch from 2f5d3c1 to 0ae466c Compare August 21, 2026 03:27
@jfeng18

jfeng18 commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

Both findings addressed in 0ae466c (single commit, force-pushed):

P1 (relative traversal) — fixed fail-closed and cwd-independently: after lexical normalization, a leading .. chain whose first non-.. component is dev/proc/sys is now blocked (relative_traversal_enters_special_dir in evaluator.rs). This layer has no cwd and no filesystem access by contract, so instead of resolving against the execution cwd it fails closed for every cwd; the accepted cost (an ordinary directory literally named proc/dev/sys reached via ..) is documented and such commands still go through interactive approval. The old src tests that pinned ../proc/version as safe were removed; the relocated tests now assert it blocked plus ../notes.txt/../../tmp/x allowed (relative_traversal_into_special_dirs_is_blocked, traversal_normalization_does_not_over_block).

P2 (CI vs body) — the fast-checks failure was the test-inventory overlap ratchet: the three new tests had landed in tools/readonly_rules/tests.rs, which is compiled into both the lib and bin test targets (main.rs re-declares lib modules), pushing the lib/bin overlap to 697 > 694 ceiling. They were moved to tools/readonly_rules/traversal_tests.rs, declared only from lib.rs via the existing wrap_tests pattern (is_safe_readonly_path is not publicly exported, so a tests/ integration file cannot reach it), restoring overlap = 694 = ceiling — the audit gate now passes locally. The Test cosh-ng failure is provider_lifecycle::cosh_core_sync_drains_child_output_while_writing_large_prompt timing out after 3s (71/72 pass; the same code is green on main; zero interaction with this diff) — will re-run it off-peak to confirm the environmental attribution.

@SunnyQjm

Copy link
Copy Markdown
Collaborator

PR number: #2708
head_sha: 0ae466c
reviewed_at: 2026-08-21T06:26:24Z

Findings

  • [P2] 当前 head 的 Test cosh-ng fast checks 仍为 FAILURE,与作者"ratchet 已修复"的自证冲突 — status checks 显示 Test cosh-ng fast checks0ae466c 上 FAILURE(04:26:16),而作者在 PR 级回复中称 overlap 已恢复 694=ceiling、audit gate 本地通过。Test cosh-ng 已转 SUCCESS(03:37:27),作者归因的环境性超时解释初步成立。合并前需让 fast checks 转绿,或给出失败日志证明与本改动无关(fast checks 通常正是 layout/test-inventory/fmt/clippy 门禁所在,恰是本 PR 声称修复的那类检查)。

结构评审确认项(无 blocking 组织问题)

未发现 blocking package/module/public API 组织问题:

  • Owner 归属正确:全部改动落在 tools/readonly_rules/,readonly 路径策略属 tools/ owner(tool classification / readonly policy),符合 standard.md §2。
  • Public API 未扩大lib.rs 新增的 mod readonly_traversal_tests(lib.rs:32-34)是 #[cfg(test)] private mount,非 pub mod/pub useis_blocked_special_path 既有 pub 签名不变,has_blocked_special_prefix/relative_traversal_enters_special_dir/normalize_path_lexically 均为 private fn,无需更新 public-api-inventory。为规避 lib/bin overlap ratchet 而把测试挂到 lib-only 位置,作者说明沿用既有 wrap_tests pattern,测试挂载细节归 shell-test-organization SDD。
  • 无 forbidden dependency / self-crate path:仅 std 字符串操作,无 crate::agent/runtime/ui/...,无 cosh_shell::...
  • 范围收敛:单 owner、三个文件、单 commit,无跨 crate 配套改动,未新增 root src/*.rs 实现文件。
  • 前轮 P1 修复有效relative_traversal_enters_special_dir(evaluator.rs:416-431)对前导 .. 链 fail-closed;normalizer 不变量(surviving .. 只可能位于前导,内层均被 pop)成立,../../.. 纯穿越正确放行;traversal_tests.rs 断言方向已反转(../proc/version../../dev/urandom../foo/../proc/version 全部 blocked),旧 head 中把 ../proc/version 钉为 safe 的不安全断言已随 tests.rs 还原一并移除(tests.rs 不再是 changed file)。fail-closed 次序正确:/proc/../etc 经 raw prefix 先行仍阻断。
  • Anti-over-block 矩阵充分/home/../usr/bin/lsusr/../local/file../notes.txt../../tmp/x 均保持 allowed;procx 类前缀不误伤(split('/') 首段精确匹配)。

剩余风险

  • cwd 本身位于 /proc//dev//sys 内部时(如 cwd=/proc/selfcat ../version 解析为 /proc/version),lexical 层无 cwd 无法识别;属 doc comment 已声明的无 filesystem access 契约内的已知取舍。
  • symlink 别名与 ~ 展开不覆盖(已声明);$ 变量/quoting 依赖上游 broker is_shell_meta 与 compound executor rule 5,后续修改上游拦截逻辑不得破坏该分工前提。
  • fail-closed 的已知代价:经 .. 访问恰好名为 proc/dev/sys 的普通目录会被拒(仍可走交互审批),已在函数文档中记录,可接受。

未跑验证(以 CI 实测为准)

  • 本评审不执行工具;evaluator.rs 按 diff 估算净增约 92 行,该文件未登记 large-file-inventory,是否越过 700 行阈值以 check-layout.sh 实测为准。
  • Test cosh-ng fast checks 的失败原因未由本评审复核,见 P2;Test cosh-ng SUCCESS 是否覆盖新增 traversal 用例未复核。
  • PR body 的 1343 通过数与 fmt/clippy clean 自证未复核。

@jfeng18
jfeng18 force-pushed the fix/cosh-ng/readonly-path-traversal branch from 0ae466c to eb1cb67 Compare August 21, 2026 09:13
@jfeng18

jfeng18 commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the follow-up monitor pass. The Test cosh-ng fast checks failure flagged here was on the previous head 0ae466c, whose old base carried a floating toolchain. After rebasing onto eb1cb67 (main now pins 1.97.1), all jobs pass on the current head (2026-08-21):

  • Test cosh-ng fast checks — SUCCESS (09:20:33)
  • Test cosh-ng — SUCCESS (09:23:30)
  • Build cosh-ng release — SUCCESS (09:19:04)

On the evaluator.rs line-count item from the not-run list: it is enforced by the fast-checks layout gate, which passes on the current head — so the 700-line threshold is cleared by measurement, not just the diff-based estimate.

@SunnyQjm

SunnyQjm commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

PR number: #2708
head_sha: eb1cb67
reviewed_at: 2026-08-27T12:14:20Z

Findings

  • [P1] 同一穿越家族仍有残余绕过:裸相对拼写放行 + ~ 被当作可弹出普通段evaluator.rs:429-430evaluator.rs:453-478。两类拼写在当前 head 仍经 readonly 自动执行链放行:
    • (a) relative_traversal_enters_special_dir 要求 traversed == true(至少一个前导 ../),于是 proc/versiondev/urandomsys/kernel/ostype./proc/versionproc/../proc/version(归一化后均为 proc/version 等裸相对形)被放行;当 shell cwd 为 / 时 OS 层解析进 /proc/dev/syscat proc/version 是配置了 readonly 的命令、不含 $/反引号,可直达本判定。
    • (b) normalize_path_lexically~ 当作可弹出的普通目录段:~/../proc/version 归一化为 proc/version 后放行;但 shell 在执行时才将 ~ 展开为 $HOME,HOME=/root(ALinux4 root 登录 shell 正是本 blocklist 防护的默认场景)时实际解析为 /proc/version。前导 ~ 在本层无 cwd/无文件系统契约下不可解析,应 fail-closed,而不是参与段弹出。PR 自述 doc comment 只声明 $/backtick 由上游拦截、未声明 ~;维护者评论已核实 broker.rsreadonly_compound.rs 拒绝集均不含 ~(本评审不读取输入外文件,该点未独立复核,但与 PR 自述一致)。
    • 这与本 PR 自己声明的不变量矛盾:relative_traversal_enters_special_dir 的 doc comment 称"本层无 cwd,故对所有 cwd fail-closed"——同一论证同样适用于 proc/version(存在 cwd=/ 使其解析进 /proc)。建议收敛为单条规则而非逐 case 补丁:去掉 traversed 要求,归一化后跳过前导 .. 的首个存活段为 dev/proc/sys 即阻断;前导 ~ 一律 fail-closed。这不引入新的取舍类别——PR 已接受"经 .. 访问恰好名为 proc/dev/sys 的普通目录也被拒"的代价,此处只是把同一代价延伸到非 .. 拼写,用户仍可走交互审批。
    • 若维护者判定残余集超出本 PR 范围,则应把 closes #2184 降级为 refs #2184 并单列跟踪,避免 issue 在部分已报向量仍放行时被关闭。上一轮(同 head)的 P3 裸相对项经本轮独立静态推演确认成立,合并入本 P1。
  • [P2] 跨层 $ 分工仅有文档约定,无回归测试钉住evaluator.rs:399-405(doc comment)。本层对 $/backtick 拼写直接放行,依赖上游 broker is_shell_meta 与 compound executor rule 5 拦截;该分工无任何测试钉住。若未来出现绕过上游两道闸直达 is_safe_readonly_path 的新 caller,[cosh-ng] bug: is_blocked_special_path 纯字符串前缀匹配,路径穿越与 $'...' ANSI-C quoting 可绕过 readonly_rules 黑名单 #2184$'/proc/self/cmdline' 类向量将静默回归。建议对每个自动执行入口补一条回归断言:上游闸门拒绝 $'/proc/version' 类拼写,让假设破裂时响亮失败而非静默放行。

结构评审确认项(无 blocking 组织问题)

未发现 blocking package/module/public API 组织问题:

  • Owner 归属正确:全部改动落在 tools/readonly_rules/,readonly 路径策略属 tools/ owner(standard.md §2),单 crate、单 owner、三文件,无跨 crate 配套改动混入。
  • Public API 未扩大lib.rs:32-34 新增的 mod readonly_traversal_tests#[cfg(test)] private mount,非 pub mod/pub useis_blocked_special_path 既有 pub 签名不变,新增三个函数均为 private fn,无需更新 public-api-inventory。
  • 无 forbidden dependency / self-crate path:仅 std 字符串操作,无 crate::agent/runtime/ui/... 命中;测试经 crate::tools::... 引用,无 cosh_shell::...
  • 无新增 root src/*.rs 实现文件:测试文件位于 owner 目录内,经 lib-only cfg(test) 挂载以规避 lib/bin overlap ratchet 属既有 wrap_tests pattern,测试挂载细节归 shell-test-organization SDD。
  • 阻断逻辑静态推演(已覆盖部分)正确:raw prefix 先行使 /proc/../etc 保持阻断(fail-closed 次序正确);normalizer 不变量(surviving .. 只可能位于前导、绝对路径越根 .. 丢弃)成立;测试矩阵中 /../proc/./proc//proc/tmp/../proc../foo/../proc 等阻断方向与 /home/../usr/bin/ls../notes.txt..../.. 等放行方向均与实现一致;首段精确匹配不误伤 procx 类前缀。
  • 行数阈值evaluator.rs 净增约 92 行(diff 估算),未登记 large-file-inventory;当前 head 上 Test cosh-ng fast checks(含 layout gate)实测 SUCCESS,按规则以 CI 实测为准,未越线。
  • 前轮 findings 闭环情况:首轮 P1(相对 .. 穿越)已 fail-closed 修复且断言方向反转,首轮/次轮 P2(CI)已在当前 head 转绿(fast checks / Test cosh-ng / release build 均 SUCCESS)。

剩余风险

  • cwd 本身位于 /proc//dev//sys 内部时的回退拼写无法识别,属 doc comment 已声明的无 cwd/无 filesystem access 契约内的已知取舍。
  • symlink 别名不覆盖(已声明);若 P1 按建议修复,"恰好名为 proc/dev/sys 的普通目录"被拒的代价面会略扩大,仍可走交互审批,可接受。

未跑验证(以 CI 实测为准)

  • 本评审不执行工具;normalizer 与阻断逻辑正确性为静态推演,未实际编译运行。
  • P1 的 ~/裸相对绕过路径依据 diff 静态推演,与评论串中维护者的实测结论一致;~ 不在上游闸门拒绝集这一点未由本评审独立读取 broker.rs/readonly_compound.rs 复核。
  • PR body 的 1343 测试通过数与 fmt/clippy clean 自证未独立复核,但对应门禁在当前 head 实测 SUCCESS。

kongche-jbw pushed a commit that referenced this pull request Aug 26, 2026
Clippy 1.98 on stable rolls the new chunks_exact_to_as_chunks lint
onto pre-existing decode_hex code, turning the fast-checks job on
#2708 red. as_chunks::<2>().0.iter() is semantics-preserving: same
full chunks, same dropped tail remainder as chunks_exact(2).
Fixes: 1a93589 ("feat(cosh-ng): [core,shell] add audit log")
Assisted-by: Qoder
Signed-off-by: Jiangtian Feng <jiangtianf97@163.com>
@SunnyQjm

SunnyQjm commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

PR number: #2708
head_sha: eb1cb67
verified_at: 2026-08-27T15:04:06Z
tier: T2

PR #2708 验证报告:fix(cosh-ng): block path traversal in readonly

验证范围

  • HEAD 核对:git rev-parse HEAD = eb1cb67f3139071174b62fd946355079780c27ab,与作业一致。
  • doctor:contended=true(load 6.03/12 核,WindowServer/CloudShell/SogouInput 繁忙),仅作归因参考。
  • Backend:container(profile cosh-container-verify-2708runtime=docker)。默认 example profile 含占位 auth 路径导致 auth-blocked,改用自建无 auth profile;Apple container runtime 当前 NAT 全主机失效(本作业容器与另一在跑作业的 cosh-lab-verify-2917-arm64 均出外网超时,网关可达),按"container 后端自动探测 container/docker"流程切到 docker runtime。
  • 环境 RUN_ID:20260827T141657Z-2c04667c(首次 env upenvironment-unavailable:镜像拉取成功但 dnf provisioning 超过 600s 超时;随后在同一容器内手动补全 dnf 安装并复用该 env manifest 跑测试,复用的 cargo 缓存 cosh-lab-verify-2708 为无属主遗留缓存,target_cache_key bce21a53b203bf5d4d47 与本 worktree 匹配)。
  • 已运行 scope(经 cosh-lab test run):
    1. cargo test --locked --package cosh-shell --lib readonly_rules(RUN_ID 20260827T145526Z-1241da3a
    2. cargo test --locked --package cosh-shell --lib readonly_traversal_tests(RUN_ID 20260827T145849Z-e61183bf
  • scope 校验:cosh-lab test plan --base origin/main 因 worktree 被 .cosh-lab/runs 运行元数据污染而退化为全 crate 建议,未采纳;作业 scopes 与 3 个变更文件精确对应,按作业 scopes 执行。

结果分类

两个 focused scope 首跑即通过,无需复跑:

Scope 退出码 结果
readonly_rules 0 34 passed; 0 failed; 1317 filtered out
readonly_traversal_tests 0 4 passed; 0 failed; 1347 filtered out(PR 新增 4 个穿越测试全绿)

stderr 证实两次运行均重新编译了当前 worktree 的 cosh-shell v0.18.0(非陈旧缓存产物)。

Findings 验证

[P1] 残余绕过(裸相对拼写 + ~ 弹出)— 证实

谓词层(容器内实测,补充测试 RUN_ID 20260827T145949Z-3265012c):

  • is_safe_readonly_pathproc/versiondev/urandomsys/kernel/ostype./proc/versionproc/../proc/version~/../proc/version 全部返回 true(放行)cat proc/version / cat ./proc/version / cat proc/../proc/version / cat ~/../proc/version 均通过 is_readonly_command
  • 期望 fail-closed 的反向断言在首个用例 proc/version 即 panic,证实当前实现不放行失败方向。

机制层(代码精读当前 head):

  • evaluator.rs:416-430relative_traversal_enters_special_dir 确实要求 traversed == true(至少一个前导 ../),裸相对形归一化后无前导 .. 直接放行。
  • evaluator.rs:452-478normalize_path_lexically~ 当普通段参与弹出,~/../proc/version 归一化为 proc/version
  • 上游闸门独立复核:评审转述属实——broker.rs:89-108 is_shell_meta 拒绝集(; | & > < $ \ ( ) { } ' " \ \n \r)**不含 ~**;readonly_compound.rs:102rule 5 仅查$` 与反引号。
  • 执行链可达性:DirectReadonlyBroker 路由自动批准后经 ApprovalOutcome::ForegroundShellHandoffapproval_bridge.rs:572-635approval/handoff.rs:32-67)把命令文本注入用户交互 shell PTY,真实 bash/zsh 执行时展开 ~;compound 执行器以请求 shell 的可验证 cwd 运行(approval_bridge.rs:690readonly_compound.rs:139-160)。两条自动执行链下两类向量均端到端可行。

OS 层前提(容器内实测,RUN_ID 20260827T150015Z-8917c002):

  • cd / && head -c 60 proc/version → 读到 /proc/version 内容;
  • cd /tmp && head -c 60 ~/../proc/version(HOME=/root)→ 读到 /proc/version 内容;
  • cd / && head -c 60 ./proc/version → 读到 /proc/version 内容。

[P2] $ 分工无回归测试钉住 — 部分证实

补充测试(评审证据,不 commit 不 push)

临时追加在 src/cosh-ng/crates/cosh-shell/src/tools/readonly_rules/traversal_tests.rs 尾部(运行后已 git checkout 还原,worktree 已回到 PR head 洁净态):

#[test]
fn verify2708_bare_relative_spellings_currently_allowed() {
    for path in ["proc/version", "dev/urandom", "sys/kernel/ostype",
                 "./proc/version", "proc/../proc/version"] {
        assert!(is_safe_readonly_path(path), "{path}");
    }
    assert!(allowed("cat proc/version"));
    assert!(allowed("cat ./proc/version"));
    assert!(allowed("cat proc/../proc/version"));
}

#[test]
fn verify2708_tilde_traversal_currently_allowed() {
    assert!(is_safe_readonly_path("~/../proc/version"));
    assert!(allowed("cat ~/../proc/version"));
}

#[test]
fn verify2708_desired_behavior_currently_fails() {
    for path in ["proc/version", "dev/urandom", "sys/kernel/ostype",
                 "./proc/version", "proc/../proc/version", "~/../proc/version"] {
        assert!(!is_safe_readonly_path(path), "{path}");
    }
}

运行结果(RUN_ID 20260827T145949Z-3265012c):前两个测试 ok(当前放行被钉死),第三个 FAILED 于首个断言 proc/version(期望阻断、实际放行)。2 passed / 1 failed / 1351 filtered。

证据索引

纯 CLI/单元测试验证,无 UI 流程,无截图(evidence_dir 为空)。证据托管于 cosh-lab run 目录(含 run.json / commands.log / stdout 全文):

  • (env)20260827T141657Z-2c04667c — docker runtime 环境(首次 env up 超时失败后手动补全 provisioning)
  • (scope1)20260827T145526Z-1241da3a — readonly_rules 34 通过
  • (scope2)20260827T145849Z-e61183bf — readonly_traversal_tests 4 通过
  • (repro)20260827T145949Z-3265012c — verify2708 复现 2 ok + 1 expected-fail
  • (OS 演示)20260827T150015Z-8917c002 — 三种拼写读到 /proc/version
  • (env down)20260827T150133Z-c0b7c857 — 容器已清理

cosh-lab 未覆盖、直接执行的原生命令(如实列出):container list/exec/stop/rm(Apple runtime 探查与清理我自建失败容器)、docker info/ps/exec/restart、容器内手动 dnf install curl git gcc make openssl-devel pkgconf-pkg-config procps-ng tar(补 provisioning)、curl 测速、open -a Docker 及退出 Docker Desktop、补充测试文件的 git checkout 还原。

未运行 scope

  • cargo test --workspace、cosh-shell 的 logic / protocol / raw-cli / shell-host targets;
  • fmt / clippy / layout gate(check-layout.sh)、release build;
  • test plan 建议的全 crate cargo test --locked --package cosh-shell(因污染退化,未采纳)。
  • 以上为容器内 focused green,不代表全量/CI 通过,CI 仍是合入门禁权威。

结论

No-Go — 静态评审 P1 经容器内单元测试与 OS 层解析双重证实:当前 head 仍放行 proc/version./proc/versionproc/../proc/version 等裸相对拼写及 ~/../proc/version(cwd=/ 或 HOME=/root 时均实际解析进 /proc),与 PR 自称的 fail-closed 不变量矛盾,closes #2184 不宜按现状合入。静态评审结论本身无需修正;仅 P2 表述建议从"无任何测试钉住"修正为"机制层已有 $ 拒绝用例、缺 #2184 向量级入口断言"。

@SunnyQjm

Copy link
Copy Markdown
Collaborator

Reviewed against origin/main @ 880173c. The normalization approach is right and the reported vectors are genuinely fixed — but the same vector family still has residual bypasses, and they contradict this PR's own stated invariant.

Verified fixed

/proc/version, /../proc/version, /./proc/version, //proc/version, ../proc/version, ~/../../proc/version — all blocked. Keeping the raw-prefix pass first so /proc/../etc stays fail-closed is the correct ordering.

P1: residual bypasses in the same family

relative_traversal_enters_special_dir requires traversed == true, and normalize_path_lexically pops ~ as an ordinary segment. So relative spellings that resolve into the blocklist without a surviving leading .. are still allowed:

input                       allowed  normalized      resolves to
~/../proc/version              true  proc/version    /proc/version   (HOME=/root)   <-- BYPASS
proc/version                   true  proc/version    /proc/version   (cwd=/)        <-- BYPASS
dev/urandom                    true  dev/urandom     /dev/urandom    (cwd=/)        <-- BYPASS
sys/kernel/ostype              true  sys/kernel/ostype /sys/kernel/ostype (cwd=/)   <-- BYPASS
src/main.rs                    true  src/main.rs     -               (control, correctly allowed)

Reproduce by extracting the four functions from this PR's evaluator.rs verbatim into a standalone rustc file and running the inputs above — no cosh-shell run needed, same method as the issue's static harness.

Two notes on reachability:

  • cat proc/version with cwd=/ is a configured readonly command with no $/backtick, so it reaches is_safe_readonly_path and the auto-execution chain.
  • HOME=/root makes the single-level ~/.. form land on /, and root login shell is exactly the ALinux4 default-shell scenario this blocklist protects.

Why this is a design gap, not a missing case: the doc comment on relative_traversal_enters_special_dir states the rule as "this layer has no filesystem access and no cwd, so it fails closed and refuses the spelling for every cwd". That reasoning applies identically to proc/version — there exists a cwd (/) under which it resolves into /proc. The predicate currently applies cwd-independent fail-closed only to ..-led spellings, which is asymmetric.

Suggested convergence (one change, not four cases): drop the traversed requirement and block whenever the first surviving component after normalization — skipping any leading .. — is dev/proc/sys; and treat a leading ~ as unresolvable (fail closed) instead of a poppable segment. This does not introduce a new tradeoff category: the PR already accepts "an ordinary directory literally named proc/dev/sys reached through .. is also refused", and this extends exactly that accepted cost to the non-traversal spellings. Users keep the interactive-approval path.

P2: the cross-layer $ assumption is documented but not test-pinned

The doc comment correctly cites both upstream gates, and I confirmed them: broker::is_shell_meta denies $, and readonly_compound.rs:102 rejects tokens containing $ or backticks. But nothing in the test suite pins that division of labor, and evaluator.rs itself has no $ handling — if a future caller reaches is_safe_readonly_path without passing those gates, the $'...' vector from #2184 silently returns. Worth one regression asserting the upstream gates deny $'/proc/version' for each auto-execution entry point, so the assumption breaks loudly rather than silently.

Also note ~ is in neither gate's rejection set (broker.rs:90-107, readonly_compound.rs:102), which is what makes the ~/.. form above reachable.

Scope suggestion

If the residual set is judged out of scope for this PR, then closes #2184 should be downgraded to refs #2184 and the remaining spellings tracked, so the issue does not close while part of its own reported vector family is still allowed.

is_blocked_special_path matched /proc, /dev and /sys by raw prefix
only, so spellings like /../proc/version bypassed the blocklist.
Normalize lexically (no filesystem access) before matching, keep
the raw-prefix pass first (fail-closed), and block relative
spellings whose first surviving component is proc/dev/sys; a
leading ~ popped by .. is unresolvable and fails closed.
$-quoting forms stay intercepted upstream by the exec layer.
Traversal tests moved to a lib-only module (overlap ratchet).

Closes #2184

Assisted-by: Qoder
Signed-off-by: Jiangtian Feng <jiangtianf97@163.com>
@jfeng18
jfeng18 force-pushed the fix/cosh-ng/readonly-path-traversal branch from eb1cb67 to 8af1480 Compare August 28, 2026 02:59
@jfeng18

jfeng18 commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough verification round — the OS-level reproductions (cwd=/ + proc/version, HOME=/root + ~/..) and the upstream-gate cross-check made the residual set unambiguous.

Landed in 8af1480 (single commit, force-pushed onto latest main), following your convergence suggestion exactly — one rule instead of per-case patches:

  • Dropped the traversed precondition. first_component_targets_special_dir (renamed from relative_traversal_enters_special_dir) now blocks whenever the first surviving component after lexical normalization — skipping any leading .. chain — is dev/proc/sys. One rule covers the whole bare-relative family you reproduced: proc/version, dev/urandom, sys/kernel/ostype, ./proc/version, proc/../proc/version, plus the already-blocked ../proc family.
  • Leading ~ fails closed. normalize_path_lexically returns Option now: a .. that would pop a leading ~/~user segment is unresolvable at this layer (the shell expands it only at execution time), so it returns None and is_blocked_special_path blocks. A ~ after another segment stays a literal directory name and pops normally — ~/notes.txt and ~/foo/../bar remain allowed.
  • Cross-layer $ assumption is now test-pinned (your P2): readonly_broker_blocks_shell_metas_and_mutation asserts $'/proc/version' / $'/etc/shadow' are denied by the broker's shell-meta gate (asserting the metacharacter error, so the denial path is the gate itself), and build_plan_fails_closed_for_ineligible_shapes pins the same vectors for the compound executor's rule 5. A regression in either gate now breaks loudly instead of silently reaching the predicate.

Anti-false-positive matrix re-run: ../notes.txt, ../../tmp/x, usr/../local/file, procx/file, local/dev/file, ~/notes.txt, cat /home/../usr/bin/ls all stay allowed — the legitimate-relative-path boundary did not move.

Discriminative two-direction check (ECS, toolchain 1.89.0): on the previous head eb1cb67f the two new tests fail at exactly the vectors you called out (proc/version, ~/../proc/version); on 8af14801 they pass. Full gates on the new head: fmt clean, clippy -D warnings clean, cargo test -p cosh-shell --lib 1404 passed / 0 failed, lib/bin test overlap unchanged at 696.

closes #2184 stands — the reported vector family is now fully covered.

@SunnyQjm

SunnyQjm commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

PR number: #2708
head_sha: 8af1480
reviewed_at: 2026-08-28T03:22:41Z

Findings

  • [P2](沿用上轮、仍待作者确认)compound 链对引号/反斜杠拼写的分词假设未声明、未钉住evaluator.rs:382-399(doc comment)、readonly_compound_tests.rs:89-93。上一轮已提出,作者尚未回复,本轮维持:本 PR 把 $ 的跨层分工写成文档并补了向量级断言,但对普通引号与反斜杠转义只字未提,两条自动执行链覆盖不对称——
    • broker 链:is_shell_meta 拒绝集含 \'"(T2 轮已容器内核实),cat '/proc/version'cat /pro\c/version 在 broker 入口即被拒,无缺口。
    • compound 链:rule 5 仅查 $ 与反引号(已核实),不含引号与反斜杠。若分词是 shell_words 式(剥离引号、处理转义),token 即 /proc/version,被本层前缀检查阻断,无缺口;若是裸空白分词,token 保留原样 → rule 5 不命中、前缀不命中、重定向 shape 不涉及 → 放行,而真实 shell 执行时引号/转义被剥掉,实际读取 /proc/version
    • 本评审不读取输入外文件,分词方式无法从 diff 判定,故按不确定项提出。建议作者一句话确认分词行为;若为 shell_words 式,在 doc comment 分工段补一句说明并各补一条向量级断言(与本次 $ pin 同模式);若为裸分词,需把引号/反斜杠纳入 rule 5 或先行规范化。此为预先存在的行为面、非本 PR 引入,确认分词为 shell_words 式后不阻塞合入。

前轮 findings 闭环(静态推演确认修复有效)

  • R3 P1(裸相对拼写 + ~ 弹出)— 已修复,与维护者收敛建议一致first_component_targets_special_dir(evaluator.rs:427-440)已去掉 traversed 前提,归一化后跳过前导 .. 链的首个存活段为 dev/proc/sys 即阻断。逐条推演:proc/versiondev/urandomsys/kernel/ostype./proc/version. 段丢弃)、proc/../proc/version(内层 proc 被弹)→ 全部命中阻断;procx/file(非精确首段)、local/dev/file(首段 local)、proc/../version(弹后剩 version)正确放行。
  • ~ fail-closed 守卫边界正确normalize_path_lexically 改返 Option(evaluator.rs:466-500),守卫条件 !absolute && segments.len() == 1 && seg.starts_with('~') 精确覆盖"前导 ~.. 弹"这一唯一在执行期展开的情形:~/../proc/version~root/..~/../../proc/version 走 None 分支阻断;~/foo/../bar(弹 foo)、a/~/../b~ 非前导作字面名)、/~/../proc(绝对路径不走守卫,归一化后前缀阻断)三个边界均处理正确。
  • R3 P2($ 分工无测试钉住)— 已修复:broker.rs:133-140 断言 $'/proc/version'/$'/etc/shadowmetacharacter 错误;readonly_compound_tests.rs:89-93 为 rule 5 钉住同组向量,任一上游闸门退化都会响亮失败。
  • CI 状态变化(相对上一轮):上一轮四项检查均 IN_PROGRESS,本轮 Test cosh-ngTest cosh-ng fast checksBuild cosh-ng releaseTest cosh-ng prebuilt action、Commit Message Lint、PR Checks、license/cla 全部 SUCCESS,合并门禁状态已转绿。

结构评审确认项(无 blocking 组织问题)

未发现 blocking package/module/public API 组织问题:

  • Owner 归属正确:全部改动落在 tools/(broker、readonly_compound、readonly_rules 均属 tool classification / readonly policy owner,standard.md §2),单 crate、单 owner,无跨 crate 配套改动混入。
  • Public API 未扩大:lib.rs:41-43 新增的 mod readonly_traversal_tests#[cfg(test)] private mount,非 pub mod/pub useis_blocked_special_path 既有 pub 签名不变;first_component_targets_special_dir/has_blocked_special_prefix/normalize_path_lexically 均为 private fn,无需更新 public-api-inventory。
  • 无 forbidden dependency / self-crate path:仅 std 字符串操作,无 crate::agent/runtime/ui/... 命中;测试经 crate::tools::... 引用,无 cosh_shell::...
  • 无新增 root src/*.rs 实现文件:新测试文件在 owner 目录内,经 lib-only cfg(test) 挂载规避 lib/bin overlap ratchet,沿用既有 wrap_tests pattern,测试挂载细节归 shell-test-organization SDD。
  • Fail-closed 次序正确:raw prefix 先行,/proc/../etc 保持阻断(traversal_tests.rs 钉住);//proc//version/tmp/../proc/version 归一化后命中前缀。
  • 行数阈值evaluator.rs 本轮净增约 113 行(diff 估算),未登记 large-file-inventory;当前 head 的 Test cosh-ng fast checks(含 layout gate)实测 SUCCESS,按规则以 CI 实测为准,未越线。

剩余风险

  • cwd 本身位于 /proc//dev//sys 内部时的回退拼写无法识别,属 doc comment 已声明的无 cwd/无 filesystem access 契约内的已知取舍。
  • symlink 别名不覆盖(已声明);字面名为 proc/dev/sys 的普通目录及 ~root/.. 类拼写被拒的代价面本轮略扩大,仍可走交互审批,已记录于函数文档与测试,可接受。
  • 与 cwd 取舍同类的一个边缘情形:$HOME 指向 / 或特殊目录内部时,~/proc/version 类拼写在执行期解析进 blocklist 而本层放行(无 env/filesystem 契约覆盖不到);ALinux4 root 登录 HOME=/root 不触发,记录备查,不要求本 PR 处理。
  • P2 的 compound 分词问题若确认为裸分词,引号/反斜杠家族需后续 PR 收敛(不属 [cosh-ng] bug: is_blocked_special_path 纯字符串前缀匹配,路径穿越与 $'...' ANSI-C quoting 可绕过 readonly_rules 黑名单 #2184 已报向量,不影响 closes #2184 成立)。

未跑验证(以 CI 实测为准)

  • 本评审不执行工具;normalizer/阻断逻辑正确性为逐条静态推演,未实际编译运行。
  • 作者自述的本地 1404 lib 测试通过、fmt/clippy clean、overlap 696 未由本评审复核,但对应门禁在当前 head 实测 SUCCESS。
  • P2 的分词行为依赖输入外源码,探针复现路径已写入验证合同的 repro_hint,留待验证环节判别。

@SunnyQjm

Copy link
Copy Markdown
Collaborator

PR number: #2708
head_sha: 8af1480
verified_at: 2026-08-28T04:00:48Z
tier: T2

PR #2708 验证报告(T2)

验证范围

  • 现场核对:git rev-parse HEAD = 8af14801ef8b7115854533ed80f7a10c683da211,与作业 head_sha 一致;PR 实际变更 = 单提交 HEAD~1..HEAD(5 文件 +258 行;本地 origin/main 已大幅超前,不作为 diff 基线)。
  • doctor:ok=truecontended=false(load_1m 7.4/12 核),container runtime = Apple container 1.0.0。
  • 执行位置:container 后端,RUN_ID 20260828T034808Z-9788c38b(容器 cosh-lab-arm64-2708-verify,alinux3 arm64)。技能自带示例 profile 为占位符(auth.source_path 指向不存在的 settings.json,首次 env upauth-blocked),按 resolve_profile_arg 支持的文件路径形式写了本地 profile 副本:去掉 [auth](纯 cargo 测试不需要 provider 鉴权)、独立容器名、沿用既有 verify 容器同 digest 镜像与共享温缓存。
  • 已运行 scope(cosh-lab test run --scope unit,原生命令均为 cargo test --locked --package cosh-shell --lib <filter>):
Scope 来源 结果 run_dir(.cosh-lab/runs/ 下)
readonly_rules 作业 scope exit 0,34 passed / 0 failed 20260828T035131Z-cace768d
readonly_traversal_tests 作业 scope exit 0,6 passed / 0 failed 20260828T035322Z-48fa91a8
readonly_compound 作业 scope exit 101,18 passed / 2 failed(含 2 个探针均 pass) 20260828T035409Z-7c701ccd
readonly_broker 补全 exit 0,9 passed / 0 failed 同批次
  • scope 调整说明:PR 对 broker.rs 的改动落在测试 tools::broker::tests::readonly_broker_blocks_shell_metas_and_mutation 内,作业给的 3 个过滤器均匹配不到该路径,补跑 readonly_broker 以覆盖 PR 全部变更文件。

结果分类

  • readonly_rulesreadonly_traversal_testsreadonly_broker通过(首跑即绿,容器内 focused 口径,非全量/CI 口径)。
  • readonly_compound失败(首跑)→ 串行复跑仍失败 → 维持失败分类,但有直接观测证据归因为容器环境限制,非 PR 回归
    • 首跑 2 个失败:executor_join_does_not_wait_for_descendants_holding_the_pipeexecutor_reaps_descendants_that_redirected_their_output(均为 pre-existing 测试,PR 只在该文件加了 5 行 $'...' 断言,未触碰这两个函数及其验证的 reap 机制)。
    • 串行复跑(逐条单独跑):两者再次失败,报错形态完全一致(exit 101)。
    • 环境归因证据:容器内 ps 实测 PID 1 为 sleep(从不回收子进程);首跑失败的 descendant PID 2260/2294、复跑的 2454/2509 全部观测为 Z <defunct> 僵尸且 reparent 到 PID 1。即被测的进程组 SIGKILL 实际生效(descendant 已死),失败的是测试的 kill(pid, 0) 活性探针——它对僵尸进程仍返回存活。在 PID 1 正常回收的 Linux 宿主上(如 CI)僵尸即时消失、探针返回 ESRCH,与该 head 上 CI Test cosh-ng 全绿一致。

Findings 验证

  1. [P2] compound 链引号/反斜杠分词假设未钉住 → 证伪"存在缺口"的担忧,确认无缺口。分词器为 shell_words 式,证据链完整:
    • 静态精读 command_risk_parser.rs:84-97:引号字符本身不进入 token(剥离),引号内容原样保留;:314-319:反斜杠消耗下一字符并字面化(转义解析)。
    • 容器内探针实测(2 个探针均 pass):parse_command("cat '/proc/version' && pwd")parse_command("cat /pro\\c/version && pwd") 的 segments 均为 [[["cat","/proc/version"]],[["pwd"]]]——引号/反斜杠拼写在 token 层已归一为 /proc/versionbuild_readonly_compound_plancat /proc/version && pwdcat '/proc/version' && pwdcat "/proc/version" && pwdcat /pro\\c/version && pwd 四种拼写全部返回 None(rule 6 的 readonly 路径判定命中阻断前缀)。
    • broker 链静态复核一致:is_shell_metabroker.rs:89-108)拒绝集含 '"\,引号/反斜杠拼写在 broker 入口即被拒。
    • 结论:finding 的"裸空白分词"假设不成立,compound 链对引号/反斜杠拼写无绕过缺口。按 finding 自身约定("确认分词为 shell_words 式后不阻塞合入"),剩余建议仅为文档分工段补一句说明 + 向量级断言(本次探针源码见下,作者可自行采纳),不阻塞合入。
  2. 前轮 R3 P1(裸相对拼写 + ~ 弹出)→ 证实已修复且行为正确readonly_traversal_tests 6/6 容器内通过:proc/version./proc/versiondev/urandom../proc/version~/../proc/version~root/.. 等阻断向量全命中;procx/filelocal/dev/file~/foo/../bar/home/../usr/bin/ls 等放行向量无误伤。静态精读 first_component_targets_special_dir(evaluator.rs:427-440)与 normalize_path_lexically~ fail-closed 分支(evaluator.rs:466-500)与测试行为一致。
  3. 前轮 R3 P2($ 分工无测试钉住)→ 证实已修复readonly_broker_blocks_shell_metas_and_mutation(含 $'/proc/version'$'/etc/shadow' 报 metacharacter 断言)与 compound 侧 build_plan_fails_closed_for_ineligible_shapes(含同组向量)均容器内通过。
  4. 结构评审确认项(public API 未扩大、cfg(test) 私有挂载、fail-closed 次序)→ 静态核对一致,与 findings 描述相符。

补充测试(评审证据,未提交)

写入 src/cosh-ng/crates/cosh-shell/src/tools/readonly_compound_tests.rs 末尾(仅 worktree,未 commit):

#[test]
fn probe_tokenizer_strips_quotes_and_backslash_escapes() {
    let quoted = super::command_risk_parser::parse_command("cat '/proc/version' && pwd");
    assert_eq!(quoted.segments, vec![
        vec![vec!["cat".to_string(), "/proc/version".to_string()]],
        vec![vec!["pwd".to_string()]],
    ]);
    let escaped = super::command_risk_parser::parse_command("cat /pro\\c/version && pwd");
    assert_eq!(escaped.segments, vec![
        vec![vec!["cat".to_string(), "/proc/version".to_string()]],
        vec![vec!["pwd".to_string()]],
    ]);
}

#[test]
fn probe_quoted_or_escaped_special_paths_stay_ineligible() {
    for command in [
        "cat /proc/version && pwd",
        "cat '/proc/version' && pwd",
        "cat \"/proc/version\" && pwd",
        "cat /pro\\c/version && pwd",
    ] {
        assert!(build_readonly_compound_plan(command).is_none(),
            "{command} must stay ineligible");
    }
}

运行结果:容器内 cargo test --locked --package cosh-shell --lib readonly_compound 中两个探针均 ok(见 run_dir 20260828T035409Z-7c701ccd 的 stdout)。

证据索引

本作业为单元测试级验证,无 PTY/渲染界面,未产生截图,evidence_dir 为空。证据载体为各 run_dir 下的 commands.log/result.json(上文表格)及容器内 ps 僵尸观测输出(会话记录)。

未运行 scope

全量 cargo test --workspace--lib 全量(1406 项)、clippy/fmt、logic/protocol/raw_cli/shell_host 集成与 PTY 层均未运行(作业 scopes 之外);CI 仍是合入门禁权威。

结论

Go —— PR 变更面对应的 focused scope 全绿,唯一待决的 P2 经容器内探针实锤为 shell_words 式分词、无安全缺口,静态评审结论无需修正;readonly_compound 首跑与串行复跑的 2 个失败已由容器内直接观测归因为 PID 1(sleep)不回收僵尸进程导致的测试探针误判(被测的进程组清除机制本身生效,descendant 均处于已死亡的 Z 态),属验证环境限制而非 PR 回归,且同 head CI 的 cosh-ng 门禁已全绿。

kongche-jbw pushed a commit that referenced this pull request Aug 29, 2026
Clippy 1.98 on stable rolls the new chunks_exact_to_as_chunks lint
onto pre-existing decode_hex code, turning the fast-checks job on
#2708 red. as_chunks::<2>().0.iter() is semantics-preserving: same
full chunks, same dropped tail remainder as chunks_exact(2).
Fixes: 1a93589 ("feat(cosh-ng): [core,shell] add audit log")
Assisted-by: Qoder
Signed-off-by: Jiangtian Feng <jiangtianf97@163.com>
@kongche-jbw
kongche-jbw force-pushed the fix/cosh-ng/readonly-path-traversal branch from 8af1480 to 736b551 Compare August 29, 2026 04:01
Forrest-ly pushed a commit to Forrest-ly/anolisa that referenced this pull request Aug 29, 2026
Clippy 1.98 on stable rolls the new chunks_exact_to_as_chunks lint
onto pre-existing decode_hex code, turning the fast-checks job on
alibaba#2708 red. as_chunks::<2>().0.iter() is semantics-preserving: same
full chunks, same dropped tail remainder as chunks_exact(2).
Fixes: 1a93589 ("feat(cosh-ng): [core,shell] add audit log")
Assisted-by: Qoder
Signed-off-by: Jiangtian Feng <jiangtianf97@163.com>
@jfeng18

jfeng18 commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator Author

P2 确认(分词行为):compound 链的分词是 shell 式的,引号和反斜杠在 token 化阶段就已解析,到达路径检查的都是解析后的字面量——无缺口。证据:

  • 引号command_risk_parser.rs 的 quote arms('/" 进入 quote 模式,引号字符本身被消费、内容原样入 token)。已有测试钉住:readonly_compound_tests.rsbuild_plan_accepts_quoted_and_newline_forms 断言 ls 'my dir' → argv ["ls", "my dir"]
  • 反斜杠:同文件 \\ 分支消费下一个字符并入 token(token.push(next)),即 cat /pro\c/version → token /proc/version,命中块名单前缀检查。

因此 cat '/proc/version'cat /pro\c/version 在 compound 链上都会以解析后的 /proc/version 进入 is_safe_readonly_path,被阻断。broker 链则由 is_shell_meta 拒绝集(含 \'")在更上游拦截。两条链对称、无放行路径。

@SunnyQjm

Copy link
Copy Markdown
Collaborator

PR number: #2708
head_sha: 736b551
reviewed_at: 2026-08-31T06:40:33Z

Findings

  • [P3] PR body 与最终 head 的行为面描述不一致(文档级,不阻塞) — PR body 仍是初版表述:Validation 写 "1343 passed (baseline 1340 + 3 new)",而当前 head 为 6 个 traversal 用例 + broker/compound 两组 $ 向量钉(作者自报 1404 lib 测试);Risk and compatibility 写 "the newly intercepted set is exactly the spellings that lexically resolve into /proc, /dev or /sys",但最终实现是 cwd 无关 fail-closed——裸相对拼写 proc/version、前导 ~.. 弹的拼写在任何 cwd 下一律拒绝,拦截集是"存在某 cwd 可解析进 blocklist"的超集,并非 "exactly lexically resolve into"。对一个安全收紧 PR,建议合并前更新 body 的 What changed / Validation 两段,使合并记录准确反映故意误伤面(字面名 proc/dev/sys 的普通目录会被拒、可走交互审批)。

前轮 findings 闭环(与上一轮引用逐条比对,无回退)

  • R1 P1(相对 .. 穿越)/ R3 P1(裸相对拼写 + ~ 弹出)— 维持修复first_component_targets_special_dir(evaluator.rs:429-441,diff hunk 推算)无 traversed 前提,归一化后跳过前导 .. 链的首个存活段为 dev/proc/sys 即阻断;normalize_path_lexically(evaluator.rs:465 起)对"前导 ~/~user.. 弹"返回 None、上层 fail-closed。本轮 diff 与 R4 评审所引实现一致。
  • R3 P2 / R4 $ 分工测试钉 — 维持修复:broker.rs:133-140($'/proc/version'/$'/etc/shadow'metacharacter)与 readonly_compound_tests.rs:89-93(rule 5 同组向量)均在。
  • R4 P2(compound 分词假设)— 已闭环:作者 PR 级评论确认 shell 式分词(引号/反斜杠在 token 化阶段解析),T2 验证轮已用容器探针实锤无缺口("确认 shell_words 式后不阻塞合入")。可选的 doc/断言补强未采纳,属作者明确回复后的可选残留,不重复列 finding;doc 分工段本轮补上了 broker 侧 "quote characters" 表述。
  • 相对 8af1480 的 delta:据上一轮评审引用与本轮 diff 比对,实现/测试逐行一致,仅 doc 表述增补与 rebase(lib.rs 挂载行号微移),无逻辑变化。

结构评审确认项

未发现 blocking package/module/public API 组织问题:

  • Owner 归属正确:全部改动落在 tools/(broker、readonly_compound、readonly_rules 均属 readonly policy owner,standard.md §2),单 crate、单 owner、5 文件,无跨 crate 改动混入。
  • Public API 未扩大:lib.rs:41-44 新增的 mod readonly_traversal_tests#[cfg(test)] private mount,非 pub mod/pub useis_blocked_special_path 既有 pub 签名不变;first_component_targets_special_dir/has_blocked_special_prefix/normalize_path_lexically 均为 private fn,无需更新 public-api-inventory。
  • 无 forbidden dependency / self-crate path:仅 std 字符串操作,无 crate::agent/runtime/ui/... 命中;测试经 crate::tools::... 引用,无 cosh_shell::...
  • 无新增 root src/*.rs 实现文件:新测试文件在 owner 目录内,lib-only cfg(test) 挂载沿用既有 wrap_tests pattern,测试挂载细节归 shell-test-organization SDD。
  • Fail-closed 次序正确:raw prefix 先行,/proc/../etc/hostname 保持阻断(traversal_tests.rs:44-49 钉住);~ 守卫条件 !absolute && segments.len() == 1 && seg.starts_with('~') 精确覆盖唯一在执行期展开的情形,~/foo/../bara/~/../b/~/../proc 三边界推演正确且均有测试钉住。
  • 行数阈值:evaluator.rs 净增约 113 行(diff 推算),未登记 large-file-inventory;当前 head Test cosh-ng fast checks(含 layout gate)实测 SUCCESS,以 CI 实测为准,未越线。

剩余风险

  • cwd 本身位于 /proc//dev/sys 内部、或 $HOME 指向 //特殊目录时的拼写无法识别,属 doc 已声明的无 cwd/无 filesystem access 契约内的已知取舍。
  • symlink 别名不覆盖(已声明);字面名 proc/dev/sys 的普通目录被误伤面维持 R3/R4 已接受水平,可走交互审批。
  • CI 状态:当前 head 各门禁最新结果均 SUCCESS(Test cosh-ng 05:11:22、fast checks 04:35:01、release build 04:32:24、prebuilt 05:00:42、Commit Message Lint / PR Checks / license-cla 均绿);同 head 较早有一次 Test cosh-ng FAILURE(04:31:10)随后复跑转绿,失败原因未由本评审复核(前轮曾归因 provider_lifecycle 环境性超时),若再现需确认与本改动无关。

未跑验证(以 CI 实测为准)

  • 本评审不执行工具;阻断/归一化逻辑正确性为逐条静态推演,未实际编译运行(前两轮 T2 验证已在 8af1480 容器内实测通过,本轮 head 相对其仅 doc/rebase 级变化)。
  • 作者自述的 1404 lib 测试通过、fmt/clippy clean、lib/bin overlap 数值未独立复核,但对应门禁在当前 head 实测绿。
  • P3 的 PR body 修订无需代码验证。

@SunnyQjm

Copy link
Copy Markdown
Collaborator

PR number: #2708
head_sha: 736b551
verified_at: 2026-08-31T09:52:43Z
tier: T1

PR #2708 验证报告(T1)

1. 验证范围

  • HEAD 核对:git rev-parse HEAD = 736b5514e99885a8ef9c4aab5a52d242fa710d3c,与作业 head_sha 一致。
  • doctor:宿主 contended=false(load1m 3.24 / 12 核),container 运行时探测为 Apple container(docker 亦可用)。
  • Backend:container(Apple container,alinux3 固定镜像,profile cosh-container-arm64-2708)。host 未跑任何编译/测试。
  • 环境:env up RUN_ID 20260831T093503Z-45c12c68;结束后已 env down(RUN 20260831T094956Z-440b2b53,cleanup=cleaned)。
  • scope 圈定:作业所给 4 个过滤器覆盖全部 5 个变更文件的行为面,予以采用。test plan --base origin/main 因本地 origin/main 漂移(全仓 500+ 文件 diff)退化为 crate 级建议,不作为补全依据(已在报告注明)。
  • 已运行 scope(均为 cosh-lab test run --scope unit,实际等价于 cargo test --locked --package cosh-shell --lib <filter>):
过滤器 RUN_ID 结果
readonly_rules 20260831T093839Z-d3e51c96 34 passed / 0 failed
readonly_traversal_tests 20260831T094125Z-1ccb04c7 6 passed / 0 failed
readonly_broker 20260831T094221Z-73657921 9 passed / 0 failed
readonly_compound(首跑) 20260831T094230Z-1db93df5 16 passed / 2 failed
readonly_compound(串行复跑) 20260831T094335Z-c90b015c 16 passed / 2 failed(同 2 例)
  • 原生辅助命令(非测试执行):容器内 ps/cat /proc/1/comm 探针一次(经 cosh-lab env exec,用于失败归因)。

2. 结果分类

  • readonly_rules:通过(34/34,含 evaluator 既有回归全组)。lib 总数实测 1404(34+1370 过滤),与作者自报一致。
  • readonly_traversal_tests:通过(6/6)。本 PR 核心新增的 6 个穿越阻断用例(绝对穿越、raw 前缀 fail-closed、相对穿越、裸相对拼写、~ 弹出 fail-closed、不过度阻断)全绿。
  • readonly_broker:通过(9/9)。含本 PR 新增的 $'/proc/version' / $'/etc/shadow' metacharacter 向量钉所在用例 readonly_broker_blocks_shell_metas_and_mutation
  • readonly_compound:失败(非 flaky,环境性归因,与本 PR 无因果)
    • 首跑与串行复跑均为同样 2 例失败:executor_join_does_not_wait_for_descendants_holding_the_pipe(readonly_compound_tests.rs:532)与 executor_reaps_descendants_that_redirected_their_output(:632),断言"被组杀的 descendant 不得存活"。
    • 归因证据:容器探针显示 PID 1 是 sleep(不回收僵尸),两跑失败的 descendant PID(752、786、921、955)全部以 Z <defunct> 状态挂在 PID 1 下——即 executor 的 killpg(SIGKILL) 已生效杀死 descendant,只是僵尸未被 reaping,而测试用 kill(pid,0)==0 判存活,对僵尸返回成功导致 2 秒超时断言失败。
    • 与本 PR 无交互:该 2 例由既有提交 4bea98432 引入(base 既有);本 PR 未触碰 executor 实现(tools/readonly_compound.rs)及这两处断言;PR 在本文件的唯一改动(rule 5 新增 2 个 $'...' 向量)所在用例 build_plan_fails_closed_for_ineligible_shapes 两跑均 ok

3. Findings 验证

  • [P3] PR body 与 head 行为面不一致:证实(实质成立)。代码侧事实全部核实:head 为 6 个新 traversal 用例(实测 6 passed)+ broker/compound 两组 $ 向量钉(实测通过),lib 总数实测 1404,确与 finding 引用的 PR body 旧表述"1343 (baseline 1340 + 3 new)"及"exactly lexically resolve into"矛盾(实现实为 cwd 无关 fail-closed 超集)。PR body 文本本身未经独立 GitHub 访问复核(本流程禁网络/gh),以 finding 引用为准。文档级、不阻塞,建议合并前更新 body。
  • R1 P1 / R3 P1(相对 .. 穿越、裸相对拼写、~ 弹出)维持修复:证实。diff 精读:first_component_targets_special_dir(evaluator.rs:431-447)无 traversed 前提、跳过前导 .. 链后首段为 dev|proc|sys 即阻断;normalize_path_lexically(:465 起)对前导 ~.. 弹返回 None、上层 fail-closed。6 个 traversal 用例(含全部上述向量及 /proc/../etc/hostname raw 前缀次序钉)容器实测全绿。
  • R3 P2 / R4 $ 分工测试钉维持修复:证实。broker.rs:133-140 与 readonly_compound_tests.rs:89-93 均在 diff 中,对应用例两轮实测通过。
  • R4 P2(compound 分词假设)已闭环:无法独立重新验证。属前轮 T2 容器探针范围,本轮 T1 未重做分词探针;无回退迹象($ 向量钉实测通过),以既有结论为准。
  • "相对 8af1480 仅 doc/rebase 级变化":证实且更强git diff 8af14801e 736b5514e -- <5 个 PR 文件>(逐字节一致,含 lib.rs,连 finding 所述"挂载行号微移"亦不存在);两 commit 间全部差异为 52 个 docs/design/acp-task-platform/ 文件删除(rebase 基线移动),src/ 零差异。
  • 结构评审确认项:证实关键项。lib.rs:41-43 为 #[cfg(test)] #[path=...] mod readonly_traversal_tests; 私有挂载;is_blocked_special_path 既有 pub 签名不变(evaluator.rs:402),新增函数均 private;仅 std 字符串操作,无新依赖。layout/行数 gate 未独立运行(以 CI 为准)。
  • 剩余风险段(cwd 在 /proc 内、$HOME=/、symlink 别名不覆盖):与代码一致。evaluator.rs 新增 doc comment 明确记载这些契约边界,finding 描述属实。

4. 补充测试

无(T1 作业,未新增测试)。

5. 证据索引

  • 无截图(T1 无 PTY/渲染验证,evidence_dir 未写入文件)。
  • cosh-lab run 目录(含 result.json / commands.log / report.md):
    • .cosh-lab/runs/20260831T093503Z-45c12c68(env up)
    • .cosh-lab/runs/20260831T093839Z-d3e51c9620260831T094125Z-1ccb04c720260831T094221Z-7365792120260831T094230Z-1db93df520260831T094335Z-c90b015c(4 个 scope,含复跑)
    • .cosh-lab/runs/20260831T094956Z-440b2b53(env down)

6. 未运行 scope

  • cosh-shell crate 全量(--lib 无过滤器)、logic / protocol / raw-cli / shell-host / workspace 级测试。
  • fmt / clippy / layout gate(T1 scopes 未含;CI 为合入门禁权威)。
  • host 侧未执行任何 PR 编译与测试(纪律要求)。

7. 结论

Go。PR 引入的行为面(穿越阻断 6 新例 + $ 分工钉 + readonly_rules 回归)focused 实测全绿,且 5 个 PR 文件与已两轮 T2 实测的 8af1480 逐字节一致;readonly_compound 的 2 例稳定失败已实锤归因容器 PID1 不回收僵尸的环境性问题,为 base 既有、与本 PR 无因果。静态评审结论(无 blocking finding、P3 文档级)无需修正。

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[cosh-ng] bug: is_blocked_special_path 纯字符串前缀匹配,路径穿越与 $'...' ANSI-C quoting 可绕过 readonly_rules 黑名单

2 participants