oh-my-openagent 安全评审阻塞项全解:RPC 子进程树回收与构建产物完整性防护实战
2026/9/19 14:31:27 网站建设 项目流程

oh-my-openagent 安全评审阻塞项全解:RPC 子进程树回收与构建产物完整性防护实战

【免费下载链接】oh-my-openagentOmO: Just type "mass ulw" keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent

导读

本文以 oh-my-openagent 仓库中 PR #6858 安全评审产生的阻塞项证据文档 security-blockers.md 为骨架,完整还原该次评审暴露出的四个安全缺陷及其修复方案:RPC 子进程树无法被彻底终止、生成式构建产物新鲜度标记可被伪造、相对可执行路径可被任务控制的工作目录劫持,以及相应的全量回归验证。读完本文,你将掌握进程组级终止、可复现压缩、防伪造完整性标记、路径规范化等一整套可复用到任意 Node/Bun 工具链的进程安全与供应链完整性加固手法,并能在本仓库中直接复现全部验证命令。

评审背景:四路评审、两个硬阻塞与四个安全项

这次安全评审针对 PR #6858 的 head 提交5e530b4e2885a9a38d0fe108f27fa5a055b37142展开,完整评审结论记录在 audit-findings.md 中。评审分为五条 lane:

Lane结论阻塞项
目标与约束(Goal and constraints)FAIL生成式 bundle 冲突、缺当前 head CI、Windows 证据不可审计
代码质量(Code quality)FAIL生成式 bundle 冲突;源码改动本身正确
安全与进程安全(Security and process safety)PASS无安全阻塞
实操 QA(Hands-on QA)FAIL缺当前 head CI;生产 RPC 路由证据失效
仓库与历史上下文FAIL生成式 bundle 冲突、缺当前 head CI

评审同时确认了五个评审阻塞项(机械性不可合并、缺当前 head CI 运行、Windows 证据不可审计、task-rpc-e2e.mjs报告execution_mode: "process"未到达 RPC runner、PR 正文链接过期),并明确拒绝了两个非阻塞项:spawnProcess为测试桩扩展公开重导出类型属既有仓库 seam 模式、其他组件的 Windows pipe spawn 属后续范围而非本 PR 回归。

真正进入安全阻塞清单的是 security-blockers.md 中记录的四项,每项都遵循严格的 RED→GREEN 证据闭环:先给出失败复现命令与失败断言,再给出修复后的生产改动、验证命令与通过结果。

阻塞项一:RPC 子进程树的存活问题(进程组级终止)

RED:TERM-ignoring 后代进程存活

原始缺陷通过一条命令复现:

bun test packages/senpi-task/src/runners/rpc/terminate.test.ts

观察结果:

  • 直接 RPC 子进程在升级(escalation)后已退出;
  • 忽略 SIGTERM 的后代进程仍然存活;
  • 断言以Expected: falseReceived: true失败;
  • 套件结果 3 通过、1 失败。

测试夹具会启动一个真实子进程,由它再启动一个长生命周期后代进程,全程不经过 shell——这意味着问题不在 shell 包装层,而在进程树的终止策略本身。

GREEN:进程组拥有者 + SIGTERM/SIGKILL 有界升级

修复后的生产改动(对应 terminate.ts):

  • POSIX 下 RPC 根进程以拥有进程组(owned process group)的方式启动;
  • 终止时先对进程组发送 SIGTERM,再按有界延迟升级为 SIGKILL;
  • Windows 下改用taskkill.exe /PID <pid> /T /Fshell: false,避免经过 shell 转发引入注入面;
  • 测试通过ps -o stat=区分"仍在执行"的进程与"已终止的 POSIX 僵尸进程",不依赖 sleep 等待。

从源码看,terminate.ts 的terminateRpcChild是 RPC 子进程终止的单一写入点(模块注释明确声明这是唯一允许向 RPC 子进程发送信号的模块)。其核心逻辑在 terminatePosixProcessGroup:

  1. 先通过process.kill(-pid, 0)探测进程组是否存在(processGroupExists,ESRCH 视为不存在);
  2. 进程组存在时,对-pid发送 SIGTERM;
  3. waitForExitOrDelayPromise.race在"子进程退出"与"默认 5 秒(DEFAULT_SIGKILL_DELAY_MS = 5_000)升级延迟"之间竞争;
  4. 延迟到期后进程组仍存在,则升级发送 SIGKILL,并以 2 秒为上限、10 毫秒间隔轮询等待(PROCESS_EXIT_OBSERVATION_TIMEOUT_MS/PROCESS_EXIT_OBSERVATION_INTERVAL_MS)。

对非组领导者调用方,terminateDirectChild 提供直系子进程的回退路径:同样的 SIGTERM→有界延迟→SIGKILL 升级。Windows 分支 terminateWindowsProcessTree 用shell: false直接 spawntaskkill.exe,并在任务结束后仍校验子进程是否退出、必要时补发 SIGKILL。

对应测试 terminate.test.ts 覆盖了五类场景:配合的子进程被 SIGTERM 优雅终止、TERM-ignoring 子进程在预算内升级为 SIGKILL、非组领导者子进程仍可终止、已退出子进程重复终止不抛错、以及"TERM-ignoring 后代进程随整棵树退出"(通过process-tree.mjs夹具读取后代 PID 后轮询验证)。

验证命令与结果

bun test packages/senpi-task/src/runners/rpc/terminate.test.ts bun test packages/senpi-task/src/runners bun run --cwd packages/senpi-task typecheck

观察结果:终止套件 4 通过 0 失败;聚焦的 runner/终止套件 15 通过 0 失败;包类型检查 exit 0。

阻塞项二:可伪造的生成式 bundle 新鲜度标记(产物完整性双校验)

RED:注入 JS 主体后标记仍然匹配

复现命令:

bun test packages/omo-senpi/plugin/scripts/build-artifact.test.mjs

观察结果:

  • 向产物中注入一段 JavaScript 主体后,原本的 source digest 仍被保留;
  • 可编辑主体的 digest 被重新计算;
  • artifactsMatch()返回true
  • 断言以Expected: falseReceived: true失败。

这说明旧校验只验证了"标记中的 digest 与当前主体一致",而没有验证"当前主体与新鲜生成的主体字节一致"——攻击者可以在篡改主体后顺手重算并覆写标记,使校验形同虚设。

GREEN:双标记 + 字节一致性 + 固定压缩器版本

修复后的生产改动(对应 build-artifact.mjs):

  • 门禁同时校验两个产物标记与其对应主体;
  • 当前主体 digest 与新鲜生成的主体 digest 必须一致;
  • 当前主体与新鲜生成的主体必须字节级一致(byte-identical);
  • Bun 标识符混淆(identifier mangling)被替换为可复现的语法/空白压缩 + 固定terser@5.44.0标识符混淆
  • 压缩器版本被纳入构建设置 digest,压缩器一旦变动,标记立即失效。

从源码看,artifactsMatch 的判定链是:current.sourceDigest === expected.sourceDigest,且current.bodyDigest === digest(current.body)expected.bodyDigest === digest(expected.body),最后要求两者 digest 相等且主体===字节相等。标记格式为// omo:<sourceDigest>:<bodyDigest>追加在产物头部(shebang 保留在字节 0,标记位于其下一行,digest 覆盖含 shebang 的整个主体),见 attachBuildMarker。digestBuildSources对 metafile 中所有输入文件按排序后的 key 逐个做 sha256 累加,并把构建脚本自身与build-artifact.mjs也纳入摘要。

可复现性保证落在 build-extension.mjs 的构建参数上:minifySyntax: trueminifyWhitespace: trueminifyIdentifiers: falsesecondaryMinifier: "terser@5.44.0",CLI 形态对应--minify-syntax --minify-whitespace--metafile,随后minifyBundle再做一次 terser 压缩。

验证命令与结果

bun test packages/omo-senpi/plugin/scripts/build-artifact.test.mjs npm exec --yes --package=bun@1.3.12 -- bash -c 'node packages/omo-senpi/plugin/scripts/build-extension.mjs --check' bun test packages/omo-senpi/src/bundle-size.test.ts

观察结果:伪造回归测试 1 通过 0 失败;跨进程字节一致性检查 exit 0;omo.js产物 764,648 字节,低于 1,050,000 字节预算;bundle 体积与产物完整性门禁 2 通过 0 失败。

关于体积预算的演进背景,bundle-size.test.ts 的注释完整记录了从 700,000 逐步上调到 1,050,000 的历史:每次上调都对应明确的功能增量(如 memory 组件、lett-memory-parity-port、native-telemetry 波次),且始终遵守"不留意外膨胀空间、不引入新第三方依赖"的纪律——这是本仓库对单文件非拆分 bundle 拓扑的坚持:压缩是文件内语义保持变换,而代码拆分会产生兄弟 chunk,需要改动加载器拓扑,因此被排除。

阻塞项三:相对可执行路径替换(路径规范化防劫持)

RED:相对路径被任务控制的工作目录重解析

复现命令:

bun test packages/senpi-task/src/runners/rpc/spawn.test.ts

观察结果:

  • 相对形式的SENPI_BIN只相对父进程校验后原样返回;
  • 相对形式的PATH条目同样返回相对可执行路径;
  • 两条回归断言均失败,因为后续任务可控的cwd可能把这些路径解析到完全不同的文件。

风险本质:RPC 子进程的cwd由任务 spec 提供(buildRpcSpawncwd: spec.cwd),若可执行路径保持相对,任务即可通过切换工作目录把"你以为的 senpi"替换成"任务放置的任意可执行文件",形成任意代码执行。

GREEN:全部候选路径绝对化 + 符号链接规范化 + 文件类型校验

修复后的生产改动(对应 senpi-launcher.ts 与 spawn.ts):

  • 显式覆盖项(SENPI_BIN)、兄弟候选、PATH 候选一律解析为绝对路径;
  • realpathSync.native规范化符号链接与平台路径别名;
  • statSync(...).isFile()在 spawn 前拒绝目录与非文件候选。

从源码看,canonicalExecutable 组合了realpathSync.native(resolve(candidate))statSync(canonical).isFile()两道闸门,任何一步抛错即返回 null。resolveSenpiExecutable的候选优先级为:显式SENPI_BIN覆盖(即使其包版本与引擎不同也直接采用)→ 编译引擎运行时自身(isCompiledEngine,即 omo 单文件二进制内嵌引擎,spawnprocess.execPath即得到同版本同资产的后代)→ Bun 二进制旁边的兄弟 senpi → PATH 扫描。兄弟与 PATH 候选还会经过引擎版本一致性校验(passesEngineParity):候选的@code-yeongyu/senpimanifest 版本与运行中引擎版本不符时被跳过并告警,无 manifest 可读的候选则保留。

配套的 Windows 细节在 normalizeSenpiLauncher:非.exe后缀候选被识别为 npm shim,回溯其相邻node_modules/@code-yeongyu/senpi/dist/cli.js;而 isRunningCompiledEngine 防止把"无扩展名的编译引擎自身"误判为 shim 而丢弃唯一保证版本匹配的候选。

验证命令与结果

观察结果:可执行解析套件 23 通过 0 失败;两类相对路径场景均返回规范化绝对路径;senpi-task 类型检查与 LSP 诊断干净。验证覆盖了SENPI_BIN缺失路径不静默回退、空 PATH 不命中、bun 兄弟缺失时透传回退、版本不匹配被拒绝、stale 与匹配 PATH 候选并存时匹配者胜出、以及显式覆盖优先于版本校验等分支。

阻塞项四:进程包含边界的诚实声明

修复后的terminateRpcChild契约在 terminate.ts 与证据文档中有明确边界声明:

  • POSIX:回收其拥有的 RPC 进程组;
  • Windows:通过taskkill /T /F回收可发现的进程树;
  • 非组领导者调用方:直系子进程回退;
  • 真实 TERM-ignoring 子进程与后代进程夹具无需 sleep 即可通过。

文档同时明确强调:该 runner 不是操作系统沙箱。已被信任可以访问 Agent 文件系统、网络与凭据的代码,可以刻意守护进程化(daemonize)到新会话,或制造不可逆的外部副作用;阻止这些需要独立的沙箱/容器架构,不属于本 PR 子进程生命周期契约的承诺范围。平台 QA 要求通过"证明拥有的 RPC 树退出、且无记录的子 PID 残留"来满足——这个边界声明本身就是安全评审的重要产物:它防止后续把进程终止误当作沙箱隔离来使用。

全量回归:修复后的 Senpi 完整门禁

所有安全修复落地后,运行全量门禁:

bun run test:senpi

观察结果:1,715 通过;1 个有意的 Windows-only 生产驱动跳过;0 失败;1,716 个测试共 4,844 条断言,耗时 148.41 秒。文档同时确认:全程不保留任何 provider token、凭据体、授权头、私钥或原始环境转储。

复现与验证命令速查

目的命令
进程树终止回归bun test packages/senpi-task/src/runners/rpc/terminate.test.ts
RPC runner 全量bun test packages/senpi-task/src/runners
可执行路径解析回归bun test packages/senpi-task/src/runners/rpc/spawn.test.ts
senpi-task 类型检查bun run --cwd packages/senpi-task typecheck
产物伪造回归bun test packages/omo-senpi/plugin/scripts/build-artifact.test.mjs
跨进程字节一致性检查npm exec --yes --package=bun@1.3.12 -- bash -c 'node packages/omo-senpi/plugin/scripts/build-extension.mjs --check'
bundle 体积与完整性门禁bun test packages/omo-senpi/src/bundle-size.test.ts
Senpi 全量门禁bun run test:senpi

总结:四条可复用的安全加固经验

  1. 进程树终止要按进程组治理:POSIX 用process.kill(-pid, ...)面向进程组、SIGTERM 后按有界延迟升级 SIGKILL;Windows 用taskkill /T /Fshell: false;并把信号发送收敛到单一模块,杜绝多方抢发。
  2. 产物完整性校验要防"自洽伪造":任何"标记与主体一致"的校验都必须同时叠加"当前主体与新鲜构建字节一致"的校验,digest 要覆盖含 shebang 的完整主体,压缩器版本要钉死并纳入构建摘要,保证可复现。
  3. 可执行路径在 spawn 前必须绝对化realpathSync.native+statSync.isFile()双闸门,杜绝任务可控cwd造成的相对路径劫持;候选来源(覆盖项/兄弟/PATH/引擎自身)要有明确优先级与版本一致性过滤。
  4. 安全边界要诚实声明:进程终止不是沙箱,二者不可混用;明确契约范围既能防止误用,也是评审可审计性的基础。

上述全部实现与测试均可直接在本仓库中检视与复现:进程终止见 terminate.ts 与 terminate.test.ts,可执行路径解析见 senpi-launcher.ts 与 spawn.test.ts,产物完整性见 build-artifact.mjs、build-extension.mjs 与 build-artifact.test.mjs,完整评审背景见 audit-findings.md。

【免费下载链接】oh-my-openagentOmO: Just type "mass ulw" keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询