Files
soft_quay/docs/review/phase4-review.md
T
ilaandClaude Fable 5 a2cda73deb
Harness governance / validate (push) Has been cancelled
Phase 0 build gate / verify (push) Has been cancelled
Add Phase 4 launch/update/self-update review with cross-check ruling
Code-level audit of T-401~T-403: launch has no command-injection surface
(AppID-only request, entrypoint must be in recorded installed files,
safepath+JoinUnder+Lstat, no-arg exec.Command and parameterless
ShellExecuteExW), update never force-kills (confirm + natural-exit wait
only), and self-update is journaled, rollback-capable and health-gated
before backup deletion. Confirms Phase 3 O1 is closed by T-401's
preSwitchCheck.

Ruling accepts Codex's follow-up: O4 (self-flagged T-403 fault-injection
and mid-phase Recover test gaps - all four claims verified) and O5
(split assembly prerequisites instead of lumping them under T-502).
Adds a low-cost path to close O4 mostly via the existing DirectorySyncer
seam and filesystem permission tricks, without new production seams.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 23:44:01 +08:00

16 KiB

Phase 4 审查(T-401 ~ T-403:进程检测与启动 / 子软件更新 / 盒子自更新)

审查范围:87083c3(T-401 进程检测、受控启动与切换临界区复查)、339beaa(T-402 子软件更新编排与自然退出等待)、1300421(T-403 SoftBox 自更新助手与健康确认恢复)。 审查视角:全栈开发工程师 + 安全审计。 审查方式:静态代码审计 + 攻击路径推演(命令注入、TOCTOU、自更新崩溃恢复)。因 WSL 无 Go,未执行测试。 日期:2026-07-19

总体判断

Phase 4 的实现设计质量高,未发现可证实的命令注入、强杀或越界移动安全缺陷。 三个最敏感的点都做对了:启动无命令注入面(调用方给不了路径/参数/工作目录,平台层无 shell、无用户参数、绝对路径)、更新绝不强杀(只请求关闭 + 等自然退出)、自更新可回滚且健康确认前不删 backup。同时 Phase 3 审查的 O1(切换前不复查运行状态)已被 T-401 闭合。

不过,补充全栈复核发现 T-403 的自动化故障覆盖低于任务验收所要求的范围。因此 O1 不能仅视为普通“优化项”:Windows 真机锁语义仍属于 T-601,但可在无头环境验证的失败注入、崩溃 phase 和恢复契约应作为后续整改的高优先级测试项。O2、O3 仍是准确的架构/装配边界说明。

遗留项闭合确认

遗留项 状态 证据
Phase 3 O1 · switch 前不复查 running 已闭合(T-401) switcher.go 新增 preSwitchCheck,在 current→backup rename 前执行(第 97-98 行);InstallService.preSwitchCheck 显式 IsRunning,运行则返回 ErrTargetRunning(→ app_running),检查失败返回 ErrTargetStateCheck——正是 Phase 3 裁定要求的"显式复查、不从 rename 失败推断"。覆盖"WaitForExit 后重新启动"竞态

逐模块核验

T-401 · 进程检测与受控启动(application/launch + platform/windows)

无命令注入面(核心安全属性):

  • Request 只含 AppID——"caller cannot supply a path, arguments or working directory";Command 全部由已验证 installed 元数据构造。
  • launchCommand:entrypoint/workdir 经 safepath.ValidateRelative + JoinUnder(destination 包含性)+ Lstat(拒符号链接/非普通文件);entrypoint 必须在已记录的 installed Files 里(containsEntrypoint),不能启动未随可信安装记录落盘的任意文件;current/workdir 经 requireRealDirectory(拒符号链接目录)。
  • 平台层 Start:exec.Command(command.Entrypoint) 无参数 + cmd.Dir,要求绝对路径;startElevated 用 ShellExecuteExW "runas" 且不传 parameters。无 shell、无用户参数、无 arg 注入。

Fail-closed 门禁链: ResolveCurrent → compat → authorization → running → launch,5 个检查器强制非 nil,任一失败/不满足即拒;稳定错误码枚举(not_installed / launch_target_unsafe / app_incompatible / not_authorized / app_running…)。AuthorizationChecker 是必需边界(真实许可策略留 T-502),不发 allow-all。

Windows 隔离: Toolhelp 检测、RtlGetVersion、无参数进程创建、runas 均在 platform_windows.go;platform_stub.go(!windows)全部返回 ErrUnsupported(fail-closed)。

T-402 · 子软件更新编排(application/update)

  • 绝不强杀:running 时只 ConfirmClose(问用户)+ WaitForExit(等自然退出);超时 → DeadlineExceeded(exit_wait_timeout),取消 → ctx.Err()(exit_wait_canceled);接口注释明确 confirmer/waiter"must not close or terminate the process"。
  • 无降级/无重装:validateTargetVersion 要求新版本 SemVer 严格大于已装版本,package 与 App.Packages[arch] 精确对应。
  • 委托可信安装器:staging/switch/rollback 与切换前第二次 running 复查都交给 InstallService;因此"等待结束后应用重新启动"的竞态由 T-401 的 preSwitchCheck 兜住。
  • 5 依赖强制非 nil,CloseTimeout ∈ [1s, 10min];更新 use case 不改 transaction 或 data/licenses。

T-403 · 盒子自更新(core/updater + cmd/softboxupdater)

  • 等父进程自然退出:WaitForProcessExit(ctx, ParentPID, timeout);超时返回错误,从不 kill。
  • 固定 layout,无任意端点:StagingDir/TargetDir 对照固定 layout 校验;RequestID 从 staging 规范目录名派生复验(不走 CLI)。
  • 只启动 SoftBox.exe + 固定健康标志:ProductExecutableName="SoftBox.exe"、InternalHealthFlag;StartSelfUpdate 只传固定健康 requestID。
  • journal 化可回滚:prepared → target_backed_up → staging_activated → launched → committed,任一失败 rollback(errors.Join 保留根因),Recover 处理中断;DirectorySyncer(Windows FlushFileBuffers)做目录耐久。
  • 健康确认前不删 backup:WaitForHealth 只接受精确 requestID,通过后才 removeManagedTree(backup);失败 → rollback 恢复旧版;锁导致 restore 失败则保留 journal/backup 不强杀。

需优化项(按优先级,均非安全缺陷)

O1 · 自更新的"先启动后健康确认"必然要处理运行中的新进程

现状: StartSelfUpdate 先启动新 SoftBox,WaitForHealth 才等确认。若健康超时/取消,rollback 需移动含正在运行的新 SoftBox.exe 的 target 目录;Windows 文件锁可能使 restore 失败,设计选择保留 journal/backup 交下次 Recover(不强杀)。

影响: 这是自更新的固有约束(必须运行新版才能健康检查),处理方式安全(不强杀、可恢复),非缺陷。但"launched 但未 committed、rollback 被锁阻塞"的中间态依赖下次 Recover + 新进程在"未 committed 事务"下的行为收敛。

建议: 重点测试:健康超时后的 Recover 路径、新 SoftBox 在"仅匹配已激活事务下写健康"门禁的正确性、以及锁阻塞 restore 后的可恢复性。真机文件锁语义归 T-601。

O2 · 无生产装配,端到端启动/更新闭环未接通(与 Phase 3 一致)

现状: launch/update/updater 三个 use case 在 core 层建成且测试覆盖,但无生产 cmd 装配(无可信 Catalog/许可证/下载完成文件来源);当前有意不注入 allow-all 授权或伪造端到端按钮。

影响: 与 Phase 3 结论一致——无头 core 的启动/更新/自更新边界已完成;端到端编排未接通。M3 定义含"启动",故 M3 仍需端到端装配(依赖真实发布配置 + T-502 授权)才算闭环。

建议: 在 current-state.md 保持准确表述;端到端装配随发布配置与 T-502 授权落地。

O3 · 授权门禁在位但策略待 T-502

现状: launch 服务强制 AuthorizationChecker(不可绕过),但真实许可验证是 T-502。

影响: 架构正确(把授权做成必需边界);当前不发 allow-all 是好纪律,意味着尚无生产启动路径。非缺陷。

建议: T-502 落地时把真实许可策略接入该边界,并补"未授权拒绝启动/试用可启动"的端到端测试。

O4 · T-403 的故障注入与恢复测试覆盖不足

**现状:**现有 core/updater/updater_test.go 覆盖成功激活、健康失败后的顺利回滚、父进程等待失败、部分固定布局拒绝、target_backed_up phase 的恢复、健康文件定位与 request ID 拒绝。但它没有以可控 seam 覆盖 rename、transaction 写入、目录同步、backup/journal 清理失败,也没有覆盖 staging_activated、launched、committed 三个中断 phase 的 Recover。cmd/softbox 的内部 health flag 参数处理也没有专门测试;现有 CLI 测试只覆盖 SoftBoxUpdater 的重复参数拒绝。

**影响:**这不是已证实的实现安全漏洞:现有代码在恢复被 Windows 文件锁阻止时会 fail closed、保留 journal/backup,且不强杀进程。但它与 T-403 验收中“每个 rename/journal 故障和崩溃注入均有稳定错误、可恢复时旧 app 恢复”的测试证据不相符。没有这些测试,无法充分证明 O1 中“锁阻塞后等待下次 Recover”的状态机在所有持久化边界都可预测。

**建议:**单列整改任务,先为 updater 的文件操作和目录同步提供最小失败注入 seam,补齐健康超时→rollback 被锁阻塞→后续 Recover、各 transaction phase 崩溃恢复、journal/rename/sync/cleanup 失败与 health CLI 参数拒绝的双端测试。真实 Windows 文件锁、杀毒软件和断电结果仍只由 T-601 真机/VM 验证,不以单元测试替代。

O5 · 端到端装配依赖应拆开跟踪

**现状:**O2 正确指出 launch/update/updater 目前只完成无头 core 边界;production cmd 尚未装配可信 Catalog、许可证或 completed-download 消费源。

**补充:**不应把所有装配前置条件笼统归为“等待 T-502”。可信 softbox-catalog 发布配置、已签名 Catalog 的加载/缓存/目标过滤和下载完成文件的受控消费,可在 T-502 前独立接入;T-502 决定的是授权门禁下的实际启动资格。自更新还另缺可信 self-update 包的下载、验签、版本选择和 UI 触发器。

**建议:**后续按“可信 Catalog 发布配置 → 下载/安装/更新编排 → T-502 授权接线 → 自更新发布源与触发器”分别落任务和验收,避免将目前的 fail-closed 空壳误表述为端到端闭环。

补充验证(2026-07-19)

  • 复核环境实际运行了 go -C core vet ./...、go -C core test -count=10 ./updater ./application/launch ./application/update ./application/install ./installer 与 ./scripts/verify_phase0.ps1;核心相关包、双端 UI/platform 测试、双端 Windows amd64 构建以及治理校验均通过。
  • 尝试运行 go test -race 时,当前环境因 CGO_ENABLED 被禁用而拒绝执行;这不影响项目既定验证的通过结论,但不应把本次复核表述为 race detector 已通过。
  • 本节是对本报告静态审计结论的补充核验,不改变原报告“未在静态攻击路径中发现命令注入、强杀或越界移动缺陷”的判断。

结论

Phase 4 的核心安全属性(启动无注入面、更新不强杀、自更新可回滚且健康门控、切换临界区复查闭合 Phase 3 O1、Windows 隔离 + fail-closed stub)全部到位,失败码体系完整。建议处理顺序:

  1. [高优先级 · 可靠性/测试] O1/O4:补自更新健康超时后的 Recover/锁阻塞恢复、新进程健康门禁、transaction phase 崩溃恢复与 journal/rename/sync/cleanup 失败注入测试;真机锁语义归 T-601。
  2. [准确性 · 装配规划] O2/O5:文档保持"无头 core 已完成、端到端未接通"表述;可信 Catalog/下载消费、T-502 授权和自更新发布源分别落地,不笼统等待单一任务。
  3. [后续] O3:T-502 接入真实授权策略并补端到端授权测试。

声明:本报告为静态审计与攻击路径推演,逐项核了实现(launch 无参数 exec.Command + ShellExecuteExW 无 parameters、containsEntrypoint + safepath + Lstat、update 的 WaitForExit 不强杀、updater 的 journal/rollback/健康门控、switcher preSwitchCheck);因 WSL 无 Go 未执行测试,双 workspace 闸门通过为 Codex 自述。O1 的 Windows 文件锁语义需真机验证。

交叉复核裁定(定稿)

复核视角:Claude 全栈开发工程师,对 Codex 补充复核(O4/O5 + 验证段)逐条核验后裁定。 核验方式:枚举 core/updater/*_test.go 全部测试函数;检查故障注入 seam、各 transaction phase 的 Recover 覆盖、cmd/softbox health flag 测试存在性;因 WSL 无 Go,未执行测试。 日期:2026-07-19

裁定结论

Codex 本轮补充成立且比原审核更严谨,全部接受。 O4 是 Codex 对自己 T-403 测试覆盖的诚实自查,其每一条具体主张均经核实属实。原审核 O1 只写了"建议重点测试",未实际读 updater_test.go、未枚举具体缺口;Codex 做了该具体核查并定位到缺口,这部分比原审核更扎实。

已确认的事实(可作为后续任务前提)

项 结论 证据
updater 现有覆盖范围 与 O4 所述一致 8 个测试:成功激活、健康失败回滚、父进程等待失败、跨 layout/staging symlink 拒绝、target_backed_up 单 phase Recover、health locator、requestID 拒绝
无文件系统故障注入 seam 属实 全部用永远成功的 testSyncer{};无任何测试令 rename / transaction 写入 / 目录 sync / 清理失败(唯一失败路径来自 HealthWaiter)
中断 phase 的 Recover 未测全 属实 仅 TestRecoverRestoresTargetBackedUpTransaction 覆盖 target_backed_up;staging_activated/launched/committed 无 Recover 测试(第 194 行的 phaseStagingActivated 是 health-ack 测试的 setup,非 Recover 用例)
cmd health flag 无专门测试 属实 app-modern/cmd/softbox/ 只有 catalog_bootstrap_test.go;update_health.go 无对应测试文件

采纳

  • 接受 O4,并同意其定级:这是测试覆盖缺口,不是已证实的实现缺陷(代码在锁阻塞时 fail closed、保留 journal/backup、不强杀)。但缺少故障注入与中断 phase 恢复测试,意味着该状态机的持久化边界行为"构造上正确"而未被测试证明,且后续回归无法拦截。对自更新器(失效即"盒子自己更新不动")值得列高优先级。
  • 接受 O5 对原 O2 的细化:装配前置不应笼统归为"等 T-502"。可信 Catalog 发布配置、签名清单加载/缓存/目标过滤、下载完成文件受控消费可在 T-502 前独立接入;T-502 只决定授权门禁;自更新另缺可信包下载/验签/版本选择/UI 触发器。按四条分别落任务与验收。
  • 接受验证段:go test -count=10 对并发/恢复代码是恰当的抗 flakiness 实践;-race 因 CGO_ENABLED 禁用无法运行并如实声明"不得表述为 race detector 已通过",该诚实度正确。

补充:O4 的低成本落地路径(多数无需新增生产 seam)

  1. sync 失败注入——用现有 seam,零生产改动:DirectorySyncer 本就是注入接口。把 testSyncer 改为"第 N 次调用返回 error",即可覆盖 prepared/target_backed_up/staging_activated/launched/committed 各写入点后的 sync 失败及其触发的 rollback/Recover。
  2. 中断 phase 的 Recover——复制现有模式即可:直接 writeTransaction(layout, phaseX, ...) 造出各中间态目录布局后调 Recover 断言收敛,与 TestRecoverRestoresTargetBackedUpTransaction 同模式,成本低。
  3. rename / 写入失败注入——优先用文件系统权限技巧:updater 内部为裸 os.Rename/文件写。无头 Linux 测试可用只读父目录、rename 到非空目录等方式强制失败,不必为此在生产代码加 seam;仅当需要精确控制"哪一步失败"时,再考虑最小 seam。
  4. 真机边界不变:Windows 文件锁、杀毒软件、物理断电仍只由 T-601 真机/VM 验证,不以单元测试替代。

最终处理顺序(定稿)

  1. [高优先 · 测试] O1/O4:按上述路径 1→2→3 补齐自更新故障注入与中断 phase 恢复测试,并补 health CLI 参数拒绝的双端测试。
  2. [装配规划] O2/O5:按"可信 Catalog 发布配置 → 下载/安装/更新编排 → T-502 授权接线 → 自更新发布源与触发器"分别落任务;文档保持"无头 core 已完成、端到端未接通"表述。
  3. [后续] O3:T-502 接入真实授权策略并补端到端授权测试。
  4. [真机] T-601:Windows 文件锁 / 杀毒软件 / 断电故障注入,不由单元测试替代。

裁定:Codex 本轮补充(O4 自查、O5 细化、验证段)全部成立;原审核在测试覆盖维度不如本轮严谨,予以采纳。Phase 4 安全总结论(无注入面、不强杀、可回滚且健康门控、Phase 3 O1 已闭合)双方一致,不需回滚或返工。