Skip to content

[BUG][P2] 围栏内 setuid 二进制(ps/top/su)不可 exec —— 真实代价,但仓里零登记 #1149

Closed

Description

@jinjunnn

用户可观察的结果

沙箱围栏开启后,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

  1. self-assigned this
    on Aug 27, 2026
  2. jinjunnn commented on Aug 27, 2026

    @jinjunnn
    OwnerAuthor

    更正:我这张票面写错了三处,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 逐行看过,纯注释)。
    • AC2 双向变异,三个方向:
      ①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
      —— 空输出没有被读成「拦住了」。第 ③ 条是我要求的那条,做到了。
    • 门:ext 161 pass / 15 files(base 157/14,+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 ——
    要改另开票,不在本票欠账。

  3. added a commit that references this issue on Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions