Code-level audit of the verified install chain: hash-before-parse and single-file-handle TOCTOU defenses in verified_package.go, strict app.json parse cross-checked against the signed Catalog, untrusted download verified against Catalog Size/SHA256, mandatory non-bypassable pre-extract disk/running checks, and a complete stable failure-code enum. No security defect found; records five minor optimizations, the top being the IsRunning TOCTOU (recheck before the current->backup rename). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8.2 KiB
Phase 3 审查(T-301 ~ T-303:下载队列 / 安装整合 / 失败处理)
审查范围:
8dad40f(T-301 可恢复下载队列,早前已实现)、14589ab(T-302 安装流程整合)、ae3f64c(T-303 失败处理与磁盘预检)。 本轮深读重点:T-302/T-303 的安装整合与失败处理(core/installer/verified_package.go、core/application/install/service.go);T-301 队列此前已在架构文档层核过(可恢复、崩溃对账),本轮不重复深挖。 审查视角:全栈开发工程师 + 安全审计。 审查方式:静态代码审计 + 攻击路径推演。因 WSL 无 Go,未执行测试。 日期:2026-07-19
总体判断
Phase 3 质量高,安全正确,未发现安全缺陷。 安装整合把 Phase 1 的原型(验签清单、安全解压、原子切换)串成完整可信链,并且在两个最容易被忽略的点上做对了:先验哈希再解析 ZIP、全程复用同一 file 句柄防 verify-then-use TOCTOU。失败处理有完整的稳定错误码枚举。M3(清单→列表→下载→安装→启动)核心闭环成立。以下均为可优化观察项,非缺陷。
逐模块核验
T-302 · 安装整合(installer/verified_package + application/install/service)
包验证链顺序正确(verified_package.go,对应 api.md 安装验证顺序):
expectation.validate():Catalog 期望校验(size>0、SHA-256 32 字节、app 字段合法)。openArchiveFile(zipPath, expectation.Size)→verifyPackageSHA256在解析任何 ZIP 结构之前用io.NewSectionReader全量哈希并subtle.ConstantTimeCompare;哈希后Stat复查Size/IsRegular防并发改动。scanOpenedArchive(中央目录预扫描)→zip.NewReader→preflight(路径/上限/结构)→readPackageAppManifest(app.json,限 1 MiB)→manifest.matches(expectation.App)。beforeExtract(环境预检)→extractPlan(安全解压)。
两个关键安全属性:
- 哈希先于解析:恶意 ZIP 在 SHA-256 通过前碰不到 zip parser,消除"解析未验证字节"的攻击面。
- 单句柄贯穿:同一个
file句柄用于哈希 → 预扫描 →zip.NewReader→ 解压,攻击者无法在"验证后、使用前"替换文件(verify-then-use TOCTOU)。这是很多实现漏掉的点。
app.json 严格且与 Catalog 交叉比对: validateManifestObject 手工拒绝未知/重复/缺失字段与尾随数据;DisallowUnknownFields 二次解码;validate() 固定 schema_version=1、channel=stable、data_policy/update_policy 定值、working_directory 经 safepath;matches() 要求 ID/版本/通道/min_os/架构/entrypoint/requires_admin 全部等于已签名 Catalog 期望。
VerifiedPackage 不含 ZIP 句柄或目标路径,预检回调拿不到原始 archive/destination,无法绕过安全解压——好的 API 边界。
每文件 SHA-256 在解压流中计算(io.MultiWriter(output, digest),无二次读),写入 installed-app.json Files,为后续 files.json 修复铺路。
T-302 编排(install/service.go)
- 信任边界清晰:
InstallRequest明确"untrusted completed download + trusted Catalog selection";信任根是 Catalog 的Size/SHA256。本地对.download/元数据的篡改被 SHA-256 + 签名复检拦下。 - 先恢复再安装:
installer.Recover(appRoot)在开始新安装前解决遗留事务,与 switcher 的ErrRecoveryRequired守卫一致。 - 强制安全依赖:
NewInstallService拒绝 nil 的 Records/Health/DiskSpace/TargetState——"no caller can silently bypass the pre-extract safety boundary"。 - record 写入置于 switcher 健康回调内:写失败与健康失败走同一回滚路径(
recordWriteErr追踪 →InstallStageRecord),避免"切换成功但记录半写"的中间态。
T-303 · 失败处理与磁盘预检(preExtractCheck + 错误码)
- 磁盘预检:
required = payloadBytes + 64 MiB reserve,available < required或< 0拒绝;在解压目标创建前 fail-fast。 - 运行检测:
TargetState.IsRunning在预检拒绝正在运行的目标(ErrTargetRunning);接口注释明确"never starts/waits/terminates a process",符合"不强杀"。 - 稳定错误码枚举:
hash_mismatch/zip_path_escape/zip_corrupt/package_invalid/disk_full/disk_check_failed/app_running/target_state_unavailable/install_failed,failureCodeFor用errors.Is映射;UI 只本地化码、不显原始错误。与 api.md 错误码一致,满足"各情况有确定结果与错误码"。
需优化项(按优先级,均非安全缺陷)
O1 · 运行检测存在 TOCTOU,switch 前不复查
现状: IsRunning 只在 preExtractCheck(解压前)执行;switcher.Switch 在 current → backup rename 前不复查。解压可能耗时数秒,期间用户可能启动该软件。
影响: Windows 对运行中 EXE 的文件锁能否可靠阻止父目录 rename,并无明确保证(取决于镜像 section 锁语义),不应默认"Windows 一定会挡住"。两种劣化路径:① rename 失败 → 回滚(较干净,但错误码是通用 switch 失败而非 app_running);② rename 成功但 commit 阶段删 backup 因运行中 EXE 锁失败 → 新版已装但 backup 滞留 + 报错。均非数据丢失,但都不干净;非 Windows(测试)路径无锁,TOCTOU 更实。
建议: 在 switcher current→backup 紧邻前再查一次 IsRunning(把窗口从"整个解压期"收窄到"切换瞬间");并确保"因占用导致 rename 失败"映射到清晰的 app_running 诊断而非通用错误。TOCTOU 无法在无 OS 级锁下完全关闭,但可显著收窄。
O2 · 磁盘预检是建议性,真正保证来自解压失败清理
现状: AvailableBytes 在解压前查,但其他进程可在"查"与"写"之间占用磁盘。
影响: 中途 ENOSPC 由 extractor 的失败清理(删 staging)兜底,只是错误路径与预检的 disk_full 码不同。预检是 fail-fast UX,不是保证。
建议: 明确文档表述"预检为 fail-fast,真正的原子性由 extractor 失败清理保证";可选:把中途 ENOSPC 也归一到 disk_full 码,避免同一现象两种码。
O3 · 已完成下载文件的生命周期归属未在安装服务体现
现状: Install 不删除 request.DownloadPath;安装成功后 ZIP 由谁清理未在此层体现。
建议: 明确下载 ZIP 的清理归属(下载队列 or 专门 GC),避免 downloads/ 无限增长。属生命周期职责,非缺陷。
O4 · verified_package 的 stage 标签命名不一致
现状: ZIP preflight() 失败标记为 PackageStageVerify,而 PackageStagePreflight 用于 beforeExtract;normalizeEntrypoint 失败标 PackageStageManifest。
影响: 纯观测性命名,不影响安全或行为;诊断时 stage 语义略含糊。优先级低。
O5 · StagingDiskReserveBytes 固定 64 MiB
现状: 固定预留,不随包大小缩放。更新场景峰值磁盘为"旧 current + 新 staging 并存"(current→backup、staging→current 均为同卷 rename,无额外空间),预检只保证新 payload+reserve 的空间,假定旧 current 已在盘。
影响: 该假定对更新/首装都成立,heuristic 合理。仅提醒:若未来 backup 策略改变(非同卷 / 复制而非 rename),需重估预留模型。
结论
Phase 3 可作为启动/更新/授权阶段的可信安装基线。安装整合的安全属性(哈希先于解析、单句柄防 TOCTOU、app.json 与 Catalog 交叉比对、untrusted 下载 + Catalog 信任根、强制不可绕过的预检)都到位,失败码体系完整。建议处理顺序:
- [可靠性] O1:switch 前复查
IsRunning收窄 TOCTOU,并把占用导致的 rename 失败映射到app_running码。 - [一致性] O2:统一中途 ENOSPC 与预检的
disk_full码;文档写清预检为 fail-fast。 - [生命周期] O3:明确已完成下载 ZIP 的清理归属。
- [低优先] O4/O5:统一 package stage 命名;预留模型随 backup 策略复核。
声明:本报告为静态审计与攻击路径推演,逐项核了实现(含
verifyPackageSHA256单句柄链、io.MultiWriter每文件哈希、preExtractCheck强制性、failureCodeFor映射);因 WSL 无 Go 未执行测试,双 workspace 闸门通过为 Codex 自述。O1 的 Windows 文件锁语义需真机验证。