RFC-0071:OTA 停止

RFC-0071:OTA 后备
状态已接受
区域
  • 系统
说明

阻止设备跨版本边界向后 OTA。

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

摘要

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

设计初衷

当存储堆栈对文件系统格式进行重大更改时,它们会滚动格式的主版本号,以防止在较旧系统版本上运行的驱动程序尝试挂载和使用新格式的映像。

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

这样做有助于提升价值,因为:

  • 它对任何会保留状态的应用都非常有用。例如,维护 sqlite 数据库的应用,其架构可能会随时间而变化。
  • 具体而言,这对存储团队非常有用,因为他们过去不得不花费大量时间来排查最终由跨版本边界的反向 OTA 引起的问题。
  • 这将进一步说明 Fuchsia 不支持向后 OTA;它们只能尽力而为。

请务必注意,此提案不会更改支持哪些 OTA 序列以及不支持哪些 OTA 序列。它只是使此支持变为显式支持。OTA 后备的主要目的是防止开发者设备进入无效状态。对于生产设备,无向后 OTA 更新的不可变性应主要通过版本管理来强制执行。

如果没有此提案,当开发者尝试启动设备时,尝试跨不兼容边界进行向后 OTA 会导致问题(例如,驱动程序可能不支持文件系统格式)。采用此提案后,开发者可以在执行 OTA 之前发现此问题(并且错误会更加清晰),从而获得更好的开发者体验。

背景

术语

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

踏脚石 build 是指在 OTA 中无法跳过的 build。例如,假设有三个顺序版本 A、B 和 C。传统上,我们需要支持 A->BB->CA->C 中的 OTA。如果我们将 B 声明为踏脚石版本,这会移除 A->C 边缘,因此 A 升级到 C 的唯一方式是先 OTA A->B,然后 OTA B->C。在实践中,这对于风险较高的迁移以及减少我们需要测试的向前 OTA 的数量非常有用。

OTA 回退与分级更新之间的关系

OTA 后备和踏脚石版本都是我们必须执行安全迁移(例如存储格式迁移)的基本要素。本 RFC 不涵盖有关如何使用 OTA 回退和踏脚石版本的确切操作手册。不过,我们在此提供了一个示例,说明如何使用这些基元来支持安全迁移。

考虑进行存储格式迁移。我们可能会采取以下措施:

  1. 添加了对新格式的支持,但尚未启用/迁移该格式。提高 OTA 后备电源的版本号。
  2. 请稍等片刻。
  3. 使用上述某种迁移策略启用新格式。

如果我们确实迁移了设备,则可以执行以下两个后续步骤来实现清理:

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

借助这个过渡版本,我们可以假定设备将使用包含迁移代码的 build,因此我们可以移除对旧格式的读取支持。

在 (1) 中提高 OTA 后备版本可确保设备不会降级到不支持新格式的版本。

关于提高后备用金的政策

应根据需要一次性提高回退价。绝大多数更改都不需要后备缓冲区。如果此 RFC 获得批准,则应发布官方 Playbook 文档,其中说明了提高后备用量的具体步骤。与此同时,我们在此简要介绍了此政策。

在提交用于提前发布回退版本的 CL 时,作者应:

  • 提供指向 bugs.fuchsia.dev 上问题的链接,其中说明了为何需要进行升级,以及如果开发者绝对需要跨回退点降级设备,他们可以如何操作(例如,答案可能是刷写或铺平)。
  • 获得 //src/sys/pkg/OWNERS 批准。

设计

我们将引入一个 epoch.json 文件,使其同时出现在更新软件包和系统中。该文件应为包含两个字符串键的 JSON 文件:

  • “version”,应为 epoch.json 架构版本提供单个字符串值。实际上,在执行更新时,系统不会检查此值;此键仅用于在生产环境中进行 epoch.json 架构更改时明确说明。
  • “epoch”,应为 OTA 后备的单个整数值。如果更新软件包的纪元 < 系统的纪元,我们应在准备阶段使用 UNSUPPORTED_DOWNGRADE 让 OTA 失败。

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

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

为了安全地递增该纪元,我们还引入了一个 epoch_history 文件,该文件会通过构建系统编译为 epoch.jsonepoch_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 添加到 system-updater 的 out 目录。
  • 修改 BUILD,以便 epoch.json 也被添加到更新软件包中。
  • 系统更新程序应在准备阶段结束时检查 epoch.json
    • 如果更新软件包中没有 epoch.json,或者反序列化 epoch.json 时出现问题,请假定该纪元为 0。我们故意忽略错误,以便在 epoch.json 架构发生变化时仍能进行 OTA。
    • 如果 system-updater 的 out 目录中没有 epoch.json,或者反序列化它时出现问题,则失败,因为这是意外情况。考虑使用 include_str 宏从 out 目录读取数据。
    • 如果更新软件包中的纪元 < system-updater 中的纪元,则准备失败,原因为 UNSUPPORTED_DOWNGRADE。我们需要为 UNSUPPORTED_DOWNGRADE 创建新的 PrepareFailureReason

安全

这不是一项安全功能。不过,它可能会与安全功能互动,以改进开发者工作流。例如,假设有一个回滚保护功能,它会拒绝启动版本低于 N 的映像。如果我们在发布映像版本 N 时递增了纪元,这将阻止开发者降级无法启动的版本,因为这些降级将在 OTA 后备位置失败。

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

隐私权和效果注意事项

不适用

测试

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

此外,OTA e2e 测试将确保回退点不递减且格式有效。例如:

  • 如果 build N 降低了 OTA 后备,我们将在 CI 中从 build N-1 到 build N 的 OTA 中失败。
  • 如果 build N 在 system-updater 中生成无效的 epoch.json,我们将在 CI 中从 OTA 失败
  • N 构建为 N'

文档

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

此外,我们还需要修改:

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

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

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

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

另一种策略是正式支持所有向后 OTA。这并不切实,因为如果我们不知道未来会发生哪些变化,就无法编写能够适应未来变化的代码。

另一种策略是明确禁止所有向后 OTA(即使在其他情况下是可能的)。例如,我们可以自动为每个新 build 提高回退版本。我们决定不这样做,因为在实践中,有些开发者确实依赖于这些向后兼容的 OTA,而我们不希望破坏这些开发者的应用。

另一种方法可能是直接与 Fuchsia 平台版本控制集成(请参阅 RFC-0002)。不过,这其中存在几个模棱两可的问题。例如,是否应阻止 API 级别中的所有向后 OTA,还是应选择特定级别?我们会打破谁的记录?由于 Fuchsia 中已有为系统的不同部分使用不同版本标识符的先例(例如,文件系统有自己的版本标识符),因此这似乎是一个更简单的选项。

在先技术和参考文档

如需详细了解 OTA,请参阅 Android

致谢

James Sullivan 为动机和垫脚石部分做出了贡献。Zach Kirschenbaum 撰写了原始设计文档,并由 Dan Johnson 进行审核。