RETEST3 三个 cell 新缺陷的修复方向与实现位置记入 §6: 起步门 + init pid 轮询、volatile 空根只读绑(/app、CA、 单元专属 resolv.conf/hosts;不绑 nsswitch.conf 的原因)、 唯一身份分配 + broker 多 cell 对照、执行器记 body。
163 lines
15 KiB
Markdown
163 lines
15 KiB
Markdown
# 对齐 Muse P2 差距表:客机 nspawn 单元 + 浏览器子代理独立
|
||
|
||
依据:`~/workspace/research_notes/meta-muse-ai-architecture-20261101-0755/report.md`
|
||
(官方以 Meta security.muse.ai 2026-09-08 为准;第三方拆解只作参考,
|
||
逆向细节不得写成官方架构)。Muse 模型一句话:VM 内 systemd-nspawn
|
||
运行时单元(Hatch)跑代理,单元 root 映射为 host 侧非特权用户,自有
|
||
虚拟网络接口,过滤 syscall(如禁用 io_uring),受限能力(如无
|
||
CAP_SYS_PTRACE、无 CAP_NET_ADMIN);浏览器任务由专门浏览器子代理
|
||
完成,只见页面无障碍树快照、不见原始 DOM,不能在页面上下文跑 JS,
|
||
CDP 由单元外 broker 管理。
|
||
|
||
本轨范围只限 P2:客机镜像层——systemd-nspawn 运行时单元 + 浏览器
|
||
子代理独立。不碰 P1(执行器 WarmPool/气球/vsock,常驻 VM)、不碰 P3
|
||
(authd 替身凭证、Sentinel 式出口审批)。分支 `feat-chv-muse-p2`
|
||
(从 `feat-chv-cover100` 分出,基线 HEAD `06c3e17`)。
|
||
|
||
## 1. 现状 vs Muse
|
||
|
||
| 项 | 现状(实测/代码为准) | Muse 目标 | 本轨做法 |
|
||
|---|---|---|---|
|
||
| 代理运行位置 | codex 直接跑在客机用户态(执行器经 SSH 以 `chv` 身份调 `python3 /app/host.py`,它再起 `codex app-server`);无内层隔离 | 代理跑在 systemd-nspawn 运行时单元内 | `chv-cell run -- python3 /app/host.py --state … --codex-home … <cmd>`:单命令单元(`--as-pid2`,不 boot 第二个 systemd);单元即沙箱 |
|
||
| 单元 root 映射 | 无此概念(`chv` 就是普通用户) | 单元 root 映射为 host 侧非特权用户 | `--private-users=1000:65536`:单元内 uid 0 → 客机侧 `chv`(uid 1000)。效果:workspace/`~/.codex`(`chv` 所有)单元内仍可写;host-root 0600 文件(如 `/etc/chv/guest-env`)在单元内显示为 overflow uid、不可读;再加 `--inaccessible=/etc/chv` 纵深防御 |
|
||
| 单元根文件系统 | 无(直接用客机根文件系统) | 单元自有根文件系统(完整 Debian 镜像),与 host 系统分离 | **本轮不做独立根文件系统**:单元复用客机根文件系统(`--directory=/`)。独立 minbase 根文件系统约 200–300MB,另要把 codex/playwright 依赖再装一份,会顶破 qcow2 ≤1GB 验收。文件隔离本轮是“读多写少 + 遮蔽”:`/usr`、`/app` 等 host-root 所有路径单元内只读不可写,`/etc/chv`、playwright 包与浏览器二进制 `--inaccessible` 遮掉,可写路径只有显式 bind(workspace、`~/.codex`、`/run/chv`)与 tmpfs(`/tmp`)。完整独立根文件系统列为下一阶段,需先重定镜像体积预算 |
|
||
| syscall 过滤 | 无 | 过滤 syscall,如禁用 io_uring | `--system-call-filter=@system-service ~io_uring_setup ~io_uring_enter ~io_uring_register` |
|
||
| 能力 | 客机 `chv` 是普通用户,但 codex 的 bwrap 沙箱在单元内另有问题(见下) | 无 CAP_SYS_PTRACE、无 CAP_NET_ADMIN | `--drop-capability=CAP_SYS_ADMIN,CAP_SYS_PTRACE,CAP_NET_ADMIN,CAP_NET_RAW,CAP_SYS_MODULE,CAP_SYS_RAWIO,CAP_SYS_BOOT,CAP_MKNOD` + `--no-new-privileges` |
|
||
| 单元内 codex 沙箱 | `host.py` 默认 `sandbox="workspace-write"`(bwrap) | 单元本身就是边界 | 单元内 codex 用 `danger-full-access`:bwrap 的 `--proc /proc` 挂载在嵌套命名空间里是已知走不通的形状(见 AGENTS.md 内层 proc 定位),单元内再套 bwrap 只会假隔离。执行器侧改 `--sandbox` 透传是 P1 的文件,本轨只定义接口(见 §4),不改 `executor.py` |
|
||
| 单元网络 | 无(与客机共享网络命名空间) | 单元自有虚拟网络接口 | `--network-veth`:宿主侧 `ve-chv-cell`(10.200.0.1/24)、单元侧 `host0`(10.200.0.2/24,默认路由经 10.200.0.1),客机侧 NAT 出站(MASQUERADE 走客机默认路由的上行口,动态发现,不写死口名)。单元内配网经 nsenter 进单元 init 的 netns 做(`chv-cell.py` 内实现)。veth 建不起来即大声失败,不静默回落共享网络;调试逃生口 `CHV_CELL_NETWORK=shared` 须显式设置、打日志,绝不默认 |
|
||
| 单元内外 IPC | 无(SSH 直达,host key 钉死) | Unix socket + SO_PEERCRED + 对端 ACL 的最小权限 IPC | `chv-broker`(客机侧、单元外):Unix socket `/run/chv/broker.sock`(bind 进单元)。SO_PEERCRED 校验:对端 uid 须在单元映射范围 1000–66535,且对端 pid 的 pid ns 须与 `/run/chv/cell.pid` 记的单元 init 同 ns(cell.pid 不存在时只做 uid 检查,并打日志)。操作白名单:`ping`、`sync pull|push|status`(仓目录只允许 `~/workspace` 下,`chv-sync` 以 `chv` 身份跑,不进 shell)、`browser …`(转发给浏览器子代理)。其余一律拒绝 |
|
||
| 浏览器 | Playwright headless-shell 由 agent 在客机内直接跑,能读原始 DOM、能跑 JS | 浏览器任务由专门浏览器子代理完成,只见无障碍树快照,不见原始 DOM,不能在页面上下文跑 JS,CDP 由单元外 broker 管理 | `chv-browser`(客机侧、单元外独立进程,用户 `chv-browser`):Unix socket `/run/chv-browser/browser.sock`,操作只有 `open|snapshot|screenshot|close`。`open`/`snapshot` 返回标题、URL 与 Playwright `Page.aria_snapshot()` 的 ARIA 快照字符串(截断到 200 节点行/64KB);`screenshot` 落盘到 `/run/chv/shots/`(bind 进单元,单元只读路径取图)。**不暴露** `evaluate`、`content`(原始 DOM)、DevTools。单元内 playwright 包与浏览器二进制用 `--inaccessible` 遮掉,单元内跑不起自己的浏览器,只能走 broker |
|
||
| 凭证 | `/etc/chv/guest-env`(0600 root)+ `~/.git-credentials`(0600 chv),codex 在客机内直接可读后者 | 凭证由 authd 保管,代理只见替身令牌 | 本轨只做“单元看不见”:`/etc/chv` 与 `~/.git-credentials` 都 `--inaccessible`,同步走 broker 的 `sync` 操作(broker 在单元外跑 `chv-sync`,token 不进单元)。替身化本身是 P3,不在本轨冒充已做 |
|
||
| 客机启动同步 | `chv-sync boot` 由 cloud-init runcmd 以 root 跑,clone 到 `~/workspace` | — | 不动:boot clone 仍在单元外做;单元内只经 broker 做 pull/push/status |
|
||
|
||
## 2. 改了什么(本轨交付物)
|
||
|
||
- `chv-guest/chv-cell.py`(新,标准库):单元启动器。`build_nspawn_argv()` 纯函数产
|
||
nspawn 参数表(单元映射、能力丢弃、syscall 过滤、遮蔽、bind、tmpfs、veth、
|
||
`--as-pid2`、`CHV_CELL=1`);`configure_cell_network()` 产配网命令表(宿主侧
|
||
ve 接口 + NAT + nsenter 进单元配 host0);`chv-cell run -- <cmd>` 起单元、
|
||
写 `/run/chv/cell.pid`、等退出码、清理 NAT 规则;`chv-cell argv` 只打印
|
||
参数表、不启动(干检查用)。
|
||
- `chv-guest/chv-broker.py`(新,标准库):单元外 IPC broker。Unix socket
|
||
`/run/chv/broker.sock`,SO_PEERCRED + pid ns 双重校验,白名单操作见 §1,
|
||
`sync` 调 `chv-sync`(`runuser -u chv`,list argv、不进 shell),`browser`
|
||
转发 `chv-browser.sock`。
|
||
- `chv-guest/chv-browser.py`(新,标准库 + playwright 懒加载):浏览器子代理。
|
||
Unix socket `/run/chv/browser.sock`,操作 `open|snapshot|screenshot|close`,
|
||
只返回无障碍树快照(截断)+ 标题/URL/截图路径;无 `evaluate`/`content`。
|
||
- `chv-guest/build.sh`:apt 加 `systemd-container`、`iproute2`、`iptables`;
|
||
拷入 `chv-cell.py`/`chv-broker.py`/`chv-browser.py`(`--copy-in` + chmod);
|
||
建系统用户 `chv-browser`;装 `chv-broker.service`、`chv-browser.service`
|
||
并 `systemctl enable`;`/etc/tmpfiles.d/chv.conf` 建 `/run/chv` 与
|
||
`/run/chv/shots`。P1 交接:`cloud/chv/guest/vsock_notify.py` 若在仓库里
|
||
存在则一并拷入(条件式,不存在时只警告、不失败——P1 文件在本分支尚无,
|
||
合并后才有)。
|
||
- `chv-guest/buildinfo.py`:`BASE_CONTENTS` 加 `systemd-container`、
|
||
`chv-cell`、`chv-broker`、`chv-browser`;`build()` 支持可选 `extra_contents`
|
||
(vsock 接入时用,不改现有调用签名)。
|
||
- `cloud/tests/test_chv_muse_p2.py`(新):单元参数表、broker 校验与白名单、
|
||
浏览器 API schema 与快照截断、build.sh 干跑含新步骤、buildinfo 新内容项。
|
||
全部用假 socket/假进程,绝不连真实网络、不起真实 VM。
|
||
- `docs/chv-production-readiness.md`:补 P2 一节(单元隔离验收与待验证项),
|
||
旧定案不删、只追加。
|
||
|
||
## 3. 诚实边界(本轮不宣称的东西)
|
||
|
||
1. **没有独立根文件系统**:单元与客机共享根文件系统(只读挂载 + 遮蔽 +
|
||
bind),不是 Muse 的独立 Debian 镜像。一个有决心的单元内进程仍能读到
|
||
客机侧 world-readable 的文件内容(如 `/app/host.py`、系统库)。真正
|
||
的文件系统分离要等下一阶段的独立根文件系统 + 体积重预算。
|
||
2. **SO_PEERCRED 的 uid 检查不区分单元 root 与客机 `chv`**:单元映射
|
||
`--private-users=1000:65536` 让单元内 uid 0 在客机侧就是 uid 1000,
|
||
与客机 `chv` 同 uid。broker 的主校验是 pid ns 对照(`cell.pid`),
|
||
uid 范围检查只是第二道;`cell.pid` 不存在时只做 uid 检查,并打日志。
|
||
这不是“单元身份”的密码学证明,是命名空间对照。
|
||
3. **a11y-tree-only 是方向,不是已封死的强制**:浏览器 socket 不暴露
|
||
DOM/JS 操作,且单元内跑不起 playwright(已遮蔽),但单元内进程仍
|
||
有 curl/Python 标准库,可以直接发 HTTP 请求取页面原文——那不是
|
||
“浏览器 DOM 访问”,但也不是无障碍树。完整封堵(出口只走浏览器
|
||
子代理)是 P3 Sentinel 的事。
|
||
4. **性能数字全是待验证目标**:单元启动耗时、veth+NAT 对模型调用延迟
|
||
的影响、快照截断后的 token 占用——本轮无真机验证(第三棒 V-1~V-9
|
||
占着 WSL 测试机),一律记入“真机待验证清单”,不在代码/文档里写成
|
||
已测得。
|
||
5. **nspawn 参数以 systemd 255(`systemd-nspawn --help` 实测)为准**:
|
||
`--private-users=UIDBASE[:NUIDS]`、`--drop-capability`、
|
||
`--system-call-filter`、`--inaccessible`、`--network-veth`、
|
||
`--as-pid2` 均在本机 help 里确认存在;但在 CHV 客机内核(云镜像
|
||
generic 内核)里 userns/veth/idmap 的实际行为,须真机验证。
|
||
6. **WarmPool/常驻路径与本轨无关**:P1 的常驻 VM 若接进来,单元在常驻
|
||
VM 内同样起(`chv-cell run` 与 VM 生命周期无关);本轨不改执行器,
|
||
不碰 P1 的文件。
|
||
|
||
## 4. 与 P1/P3 的接口(不碰对方文件,只定契约)
|
||
|
||
- **P1 → P2(执行器接单元)**:执行器调客内 host.py 的命令从
|
||
`python3 /app/host.py …` 改为
|
||
`chv-cell run -- python3 /app/host.py --state /home/chv/.codex/host-state.json --codex-home /home/chv/.codex --sandbox danger-full-access …`。
|
||
`--sandbox danger-full-access` 只在单元内生效(单元即沙箱);非单元
|
||
路径(本地/Docker/冷启动回落)保持原沙箱不动。改动在 `executor.py`,
|
||
由 P1 做,本轨只提供客机侧 `chv-cell` 命令与本节契约。
|
||
- **P1 → P2(vsock 进镜像)**:P1 的 `cloud/chv/guest/vsock_notify.py`
|
||
由本轨 `build.sh` 条件拷入(§2),合并后自动生效;P2 不改该脚本内容。
|
||
- **P2 → P3(broker 预留)**:broker 的 `sync` 操作现在单元外直接跑
|
||
`chv-sync`(token 仍在客机侧落盘);P3 做 authd 替身化时,只改 broker
|
||
内部的凭证获取方式(调 authd 拿短期替身),不改 broker 的 socket 协议
|
||
与白名单。浏览器出口审批(只允许经 `chv-browser` 出站)同样是 P3 的
|
||
Sentinel 接管点,本轨只做到“单元内无浏览器可跑”。
|
||
|
||
## 5. 真机待验证清单(round 11,测试机空闲后)
|
||
|
||
- lite 验收照旧:qcow2 ≤1GB、冷启动到 SSH、codex 版本、headless-shell
|
||
例、Gitea 双向同步(boot clone + push + pull + 分叉失败关闭)。
|
||
- 单元隔离验收(新增,否定测试为主):
|
||
1. 单元内 `cat /etc/chv/guest-env` 失败(遮蔽 + 0600 overflow uid)。
|
||
2. 单元内 `cat ~/.git-credentials` 失败;经 broker `sync status` 成功。
|
||
3. 单元内写 `/usr/local/bin/x` 失败(只读根文件系统)。
|
||
4. 单元内 `nsenter`/ptrace 客机侧进程失败(pid ns + dropped caps)。
|
||
5. 含 `io_uring_setup` 的程序在单元内报 EPERM/ENOSYS(syscall 过滤)。
|
||
6. 单元内 `python3 -c "import playwright"` 失败(已遮蔽);经 broker
|
||
`browser open https://example.com` 返回标题 Example Domain 与
|
||
无障碍树快照。
|
||
7. 单元内 codex 一轮 turn 完成(`danger-full-access` 下,不再套 bwrap)。
|
||
8. 单元内出站到模型网关通(veth + NAT);`chv-cell` 退出后 NAT 规则已清理。
|
||
9. broker 拒绝非单元进程的连接(pid ns 不符时拒绝;`cell.pid` 缺失时
|
||
只做 uid 检查并打日志——该降级须在日志里可见)。
|
||
- 性能记录(只记录、不设门):单元启动到 codex 就绪耗时、veth+NAT 下
|
||
模型调用往返、qcow2 实际占用(systemd-container 与新脚本的增量)。
|
||
|
||
## 6. round-11 part 4 补记(2026-11-07,RETEST3 的 3 个 cell 新缺陷)
|
||
|
||
RETEST3 证实 `--volatile=yes` + 只遮 /usr 的形状本身生效(遮蔽经
|
||
mount/ls 实测有效),但引出三个阻塞缺陷,本轮按报告方向修:
|
||
|
||
1. **启动竞态**:`run_cell` 原顺序是 Popen → 配网 → 单次找 init pid,
|
||
快 payload 在配网完成前退出即撞墙(`run -- true` 11 次只成 6 次)。
|
||
修:payload 包一层 sh 起步门(等 `/run/chv/.cell-gate-<machine>`
|
||
出现再 exec),配网完成后才开门;`find_cell_init_pid` 改带超时
|
||
轮询;nspawn 已死时返回它的退出码而不是抛错;veth 等待放宽到
|
||
20 秒。验收:连续 15 次 `run -- true` 计数(cellrace4)。
|
||
2. **volatile 空根缺文件**:单元根是基本空的 tmpfs,`/app`、
|
||
`/etc/resolv.conf`、`/etc/hosts`、`/etc/ssl/certs` 全无,执行器
|
||
单元路径一律 ENOENT、DNS/TLS 不可用。修:`--bind-ro=/app` 与
|
||
`--bind-ro=/etc/ssl/certs` 固定绑;resolv.conf/hosts 由 run_cell
|
||
按客机有效 DNS 生成单元专属文件(`/run/chv/resolv.conf.<machine>`、
|
||
`hosts.<machine>`,0644)再只读绑——客机 resolv.conf 可能是
|
||
systemd-resolved stub(127.0.0.53),单元内无 resolved,直接绑
|
||
即 DNS 全灭。不绑 nsswitch.conf(客机版可能含 resolve 模块,
|
||
glibc 默认 files+dns 正是单元需要的)。P2-8 的 example 腿换
|
||
v4 目标(gitea.tose.top)。
|
||
3. **并发 cell 撞毁**:固定 `ve-chv-cell` 名 + 固定 10.200.0.1/24,
|
||
第二个 cell 在 nspawn 建 veth 时即 `File exists`。修:每次
|
||
run_cell 在锁下分配唯一身份(CellNet:machine `chv-cell-<xx>`、
|
||
`ve-chv-cell-<xx>`、10.200.`<xx>`.0/24;machine 名 11 字符,
|
||
ve- 前缀后 14 字符,符合接口名 15 字符上限;扫描活接口 + 地址
|
||
+ machinectl 在册名避让注销滞后)。broker 的 pid ns 对照改成
|
||
多 cell 版(`cell-<machine>.pid` glob + 兼容旧 `cell.pid`,对端
|
||
ns 与任一活单元同 ns 即放行;旧单文件仍由 run_cell 写最新单元,
|
||
P2-9 驱动仍读它)。
|
||
|
||
另:执行器 `_boot_vm` 的 vm.create/vm.boot 失败消息现在带截断后
|
||
的响应体(`_truncate_api_body`,500 字符;只留 status 即无法定位
|
||
间歇 500 的根因,两轮已提)。
|