RETEST3 三个 cell 新缺陷的修复方向与实现位置记入 §6: 起步门 + init pid 轮询、volatile 空根只读绑(/app、CA、 单元专属 resolv.conf/hosts;不绑 nsswitch.conf 的原因)、 唯一身份分配 + broker 多 cell 对照、执行器记 body。
15 KiB
15 KiB
对齐 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 |
| 浏览器 | Playwright headless-shell 由 agent 在客机内直接跑,能读原始 DOM、能跑 JS | 浏览器任务由专门浏览器子代理完成,只见无障碍树快照,不见原始 DOM,不能在页面上下文跑 JS,CDP 由单元外 broker 管理 | chv-browser(客机侧、单元外独立进程,用户 chv-browser):Unix socket /run/chv-browser/browser.sock,操作只有 `open |
| 凭证 | /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. 诚实边界(本轮不宣称的东西)
- 没有独立根文件系统:单元与客机共享根文件系统(只读挂载 + 遮蔽 +
bind),不是 Muse 的独立 Debian 镜像。一个有决心的单元内进程仍能读到
客机侧 world-readable 的文件内容(如
/app/host.py、系统库)。真正 的文件系统分离要等下一阶段的独立根文件系统 + 体积重预算。 - SO_PEERCRED 的 uid 检查不区分单元 root 与客机
chv:单元映射--private-users=1000:65536让单元内 uid 0 在客机侧就是 uid 1000, 与客机chv同 uid。broker 的主校验是 pid ns 对照(cell.pid), uid 范围检查只是第二道;cell.pid不存在时只做 uid 检查,并打日志。 这不是“单元身份”的密码学证明,是命名空间对照。 - a11y-tree-only 是方向,不是已封死的强制:浏览器 socket 不暴露 DOM/JS 操作,且单元内跑不起 playwright(已遮蔽),但单元内进程仍 有 curl/Python 标准库,可以直接发 HTTP 请求取页面原文——那不是 “浏览器 DOM 访问”,但也不是无障碍树。完整封堵(出口只走浏览器 子代理)是 P3 Sentinel 的事。
- 性能数字全是待验证目标:单元启动耗时、veth+NAT 对模型调用延迟 的影响、快照截断后的 token 占用——本轮无真机验证(第三棒 V-1~V-9 占着 WSL 测试机),一律记入“真机待验证清单”,不在代码/文档里写成 已测得。
- 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 的实际行为,须真机验证。 - 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 + 分叉失败关闭)。
- 单元隔离验收(新增,否定测试为主):
- 单元内
cat /etc/chv/guest-env失败(遮蔽 + 0600 overflow uid)。 - 单元内
cat ~/.git-credentials失败;经 brokersync status成功。 - 单元内写
/usr/local/bin/x失败(只读根文件系统)。 - 单元内
nsenter/ptrace 客机侧进程失败(pid ns + dropped caps)。 - 含
io_uring_setup的程序在单元内报 EPERM/ENOSYS(syscall 过滤)。 - 单元内
python3 -c "import playwright"失败(已遮蔽);经 brokerbrowser open https://example.com返回标题 Example Domain 与 无障碍树快照。 - 单元内 codex 一轮 turn 完成(
danger-full-access下,不再套 bwrap)。 - 单元内出站到模型网关通(veth + NAT);
chv-cell退出后 NAT 规则已清理。 - 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 实测有效),但引出三个阻塞缺陷,本轮按报告方向修:
- 启动竞态:
run_cell原顺序是 Popen → 配网 → 单次找 init pid, 快 payload 在配网完成前退出即撞墙(run -- true11 次只成 6 次)。 修:payload 包一层 sh 起步门(等/run/chv/.cell-gate-<machine>出现再 exec),配网完成后才开门;find_cell_init_pid改带超时 轮询;nspawn 已死时返回它的退出码而不是抛错;veth 等待放宽到 20 秒。验收:连续 15 次run -- true计数(cellrace4)。 - 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)。 - 并发 cell 撞毁:固定
ve-chv-cell名 + 固定 10.200.0.1/24, 第二个 cell 在 nspawn 建 veth 时即File exists。修:每次 run_cell 在锁下分配唯一身份(CellNet:machinechv-cell-<xx>、ve-chv-cell-<xx>、10.200.<xx>.0/24;machine 名 11 字符, ve- 前缀后 14 字符,符合接口名 15 字符上限;扫描活接口 + 地址- machinectl 在册名避让注销滞后)。broker 的 pid ns 对照改成
多 cell 版(
cell-<machine>.pidglob + 兼容旧cell.pid,对端 ns 与任一活单元同 ns 即放行;旧单文件仍由 run_cell 写最新单元, P2-9 驱动仍读它)。
- machinectl 在册名避让注销滞后)。broker 的 pid ns 对照改成
多 cell 版(
另:执行器 _boot_vm 的 vm.create/vm.boot 失败消息现在带截断后
的响应体(_truncate_api_body,500 字符;只留 status 即无法定位
间歇 500 的根因,两轮已提)。