ci(bun-test): 补 bubblewrap 安装步骤,并修好它暴露的三个既有失败 - #1304
Conversation
`test` 腿有一步 best-effort 安装 bubblewrap 并放开 userns 限制, `bun-test` 腿没有。job 之间不共享文件系统,所以这一步必须各写各的, 否则 bun-test 完全靠 runner 镜像自带 bwrap——抽到不带的就红。 后果不只是"少测了点东西",而是**红得像代码回归**:bwrap 缺席时 `prepareDirectSandbox()` 返回 null,test/sandbox-shim-compiled-form.test.ts 里的 `overlayTargets()` 直接返回 `[]`,4 个断言 `toEqual([launcher])` 必挂。⚠️ 与 vitest 不同,`bun test` 这边没有宿主级 skip gate:那组用例的 `describe.skipIf` 只挡非 linux,在 linux runner 上照跑,bwrap 一缺就只能失败。 实测: - 同一段 commit 范围的两次 CI,一次日志里 `bwrap missing` 出现 5 次并失败, 另一次 0 次并通过——同代码同 job 定义,差的只是 runner 状态 - 本机对照:bwrap 可用 17 pass / 0 fail;把 bwrap 从子进程 PATH 藏掉 13 pass / 4 fail(与 CI 失败形态逐字一致) - 该失败与 bun 版本无关:1.4.1 与 1.4.2 在藏掉 bwrap 时同样 13/4 - 步骤内容与 `test` 腿的那份逐字节相同(yaml 解析后对比 IDENTICAL) 注意这**不能让 bun-test 必然全绿**:该腿另有一个独立成因, test/tmux-startup-storm-recovery.test.ts 会撞 720s 单文件墙被 SIGKILL (master 738da7f 上 bwrap 正常仍因它单独红)。那个另算,不在本 PR 范围。
#1304 给 bun-test 腿补上 bubblewrap 之后,三个此前静默 skip 的沙箱套件 第一次真正执行,随即暴露三个既有缺陷。三个是三种不同的病,逐个修: ① + ② fs-policy-bwrap.e2e / plugin-mcp-sandbox —— 同一个清理 bug 沙箱把 deny-mask 源 chmod 成 0o000(设计如此:让非 root 列不出内容)。 但 0o000 的**目录不能被穿越**,而 rm -r 必须穿越进去才能 unlink,于是 rmSync(force:true) 抛 EACCES —— force 只吞 ENOENT,不吞 EACCES。 为什么长期没暴露:root 有 CAP_DAC_OVERRIDE 直接绕过 DAC,删得掉;只有 普通 uid 会挂,而 GitHub runner 正是普通 uid。实测 Node 与 Bun 表现 完全一致(都 EACCES)⟹ 这是清理 bug,不是运行时差异,不能用 bun-only skip 掩盖。表现为「所有真实用例全绿、却挂在一个 unnamed 钩子上」。 修法:删除前深度优先 chmod-then-descend 恢复穿越权限(0o000 目录必须 先 chmod 才能 readdir,所以顺序不能反),抽成 test/helpers/rm-sandbox-scratch.ts 供两处共用;用 lstat 避免顺着符号 链接走出 scratch 树。 ③ session-store-bwrap —— Bun 下这个场景根本不存在 它验证的是「关掉最后一个连接 → SQLite 删除 -wal/-shm → 重开得到新 inode」,以此证明目录 bind 优于单文件 bind。实测关闭最后一个连接后: node:sqlite (Node) → -wal 被删 ← 前提成立 node:sqlite (Bun 1.4.2) → -wal 保留 bun:sqlite (Bun,即生产路径) → -wal 保留 sqlite-compat.ts 明确规定 Bun 下使用 bun:sqlite,故第三行才是上线形态。 sidecar 不被删除即没有新 inode,末尾 toBe('v2') 在 Bun 下会**空过**—— 绿着却不再检验 bind 形状。因此不放宽断言(那等于删除保护),改按仓库 既有约定 it.skipIf(isBunRuntime()) 标为 Node-only,并在注释写明三行 实测与「为何不是放宽断言」。Node 侧仍真实执行(vitest 下 1 passed)。 验证(均在**非 root** 下进行,root 复现不出来),在一棵真实 bun install 的树上(非 worktree 的 symlink 形态): - 阴性对照 撤掉修复 → 0 pass / 3 fail(EACCES ×3) - 阳性 带上修复 → 3 pass / 0 fail - 三文件合跑 → 17 pass / 1 skip / 0 fail - vitest/Node 侧 → 3 pass,且 Node-only 那条仍为 1 passed 非 skipped 同类隐患已逐个实测排除(未按形状盲改):另有 5 个测试文件同样创建 0o000,mojo-process-tree / mojo-containment / managed-origin-capability / plugin-registry-sandbox-read / mojo-launcher-env-quarantine 在非 root 下 全绿(154 + 41 pass, 0 fail),其 0o000 不位于随后被删除的树内,无需改动。
c19e407 to
dd67918
Compare
追加:按评审意见选择 (b),先修好被唤醒的三个既有失败第一版只补了 bubblewrap 安装步骤,CI 的 这三个套件此前在 bun 腿上从未真正执行过(把 bwrap 藏掉复跑: ① + ②
|
上一轮修复后 bun-test 从 3 个红文件降到 1 个,剩下的是 plugin-mcp-sandbox 里 "resolves bare `node` under a hostile symlink-form host PATH" 一条,退出码 127(command not found)。 原因不是清理,也不是环境缺失,而是**前提在 bun 下不成立**:该用例保护的 修复是「PATH 前置 dirname(realpath(process.execPath))」,随后探针断言沙箱 里 bare `node` 跑得起来。在 `bun test` 下 process.execPath 是 **bun 二进制**, 于是沙箱被授予的是 bun 的目录,而探针去找一个从未被放进去的 `node` ⟹ 127。 不改成探测 `bun`:这条保护的 shim 里写死 `exec node`(botmuxShimExecLine), 换成 bun 形状就是在断言生产从不做的事,等于把回归保护换成装饰。按同批 session-store-bwrap 的处理方式标 it.skipIf(isBunRuntime()),注释写明原因。 为什么本机一直看不出来:dev 机有 /usr/bin/node,而该 fixture 特意在 PATH 里 保留 /usr/bin(好让宿主找得到 bwrap),于是 bare node 恰好被解析到;CI runner 的 node 在 /opt/hostedtoolcache/node/... 不在 /usr/bin,所以只在 CI 上红。 实测(真实 install 树、非 root): - 阳性对照:用 unshare + mount --bind /dev/null 挡掉 /usr/bin/node 后, 旧版复现出与 CI 逐字相同的 `Received: 127` - 修复后本机常态:2 pass / 1 skip / 0 fail(CI 上绿的那两条仍绿) - vitest/Node 侧:3 passed,被 skip 的那条在 Node 下仍真实执行
第二轮 CI 结果 + 最后一条修复第二轮 剩下的一条是 根因:又一个「前提在 bun 下不成立」该用例保护的修复是「PATH 前置 没有改成探测 为什么本机看不出来:dev 机有 验证覆盖没有净损失:bun 腿 skip 掉的 2 条,正是它无法真实验证的 2 条;Node 腿 18 条全跑。
|
问题
test腿(3 路 shard,#1288 之后)有一步 best-effort 安装 bubblewrap 并放开 userns 限制,bun-test腿没有。job 之间不共享文件系统,这一步必须各写各的——所以 bun-test 完全靠 runner 镜像自带 bwrap,抽到不带的就红。后果不是「少测了点东西」,而是红得像代码回归:bwrap 缺席时
prepareDirectSandbox()返回 null,test/sandbox-shim-compiled-form.test.ts里的overlayTargets()直接return [],4 个toEqual([launcher])断言必挂。看日志像是 overlay 逻辑坏了,实际只是沙箱起不来。bun test这边没有宿主级 skip gate:那组用例的describe.skipIf只挡非 linux,在 linux runner 上照跑,bwrap 一缺就只能失败(而不是 skip)。怎么发现的
#1301(bun 升 1.4.2)的 bun-test 腿红了,形状很像升级引起的——同一条腿在它的 base(master
d47b5f0ef)是 success,到 PR 就 fail,两次唯一差别就是 bun 版本。查下来不是:实测
本机阳性/阴性对照(其余条件不变,只切 bwrap 可见性):
排除 bun 版本这个变量(把 bwrap 藏掉,只换 bun):
步骤内容与
test腿那份逐字节相同(yaml 解析后对比结果 IDENTICAL),不是重新手写的近似版本。spawnSync('sh', ['-c', 'command -v bwrap'])起子 shell 重新读 PATH,实际根本没藏住,跑出 17 pass / 0 fail。那个绿看起来正好像「与 bwrap 无关」的证据。重建 PATH 并先确认子 shell 打出 NOT FOUND 之后,读数才可用。影响面
只动
.github/workflows/ci.yml的bun-testjob,加一步;不碰任何源码、不改其它 job、不改 bun 版本(本分支从 master 出,bun-version仍是 1.4.1,升级在 #1301)。best-effort 写法(每条都带|| true),装不上也不阻塞,与test腿的语义一致。该腿另有一个独立成因:
test/tmux-startup-storm-recovery.test.ts会撞 720s 单文件墙被 SIGKILL(spawnSync tmux ETIMEDOUT)。证据是 master738da7fa4上 bwrap 正常(missing 计数 0)仍单独因它红。本机直跑该文件 2 个用例 6.13s 全过,所以是 CI 环境下第二个用例卡住,跟 bwrap 无关,也不在本 PR 范围内。也就是说:本 PR 消掉两个成因里的一个。另一个要单独查。