RFC-0071:OTA 停止

RFC-0071:OTA 往返
状态已接受
领域
  • 系统
说明

阻止设备跨版本边界向后进行 OTA 更新。

问题
Gerrit 更改
  • 481013
作者
审核人
提交日期(年-月-日)2021-02-03
审核日期(年-月-日)2021-02-24

总结

本文档提出了一项计划,以防止设备跨版本边界向后安装无线下载 (OTA) 更新。

设计初衷

当存储堆栈对文件系统格式做出破坏性更改时,它们会引入格式的主要版本号,从而防止在较低系统版本上运行的驱动程序尝试装载和使用新格式的映像。

在系统更新堆栈中有等效的版本号可防止用户尝试通过 OTA 回退到不支持其设备所含文件系统映像的驱动程序版本。换句话说:它可以让我们在设备死机之前失败“向后 OTA”操作。

这会增加价值,原因如下:

  • 但这对于任何保留状态的应用来说都非常实用例如,应用维护 sqlite 数据库,该数据库的架构可能会随时间发生变化。
  • 具体而言,它对存储团队非常有用,因为他们过去不得不投入大量时间来对最终由跨版本边界的反向 OTA 导致的问题进行分类。
  • 这将强调 Fuchsia 不支持后向 OTA;这些是尽最大努力努力的结果。

请务必注意,此方案不会更改支持的 OTA 序列和不支持的 OTA 序列。只是明确了这项支持。OTA 后端的主要用途是防止开发者设备进入无效状态。对于正式版设备,无向后 OTA 不变应主要由版本管理来强制执行。

如果没有此方案,尝试跨不兼容的边界进行向后 OTA 会导致在开发者尝试启动设备时出现问题(例如,驱动程序可能不支持文件系统格式)。通过该方案,开发者可以在 OTA 之前了解这一点(错误也就更加清楚了),从而为开发者提供更好的体验。

背景

术语

OTA 是一种用于升级底层操作系统的机制。Fuchsia 设备可以接收和安装系统和应用软件的 OTA 更新。

Stepping Stone build 是指无法在 OTA 中跳过的 build。例如,假设有三个序列版本 A、B 和 C。过去,我们需要支持来自 A->BB->CA->C 的 OTA。如果我们声明 B 为 步进版本,则会移除 A->C 边缘,因此 A 升级到 C 的唯一方法是先 OTA A->B 升级到 B->C。在实践中,这对于有风险的迁移以及减少我们需要测试的正向 OTA 数量非常有用。

OTA 后端与踏步石的关系

OTA 备份版本和步进石版本都是我们必须执行安全迁移(例如存储格式迁移)的原语。有关如何使用 OTA 备份和步进石版本的确切策略不在本 RFC 的讨论范围内。不过,我们在这里提供了一个示例,说明可以如何使用这些基元来支持安全迁移。

考虑进行存储格式迁移。我们可能会采取的步骤如下:

  1. 添加对新格式的支持,但先不要启用/迁移它。提升 OTA 后端。
  2. 稍等片刻。
  3. 使用上述迁移策略之一启用新格式。

对于确实迁移了设备的情况,我们可以再执行两个步骤来实现清理:

  1. 剪切了包含 (3) 的踏脚石版本。
  2. 移除迁移代码和对旧格式的支持。

通过跳跃版本,我们可以假设设备将完成包含迁移代码的 build,这样我们以后就可以取消对旧格式的读取支持。

在 (1) 中提升 OTA 反向阻止可确保设备不会降级到不支持新格式的版本。

打消后盾的策略

应根据需要一次性停用后退器。绝大多数更改都不需要后盾触碰。如果此 RFC 获得批准,则应发布官方手册文档,说明提升后盾的具体步骤。在此期间,我们在此建议简要概述此政策。

在建议 CL 来增强支持措施时,作者应该:

  • 请提供一个指向 bug.fuchsia.dev 上问题的链接,其中说明了必须执行提升的原因,以及开发者在绝对需要通过后退装置降级设备(例如,答案可能是刷写或铺路)的情况下应如何继续。
  • 获得 //src/sys/pkg/OWNERS 的批准。

设计

我们来引入一个 epoch.json 文件,该文件可同时存在于更新软件包和系统中。它应该是包含两个字符串键的 JSON 文件:

  • “version”,它应该具有 epoch.json 架构版本的单个字符串值。实际上,执行更新时不会检查此项。此键的存在只是为了显而易见地在生产环境中进行 epoch.json 架构更改。
  • “epoch”,它应该具有一个用于 OTA 往返的整数值。如果更新软件包的周期小于系统周期,则应在准备阶段使 OTA 失败并返回 UNSUPPORTED_DOWNGRADE

例如,epoch.json 可能如下所示:

{
  "version": "1",
  "epoch": 5
}

为了安全地提升周期,我们还要引入一个通过构建系统编译到 epoch.json 中的 epoch_history 文件。epoch_history 文件的格式可能如下:

0=Initial epoch (https://fxbug.dev/42144857)
1=Storage format migration (https://fxbug.dev/XXXXX)
...
N=Most recent change (https://fxbug.dev/YYYYY)

每次引入向后不兼容的更改时,都应手动递增 epoch_history 文件。

虽然中间 epoch_history 文件增加了另一层复杂性,但这种方法的优势在于:

  • 它提供所有版本递增更改的日志(强制记录文档!)
  • 如果两个人出于不同原因尝试颠倒周期,就会产生合并冲突。

实现

这些更改将完全在平台(特别是系统更新堆栈)中进行。

为了实施变更,我们需要:

  • epoch_history 添加到 //src/sys/pkg/bin/system-updater。
    • 此外,创建一个将 epoch_history 转换为 epoch.json 的脚本。
    • 让构建系统使用此脚本将 epoch.json 添加到系统更新程序的输出目录中。
  • 修改 BUILD,以将 epoch.json 也放入更新软件包中。
  • 系统更新程序应在 Prepare 阶段结束时检查 epoch.json
    • 如果更新软件包中没有 epoch.json,或者对其进行反序列化时出现问题,则假设周期为 0。我们故意忽略错误,这样一来,如果 epoch.json 架构发生更改,我们仍然可以进行 OTA 更新。
    • 如果系统更新程序的 out 目录中没有 epoch.json,或者如果对其进行反序列化时出现问题,则会失败,因为这是意外情况。请考虑使用 include_str 宏从输出目录读取数据。
    • 如果 system-updater 中的更新软件包中的 epoch 小于 epoch,则准备失败,原因为 UNSUPPORTED_DOWNGRADE。我们需要为 UNSUPPORTED_DOWNGRADE 创建一个新的 PrepareFailureReason

安全性

这不是一项安全功能。但是,它可与安全功能交互,以改进开发者工作流程。例如,假设有一个回滚保护功能,该功能会拒绝启动版本低于 N 的映像。如果在发布映像版本 N 时递增周期,将阻止开发者降级不可启动的版本,因为这些降级将在 OTA 退避时失败。

除此之外,我们选择将 epoch.json 嵌入到系统更新程序二进制文件(而不是配置数据)中,以使 OTA 能够适应配置数据损坏。

隐私权和性能注意事项

不适用

测试

我们可以在 //src/sys/pkg 中使用现有的系统更新测试框架,该框架混合了单元测试和集成测试。

此外,OTA e2e 测试将确保退避算法不会减少,并且格式有效。例如:

  • 如果 build N 降低了 OTA 后端,CI 到 OTA 将从 build N-1N 失败。
  • 如果 build N 在系统更新程序中生成无效的 epoch.json,则从 CI 到 OTA 的 CI 将失败
  • build NN'

文档

我们需要创建一个文档来说明用于更新 epoch_history 的政策。

此外,我们还需要修改以下内容:

缺点、替代方案和未知情况

实施此方案的费用是多少?

实现此方案的主要代价是平台复杂性增加,因为我们要向平台添加另一个版本标识符。

还有哪些策略可以解决同样的问题?

另一种策略是正式支持所有向后 OTA。这不切实际,因为如果我们不知道这些更改的内容,就无法编写适应未来更改的代码。

另一种策略是明确禁止所有向后 OTA(即使是原本可能发生的 OTA)。例如,我们可以在每个新 build 中自动碰撞阻挡。我们决定不这样做,因为在实践中,有些开发者确实依赖于这些向后 OTA,并且我们不希望让这些开发者无法正常工作。

另一种方法是直接与 Fuchsia 平台版本控制集成(请参阅 RFC-0002)。不过,这还有几个含糊不清的问题。例如,是否应该阻止某个 API 级别的所有向后 OTA,还是应该选择特定级别?我们要破解谁呢?由于在 Fuchsia 上曾有为系统的不同部分使用不同的版本标识符(例如,文件系统有自己的版本标识符)的先例,因此它似乎是一个更简单的选项。

早期技术和参考资料

Android 提供了有关 OTA 的更多信息。

致谢

James Sullivan 为该公司的动力和踏板运动做出了贡献。原始设计文档由 Zach Kirschenbaum 撰写,由 Dan Johnson 审核。