From 1ad00f8ef217a8b65b2a489c35f1787c94bb1b1b Mon Sep 17 00:00:00 2001 From: ila Date: Sun, 19 Jul 2026 19:44:57 +0800 Subject: [PATCH] Add Phase 3 install integration review (T-301~T-303) 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 --- docs/review/phase3-review.md | 93 ++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 docs/review/phase3-review.md diff --git a/docs/review/phase3-review.md b/docs/review/phase3-review.md new file mode 100644 index 0000000..b86fa25 --- /dev/null +++ b/docs/review/phase3-review.md @@ -0,0 +1,93 @@ +# 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 安装验证顺序):** + +1. `expectation.validate()`:Catalog 期望校验(size>0、SHA-256 32 字节、app 字段合法)。 +2. `openArchiveFile(zipPath, expectation.Size)` → **`verifyPackageSHA256` 在解析任何 ZIP 结构之前**用 `io.NewSectionReader` 全量哈希并 `subtle.ConstantTimeCompare`;哈希后 `Stat` 复查 `Size`/`IsRegular` 防并发改动。 +3. `scanOpenedArchive`(中央目录预扫描)→ `zip.NewReader` → `preflight`(路径/上限/结构)→ `readPackageAppManifest`(app.json,限 1 MiB)→ `manifest.matches(expectation.App)`。 +4. `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 信任根、强制不可绕过的预检)都到位,失败码体系完整。建议处理顺序: + +1. **[可靠性]** O1:switch 前复查 `IsRunning` 收窄 TOCTOU,并把占用导致的 rename 失败映射到 `app_running` 码。 +2. **[一致性]** O2:统一中途 ENOSPC 与预检的 `disk_full` 码;文档写清预检为 fail-fast。 +3. **[生命周期]** O3:明确已完成下载 ZIP 的清理归属。 +4. **[低优先]** O4/O5:统一 package stage 命名;预留模型随 backup 策略复核。 + +> 声明:本报告为静态审计与攻击路径推演,逐项核了实现(含 `verifyPackageSHA256` 单句柄链、`io.MultiWriter` 每文件哈希、`preExtractCheck` 强制性、`failureCodeFor` 映射);因 WSL 无 Go 未执行测试,双 workspace 闸门通过为 Codex 自述。O1 的 Windows 文件锁语义需真机验证。