fix(update): 补上内核升级缺失的四道安全闸(H1–H4)

四条缺陷的共同点都是「静默产出一个看起来成功的升级」,而失败的表现
是机器进崩溃循环且没人知道原因。

H1 dataDir 为空必须拒绝升级
  Apply 此前只在 dataDir 非空时写 pending,空值仅跳过标记、后照常替换
  二进制 ⇒ 产出「无回滚能力」的升级。现改为在动任何文件之前直接失败。
  另新增导出 update.ResolveDataDir(env → <exe>/data 兜底)。

H2 备份目录不能写 /root
  backup 原为 /root/homed-upgrade-backup-<ts>,服务在 ProtectSystem=strict
  + ReadWritePaths=/var/lib/hologram 下运行,非 root 对 /root 无写权限
  ⇒ MkdirAll 必 EACCES ⇒ 首次升级必挂。改为落在 dataDir 下。
  rollback 的 homed-broken-<ts> 同样从(通常只读的)二进制目录挪到 dataDir。

H3 备份目录要有 GC
  旧注释写「确认稳定后可删」,但全仓无任何删除逻辑。新增
  PruneOldBackups:保留最近 3 个 homed-upgrade-backup-* 与最近 1 个
  homed-broken-*,其余 RemoveAll,在 Apply 成功返回前调用。
  只处理名字精确为「前缀 + NowStamp 形态」的目录,不误删 pending 标记
  或前缀相近的第三方目录。

H4 结果事件吞错误 + 成功不发 result
  publishResult 里 `if errMsg != "" { _ = errMsg }` 把失败原因丢弃,payload
  也没有 error 字段 ⇒ 订阅方只看到 ok=false。现在 error 字段恒定存在。
  Apply 成功路径此前从不发 result,现补发 ok=true。

测试:kernel_apply_test.go 新增 5 个用例(含题目要求的四个函数名)。
This commit is contained in:
JianFeeeee
2026-10-28 17:20:35 +08:00
parent 7f68d2b1bf
commit ecde8dd515
5 changed files with 626 additions and 25 deletions

View File

@ -42,6 +42,16 @@ func publishProgress(bus *events.Bus, scope, name, step, detail string) {
}
// publishResult 发布升级最终结果(成功或失败,含回滚轨迹)。
//
// ★ error 字段**总是**出现在 payload 里(成功时为空串)。
//
// 此前失败路径把 errMsg 收进一个 `if errMsg != "" { _ = errMsg }` 分支
// 直接丢掉了 —— 于是订阅方只能看到 ok=false,**看不到失败原因**,
// 而"为什么失败"正是用户唯一想知道的。字段固定存在(即使为空)还让
// 订阅方不必做"字段有没有"的兼容分支。
//
// ★ 顺序约定:调用方必须在动作**之前**发事件(见文件头注释)。
// 成功路径也一样:result 发出后调用方可能立即重启,进程随即消失。
func publishResult(bus *events.Bus, scope, name string, ok bool, from, to string, rolledBack bool, errMsg string) {
if bus == nil {
return
@ -56,12 +66,9 @@ func publishResult(bus *events.Bus, scope, name string, ok bool, from, to string
"from": from,
"to": to,
"rolled_back": rolledBack,
"error": errMsg,
},
})
if errMsg != "" {
// 错误单独放,避免成功路径也带一个空 error 字段
_ = errMsg
}
}
// publishCheck 发布一次更新检查结果(只读,不改动任何东西)。