## 用户可观察的结果 沙箱围栏开启后,agent 在 shell 工具里跑 **`ps` / `top` / `su`** 会拿到 `Operation not permitted`。这类命令在排查场景里很常用(「看看什么进程占着端口」), 而失败信息不解释原因,看起来像工具坏了。 ## 实测(2026-08-26,`ac#1144` 的打包取证顺带量到) 证据:`docs/verification/2026-08-26-req138-1144-packaged-shell-tool-chain/results/setuid-exec-observation.json` | 二进制 | mode | setuid | 围栏内 exec | 无围栏对照 | | --- | --- | --- | --- | --- | | `/bin/ps` | `0o4755` | 是 | **拒绝** | 通过 | | `/usr/bin/top` | `0o4555` | 是 | **拒绝** | 通过 | | `/usr/bin/su` | `0o4755` | 是 | **拒绝** | 通过 | | `/bin/ls` | `0o755` | 否 | 通过 | 通过 | | `/usr/bin/wc` | `0o755` | 否 | 通过 | 通过 | | `/usr/bin/git` | `0o755` | 否 | 通过 | 通过 | **这不是本仓 profile 的规则造成的** —— profile 只写了 `(allow default)` + `(deny file-write*)`。 是 **macOS seatbelt 对「沙箱进程 exec setuid 二进制」的固有拒绝**。 非 setuid 的对照组(`ls`/`wc`/`git`)在两臂都通过,所以判据能分辨,不是「围栏内什么都跑不了」。 ## 为什么值得单独一票 `REQ-138`(`#1074`)已验收关闭,其误伤集 9 条(`git init+commit` / `node` 写 TMPDIR / 读仓库 / `curl` / `mkdir -p` / `grep` / `which` …)**没有覆盖 setuid 这一类**。 ⇒ 这是一条**已知但未登记**的代价:它今天就在生产行为里,而仓里没有任何东西写着它。 ## AC - **AC1** — setuid 二进制在围栏内不可 exec 这件事,在仓内有**单一权威**的登记 (要么进误伤集并作为已知代价断言、要么在用户可见处解释),不再只存在于一份 verification 文档里。 - **AC2** — 该登记对已知的坏能翻红:若将来 profile 或 wrapper 改动**意外恢复**了 setuid exec (或反过来把非 setuid 的 `ls`/`git` 也拦了),判据必须红。 ## 需要裁的一点(不预判) **要不要让用户看得懂?** 今天的失败是裸 `Operation not permitted`。候选: (a) 只登记为已知代价,不改用户可见行为(最小); (b) 在 shell 工具的错误路径上,对 setuid 目标补一句可读解释。 (b) 会碰生产错误路径,超出「登记」范畴 —— 由 owner 裁,实现方不要自行扩。 ## Out of scope - 放宽围栏让 setuid 可 exec(那会削弱 REQ-138 的保证); - 网络轴(见 `#1077` 与 `docs/architecture/2026-08-25-network-egress-seam.md`)。 ## 复杂度 **S**(登记 + 一条判据)~ **M**(若裁 (b))。 ## 出处 `ac#1144` 打包沙箱取证(2026-08-26)。该票的 §6 明写「这一格不判 PASS/FAIL —— 围栏本身的行为 按票面属于 Out of scope」,所以另开本票承载。
Activity
[ac#1149] 围栏内 set-ID 二进制不可 exec:登记为已知代价 + 双向判据 (#1152)
jinjunnn commentedon Aug 27, 2026
更正:我这张票面写错了三处,lane 动笔前实测推翻了
票是我写的,三处都错在「我照抄了一份取证 JSON,没自己跑」。记在这里免得以后有人照票面复现。
① 类别写窄了:不是 setuid,是 set-ID
票名与正文通篇写
setuid。实测/usr/bin/write=0o2555(setgid-only,setuid 位为 0)在围栏内同样被拒。
⇒ 按
0o4000分类会把它归进「该通过」那一类,制造一条假红。判据取
S_ISUID | S_ISGID(0o6000),已按此实现。② 拒绝签名是
rc=127,不是我抄的126证据 JSON 里那一跑用的是
/bin/sh;生产 wrapper exec 的是真 shell(zsh),签名不同。我把一个特定驱动下的观测当成了这一类的通用签名。
③
su不能进语料 —— 会挂死测试有控制终端时
su会Password:阻塞读/dev/tty(piped stdin 拦不住),于是无围栏对照臂会真 exec 它并挂住。实测:
script -q /dev/null下 5 s 未返回,只能 SIGKILL;无 tty 时 15 ms 返回。
⇒ 「我这儿跑着没事」不是证据 —— 它取决于跑的人有没有 tty。而我在票面表格里把
su列进了语料。另:合成 setuid 探针这条路是死的 ——
cp /bin/echo x && chmod 4755 x后 mode 实测仍0o755,且副本两臂都
rc=137/SIGKILL。交付(我已独立复核)
packages/ext/src/alpha-sandbox-setid-exec.test.ts。成员资格从盘上真实 mode 派生(
expectationFor(mode)),不是写死路径清单 ⇒ 换机器/换 OS 版本不会假绿。生产侧
shell-sandbox.ts只加了一段「已知代价 K1」注释,明确不在那里枚举成员,指向判据文件—— 生产行为一行没改(diff 逐行看过,纯注释)。
①wrapper 去掉
sandbox-exec⇒ 4 条 set-ID 行exec-denied → exec-allowed,红;还原绿。②profile 加
(deny process-exec …)拦ls/git⇒ 只有那两行翻,wc与 set-ID 四行不动,红;还原绿。③观测手段自检:profile 写成解析不了 ⇒ set-ID 四行报
unmeasured而不是exec-denied—— 空输出没有被读成「拦住了」。第 ③ 条是我要求的那条,做到了。
+4/+1恰为新文件);ui-mac 4506/313/0 与 base 逐字相同;gate-files 162 全绿;docs 5 links/1 file;其余全 exit 0。
与 base fail-set 的差:空。
范围裁决留痕
票面「需要裁的一点」的候选 (b)(在
tool/shell.ts错误路径补可读解释)我裁为 out of scope:AC1 原文是「要么进误伤集并作为已知代价断言、要么在用户可见处解释」,(a) 已满足 AC;
(b) 要碰生产错误路径,属范围扩张。用户今天看到的仍是裸
Operation not permitted——要改另开票,不在本票欠账。
release: ship alpha-code 0.1.4 (#1158)