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

@ -170,11 +170,16 @@ func RollbackIfUnconfirmed(dataDir, binPath string) (bool, string, string, error
}
// 先备份"当前这个坏的"——万一回滚目标也有问题,至少能回到现场
//
// ★ 落在 dataDir 下,不是二进制目录:二进制目录通常是 /usr/local/bin,
// 只读(ProtectSystem=strict)⇒ MkdirAll 静默失败(旧代码没查 err),
// 现场丢失且日志里只有一条无关的 copy 失败。
ts := NowStamp()
brokenDir := filepath.Join(filepath.Dir(binPath), "homed-broken-"+ts)
os.MkdirAll(brokenDir, 0755)
if err := CopyFile(binPath, filepath.Join(brokenDir, "homed")); err != nil {
brokenDir := BrokenDirPath(dataDir, ts)
if err := os.MkdirAll(brokenDir, 0755); err != nil {
// 备份失败不阻断回滚:回滚是主线,留现场是支线
log.Printf("[update] create broken-binary dir %s failed: %v", brokenDir, err)
} else if err := CopyFile(binPath, filepath.Join(brokenDir, "homed")); err != nil {
log.Printf("[update] backup of broken binary failed: %v", err)
} else {
log.Printf("[update] broken binary kept at %s", brokenDir)