Skip to content

fix(runtime): harden host memory and screenshot uploads - #1229

Merged
deepcoldy merged 6 commits into
deepcoldy:masterfrom
hu5h:codex/botmux-host-memory-protection
Sep 7, 2026
Merged

fix(runtime): harden host memory and screenshot uploads#1229
deepcoldy merged 6 commits into
deepcoldy:masterfrom
hu5h:codex/botmux-host-memory-protection

Conversation

@hu5h

@hu5h hu5h commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@hu5h
hu5h requested a review from deepcoldy as a code owner September 3, 2026 08:24
@deepcoldy

deepcoldy commented Sep 3, 2026

Copy link
Copy Markdown
Owner

这是一条自动评审的初步意见,最终以维护者审阅为准 🙏
(本条已合并/更正此前的多条评论,请以本条为准。)

📦 更新:以下三项我已经在本地做完并推上来了,作者可以直接取用,不必重做。
分支:review/pr1229-rebased-cd(推在本仓库,没有动您的分支
已随主干前进重新 rebase,当前 tip eed310047,base 259b4de96#1284)。两位 reviewer 独立复审均无阻断。

  1. rebase 到最新 master(当前 4af5076b4):丢掉底部 4 个已在主干的 commit,保留顶部 4 个 runtime commit;
  2. 修好第 2 节那个 rebase 后必红的测试
  3. 补上第 3 节两处覆盖缺口的判别性用例(反变异确认有牙)。

取用方式(任选):

git fetch origin review/pr1229-rebased-cd
git reset --hard origin/review/pr1229-rebased-cd   # 覆盖到自己的分支后 force-push
# 或 git cherry-pick 只取那两个 test commit

若您希望我直接推到 codex/botmux-host-memory-protection(PR 开了 maintainerCanModify),
告诉我一声即可——那会重写该分支历史,所以我没有擅自动手。

rebase 中遇到的 2 处冲突及解法记录在本条末尾「rebase 记录」。

先说结论:四个 runtime 改动本身实测过关,没有发现阻断级问题,但不建议直接合——需要作者先 rebase。

1)(最重要)分支基线过旧,底部 4 个 commit 的内容已经在主干里

本 PR 的 merge-base 是 6080e36bd#930,8/20),GitHub 因此报 CONFLICTING。

底部四个 commit 的准确构成是:3 个对应已合并 #955 的原始 commitcbd2346322a1e37f7d1e0974c2a6c179ecf67b486c7b8145bcdec4+ 1 个不在 #955 里的游离 commit 3a9bb8fab;而 #955 的第四个 cd877cc6a 不在本 PR 内。

判据不是看 commit message:把 7b486c7b8 的树和 #955 的树在两者触及的文件全集上逐 blob 比对,oh-my-pi.ts / omp-transcript.ts / structured-bridge-clis.ts / file-bridge-path.ts / local-cli-opener.ts 及对应测试文件全部相同;cli-help 一对的 patch 逐字相同。

⚠️ 顺带一提:git cherry -v origin/master <分支> 对这几个 commit 会全报 +(未上游),因为 squash 合并改变了 patch-id,这个场景下它不可信

直接 rebase 整个分支会在 commit 1/8 就撞上 4 个文件的冲突,且冲突全部是"主干已经演进过去了"(例如 canonicalOmpSessionsRoot 已被主干泛化成 canonicalManagedSessionsRoot3a9bb8fab 要改的那行 isRiffBackendSession 在主干里整段已不存在)。这是冗余 + 冲突负担,不是回退风险。

建议:只保留顶部 4 个新 commit(31fe09efd / 96b643da0 / 2e2f2377c / fd1f383d2)重新基于最新 master。我这样做过:冲突从 11 个文件降到 2 处,都是良性并列(worker-pool.ts 的 import 与准入检查同主干新增的 Codex 守卫落在同一插入点;worker.ts 的截图函数与主干 #950 新增的节流日志重叠)。解完后 rebase 到当前 master(bcfa67557)是 0 冲突

2) 有一个测试在 rebase 后会必然失败(3 行可修)

test/worker-screenshot-upload-hardening.test.tsnew Function 注入依赖后执行 worker.ts真实函数体——这个思路很好,测的是生产代码本身。但主干 #950c1852adfe)给 captureAndUpload 加了节流日志,用到 lastUploadLogAtMslog,而 harness 这两个都没注入:

ReferenceError: lastUploadLogAtMs is not defined

这不是解冲突的问题:主干自己的 captureAndUpload 函数体本来就引用这两个名字,任何正确的合并都会红。它在您当前的 base 上是绿的(我在原 base 上单独跑过,2 passed),所以只有 rebase 之后才会暴露。

修法 3 行:deps 解构清单里加 log,补 let lastUploadLogAtMs = 0;,两处 deps 对象各加 log: vi.fn()

3) 两处测试覆盖缺口(非阻断,建议补)

做了变异测试(每个变异都先确认真的落地,排除空编辑)。这些都变红、说明断言有牙:删单飞守卫、删上传失败后的 hash 恢复、bus env 不放 argv、打破 GOFLAGS 两处镜像、删 MemAvailable 闸。

有两处变异后仍全绿

  • 删掉 controllerDelegated 里的 cgroup-v2 硬门if (!exists('/sys/fs/cgroup/cgroup.controllers')) return false;
  • 跳过 memoryControllerPlacementVerified(把 && memoryControllerPlacementVerified(...) 改成 && true

这两道正是防"把 MemoryMax 误报成可执行"的关键闸,注释里也专门写了理由,但目前没有用例钉住。两位 reviewer 各自独立写了判别性探针验证缺口真实可测(探针先在未变异代码上通过,再对变异体变红)。cgroup-v2 那道门的判别形态比较挑:必须让下游全部会通过(delegation 读得到、memory.max 存在且等于探针 limit),只留 v2 门作为唯一拒绝理由,才能隔离出它。

补充背景:我们的 daemon 宿主就是 cgroup v1,探测结果是 {cleanupSupported: true, memoryControllerSupported: false}——线上 scope 清理会生效、但 MemoryMax 不会,这道门恰好是这类机器上唯一起作用的判断。(v1 上 memory.max 本就不存在、placement 探针自己也会失败,所以它算第二层防御;但注释既然做了明确承诺,仍值得钉测试。)

4) 两点非阻断、供参考

  • 准入被拦时 forkWorkerreturn true,语义是「已处理、别自动重试」。用户会收到重试提示,取舍可接受,建议在 PR 说明里写明。
  • reattach 预测假阴性(预测 fresh 但 pane 仍存活)时,stopSessionScope 会先于 attach 执行。这属既有问题类别、非本 PR 引入。

实测记录(真机验证,Linux daemon 宿主)

  • TTY 语义无损:node-pty 下 baseline vs scoped 对比,TTY_STDIN/STDOUT=YES、同一个 /dev/pts24 80、TERM 一致。
  • 进程树回收为真:scope 内 6 个进程,stop 后按 PID 逐个核对 0 存活
  • 共享 tmux server 存活:tmux server 不在 scope 内,stop 后仍活着——包 pane 内命令而非 tmux new-session 这个选择是对的。
  • bwrap 可嵌套:scope 在外、bwrap 在内(本 PR 的包装顺序是最外层)实测正常;zellij/zmx/herdr 均为直接 exec bin+args,包装后 argv 无 shell 特殊字符。
  • unit 名冲突:名字被占用时 systemd-run 退出 1,CLI 会直接起不来;PR 在 wrap 前先 stopSessionScope(),实测 stop 后同名可立即复用,缓解有效。
  • relay/transfer 不误杀detach_for_transferkillCli()、不调 stopOwnedSessionScope,且重连时 willReattachPersistent=true 让整个 wrap 分支跳过,旧 scope 继续持有存活的 CLI。
  • 探测开销 14ms;buildtsc --noEmit 均通过;PR 相关 10 个测试文件 301/301。
  • 全量 unit:与同环境 pristine master 失败集合一致,差异项逐个隔离复跑均通过、且无一 import 本 PR 触及的模块,判定为既有 flake / root 身份导致 EACCES 类断言失效,无回归

rebase 记录(我这边实际怎么解的)

基于最新 master 只 cherry-pick 顶部 4 个 runtime commit,出现 2 处冲突,都是「主干与本 PR 各自动了同一处」:

src/worker.tsrestartCliProcess 里的 killCli
主干 #1144killCli 加了 preservePolicyCapability: true,本 PR 在同一位置插入 stopOwnedSessionScope('restart')
两者正交,都要保留,顺序是先停 scope 再 killCli

stopOwnedSessionScope('restart');
killCli({
  preservePending: opts.preservePending,
  preservePolicyCapability: true,
});

(这里如果只取一侧,会静默丢掉主干新加的策略能力保留或本 PR 的 scope 回收。)

src/core/maintenance.ts + test/daemon-lifecycle-env.test.ts — GOFLAGS
本 PR 往 detachedRestartEnv() 里那份手抄的键名清单加了 GOFLAGS。但主干已经把它重构成
resolveFleetDaemonEnv() + for (const key of DAEMON_ENV_KEYS)单一来源写法,手抄清单整段不存在了。
⟹ 取主干侧即可:本 PR 另一处把 GOFLAGS 加进 DAEMON_ENV_KEYSsrc/cli/daemon-lifecycle-env.ts没有冲突
所以「GOFLAGS 参与 fleet env 交接」这个目的已经达成,且不再需要两处手工镜像。
测试文件那处只是措辞差异(主干已从 "PM2 app" 改成 supervisor 表述),取主干。

补的两个用例长什么样

都放在 test/session-scope.test.ts

  • cgroup-v1 宿主:构造让 delegation 与 placement 的下游检查全部会通过memory.max 存在且等于探针 limit),
    只留 v2 根标记缺失作为唯一拒绝理由;并额外断言 MemoryMax 参数确实没被加上(不只断言 flag)。
  • 纸面 delegation:v2 标记在、controllers/subtree_control 都列了 memory,但活动 scope 的 memory.max 不存在 ⟹ 必须拒绝。

反变异:删 cgroup-v2 硬门 → 转红 1 条;把 && memoryControllerPlacementVerified(...) 改成 && true → 转红 1 条;
另做了一个行为等价的良性重构对照(把硬门改成 const v2RootMarker = …; if (exists(v2RootMarker) !== true) return false;)→ 仍全绿,
说明钉住的是契约而不是实现写法。

复验数据(在 rebase 后的分支上重跑)

  • bun run build 通过;tsc --noEmit exit 0;
  • PR 改动的 9 个测试文件 293/293;再加 2 个回归面文件(maintenance.test.tsDAEMON_ENV_KEYS 镜像、restart-worker-null-reattach.test.ts 被丢弃 commit 碰过)共 11 个则为 342/342test/session-scope.test.ts 由 6 个用例增至 8 个,全绿;
  • 全量 unit(在 base 4af5076b4 时测的,后续 rebase 到 ba431e179 只重跑了上述 11 个相关文件):本分支 35 红 / 同环境干净 master 34 红。差异项逐个隔离复跑:
    session-lifecycle-start 3/3 通过、listen-with-probe 3/3 通过(已知端口竞态 flake);
    唯一隔离后仍红的 mojo-launcher-env-quarantine(2 条 EACCES 用例)在干净 master 上同样红同样两条
    ——以 root 身份跑测试导致权限断言不成立,属既有问题。无回归

⚠️ 一个环境提示:本机 bun 是 1.4.0 而仓库 pin 已升到 1.4.1,跑全量前建议核对 bun --version
否则会看到一批与本 PR 无关的版本断言红。


更正记录:本条此前的一个版本曾声称底部 commit 是「#955 定稿前的更旧版本,硬合会把主干已精化的两处改回旧样子」。该说法不成立,已撤回return p 的精化来自 #7771f38ca8cf)而非 #955,两个 omp commit 都没碰过那一行;restart-worker 的 slice 在主干上是 #950 从 6000 一步到位改成 7500,git log -S "setup + 7000" 在主干零命中;实测 cherry-pick 到 master 该行是冲突、需人工选择,不会静默回退。差异来源是 base 漂移,不是内容新旧。教训:判断是否回退应直接读两边文件内容并用 git log -S 确认该值在主干存在过,而不是比对两份 diff。

再次说明这只是自动评审的初步意见,可能有误判,最终以维护者审阅为准。辛苦!

@deepcoldy

deepcoldy commented Sep 3, 2026

Copy link
Copy Markdown
Owner

补充一条自动评审的复审意见(第二位 reviewer 的独立验证),最终仍以维护者审阅为准 🙏

首轮评论的结论我逐条独立验证过,全部成立,并补充几个细节:

关于基线(首轮第 1 条的补充)

底部 4 个 commit 与 #955 的对应关系比「squash 前原始 commit」更精确一点:它们是 #955 原始 4 个 commit 中的 3 个cbd234632/1e0974c2a/7b486c7b8,与 #9552a1e37f7d/6c179ecf6/145bcdec4 消息一致、补丁内容一致)外加 1 个不在 #955 里的游离 commit 3a9bb8fab,同时缺少 #955 的最后一个 commit cd877cc6a

这不影响「丢掉底部 4 个」的结论,反而更稳:

我用三方合并验证过:把底部 4 个的树与 d9a438b05 合并,除上述两处「主干已演进」的文件外无差异。git cherry -v 全报 + 的现象也复现了——squash 改了 patch-id,这个场景下确实不可信。

关于测试修复(首轮第 2 条)

lastUploadLogAtMs/log 缺失是 #950c1852adfe)引入的:它在主干里、不在您的 base 里,所以您那边绿、rebase 后必红。修复(deps 解构加 log、补 let lastUploadLogAtMs = 0、两处 log: vi.fn())已验证正确,build + 相关测试全绿。

实测复核(Linux daemon 宿主,cgroup v1)

  • 宿主探测结果 {cleanupSupported: true, memoryControllerSupported: false},与首轮一致——scope 清理生效、MemoryMax 不生效;
  • 真实 tmux pane 集成测试通过:scope 内的子/孙进程在 stop 后按 PID 核对 0 存活;
  • unit 名冲突实测:占用同名 unit 后再次 systemd-run 退出 1(CLI 起不来),stopSessionScope 后同名可立即复用——wrap 前先 stop 的缓解有效;
  • zellij/zmx/herdr 后端都是直接 exec bin+args(layout / bootstrap / agent-start),包装后的 argv 不含 shell 特殊字符(unit 名已 sanitize),且 scope 包装在 bwrap 之外层,嵌套顺序正确;
  • 全量 unit:与同环境 pristine 主干失败集合一致(root 身份导致的 EACCES 类既有失败 + 2 个高负载 flake,隔离复跑全过),无回归。

建议

与首轮一致:rebase 时只保留顶部 4 个 runtime commit(测试修复可一并带上);另外两处覆盖缺口(cgroup-v2 硬门、placement 验证)建议各补一个判别性用例——我验证过缺口真实存在且可测:构造「下游全部会通过、只留该闸当唯一拒绝理由」的场景即可钉住。

辛苦!

hushihao.bob and others added 6 commits September 6, 2026 22:24
两处闸此前变异后仍全绿(删 cgroup-v2 硬门 / 跳过 placement 验证),
说明「不谎称 MemoryMax 可执行」这条承诺没有用例承重。

补两个判别性用例:
- cgroup-v1 宿主:让 delegation 与 placement 的下游检查全部会通过
  (memory.max 存在且等于探针 limit),只留 v2 根标记缺失作为唯一拒绝理由,
  并断言 MemoryMax 参数确实没被加上(不只断言 flag)。
- 纸面 delegation:v2 标记在、controllers/subtree_control 都列 memory,
  但活动 scope 的 memory.max 不存在 ⟹ 必须拒绝。

反变异:两个变异各转红一条;行为等价的良性重构仍全绿。
@deepcoldy
deepcoldy force-pushed the codex/botmux-host-memory-protection branch from fd1f383 to eed3100 Compare September 7, 2026 07:51

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

双人独立评审通过,无阻断。

分支已 rebase 到最新 master(base 259b4de96),底部 4 个已在主干的 commit 已丢弃,保留 4 个 runtime commit,另附 2 个测试补丁(rebase 后必现的 harness 注入缺失修复 + cgroup-v2 硬门与 placement 验证的判别性用例)。

验证:build / tsc 通过;PR 改动的 9 个测试文件 293/293;全量 unit 与同环境 pristine master 失败集合一致,差异项逐个隔离复跑确认为既有问题或负载 flake,无回归。真机验证 scope 进程树回收(stop 后按 PID 核对 0 存活)、共享 tmux server 不受影响、TTY 语义无损、bwrap 可嵌套、relay 接力不误杀。

@deepcoldy
deepcoldy merged commit b8c9262 into deepcoldy:master Sep 7, 2026
4 checks passed
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

🚀 Released in v3.19.3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants