RFC-0145:Eager 软件包更新

RFC-0145:Eager 软件包更新
状态已接受
领域
  • 软件交付
说明

对系统单体式应用之外的软件包进行 Eager 更新。

问题
  • 83496
Gerrit 更改
  • 583842
作者
审核人
提交日期(年-月-日)2021-10-15
审核日期(年-月-日)2021-12-13

总结

一种机制,用于在完整系统更新之外更新软件包,并在重新启动后保留这些软件包,并考虑组件框架的交互和已更新软件包的验证流程。

设计初衷

我们需要提供一种机制,让软件包所有者在设备上发布软件更新,而无需单一的全局集成流程。这样一来,Fucsia 平台和基础系统就可以与单独的软件包体验(例如网络浏览器或支持数据)分开发布。

我们的目标是支持第一方 (1P) 产品和软件包,包括在 Fuchsia 树之外制造的产品。我们目前对支持第三方 (3P) 产品或软件包没有相关要求,但此设计不会妨碍我们以后添加对这些产品或软件包的支持。

利益相关方

教员hjfreyer@google.com

审核者

  • ampearce@google.com - 安全
  • computerdruid@google.com - 软件交付
  • geb@google.com - 组件框架
  • hjfreyer@google.com - FEC,组件平台
  • jsankey@google.com - 结构化配置
  • marvinpaul@google.com - 服务器基础架构
  • camrdale@google.com - 钴蓝色

咨询人员

列出应审核 RFC,但无需审批的人员。

  • aaronwood@google.com
  • abarth@google.com
  • bryanhenry@google.com
  • ddorwin@google.com
  • gstai@google.com
  • 软件交付团队

社交

此 RFC 已经经历了由审核人员、软件交付团队和潜在客户完成的多轮社交化文档。

RFC 格式定义

本文档中的关键字“必须”“不得”“必需”“会”“不会”“应”“不应”“建议”“可以”和“可选”应按 IETF RFC 2119 中的说明进行解释。

要求

Fuchsia 平台的要求

软件包更新

软件包可以独立于 Fuchsia 系统的其他更新。软件包可在不重新启动系统的情况下更新,这意味着软件包更新不能只作为系统更新的一部分进行。

组件关系

软件包所有者必须能够独立于基本系统限定多软件包组的发布,并仅针对该组中的软件包和组件推送更新。

软件包关系应可修改(或失效),以支持单个软件包更新。

注意:本 RFC 中没有完全满足此要求。它对应于下一阶段的工作,我们将在后续设计中加以解决。此设计没有任何因素妨碍我们以后满足这项要求。

A/B 更新:仅当软件包中的所有 blob 均已更新时,才使用软件包的更新版本

您应完整下载软件包并对其进行验证,然后再提交更新。

系统版本依赖项

快速更新的软件包可以声明依赖于 Fuchsia 平台特定 ABI 修订版本的依赖项(如 RFC-0002 定义,此为这项工作的依赖项),并且只能由支持其所需 ABI 修订版本的 Fuchsia 平台版本下载。

指标

软件包更新系统必须支持与系统更新流程类似的指标,并向软件包所有者提供一些指标,例如更新成功次数、下载时间、更新大小(以字节为单位,但目前尚不适用于系统更新)等。

我们必须允许软件包所有者根据这些组件的版本(基于正在运行的版本)获取其组件的指标,这不一定是最近提交到磁盘的版本。

下载更新时不阻止组件启动

也可解释为“不要让会话重启等事件变得非常慢”。如果我们在会话重启或重新启动时检查软件包的更新,并且没有 Eager 更新检查工具,则会导致会话重启速度明显变慢。这是因为设备可能需要下载整个软件包或一组软件包才能完成会话重启。

重新启动系统不应还原成功应用的更新的影响

如果设备在重新启动时无法访问网络,则不得在重新启动时回退到之前的软件包版本;并且在网络不稳定的情况下,我们不得阻止应用启动,除非该应用版本过旧。

我们可以定义关于软件包“充分过期”的具体政策,并在更新之前拒绝发布

如果应用的版本过旧且已知有更新可用,我们以后可能会进入“强制更新模式”,以防止启动已知有漏洞或已知有缺陷的软件版本。

客户端代码中具有单个网址的软件包必须能够根据设备上下文包含不同的代码或数据

许多开发者都不想或无法在所依赖的软件包有可用更新时更改代码或清单。一个示例可以在模块化配置中找到,该配置会对模块化组件的特定组件网址进行编码。开发者需要为依赖项中的每项更改更改这些配置,并且在其他情况下无法做到这一点。

这意味着,单个软件包网址必须根据使用位置和时间来表示软件包的不同版本(不过,给定软件包的更新规则也可以嵌入系统 ABI 等要求)。

由于单个组件网址不足以准确确定运行的软件版本,因此我们需要添加指标和反馈集成,以获取用于调试流程的信息。

从长远来看,如果解决方案需要一个全局性清单的兼容版本,则不可接受

多个客户可能希望发布软件包,而不与 Fuchsia 或产品所有者协调或集成。如果解决方案需要从服务器预定义的元组中包含所有软件包版本的单个清单,则解决方案可能无法满足此类要求。(这也会违反软件交付目标,即平台应能够生成可安装任意软件的产品,在产品构建时未知)。

在一天中的各个时段传播软件包下载情况

将软件包下载限制在一天中的特定时间(例如,会话重启前后)意味着设备可能会在其尝试更新期间长期离线。更新应在当天任何可用时段下载。

软件包托管和发布流程的要求

其中部分要求还会导致平台功能支持其服务器端实现。

软件包代码库

快速更新的软件包必须单独托管在发布基础架构上,并且独立于 Fuchsia 系统更新软件包和 blob。这可以保证托管 Fuchsia 平台软件包的代码库不会受到应用的任何软件包更改的影响。这意味着目前不会立即更新平台软件包,但我们稍后可能会放宽此限制。

如果产品希望提供自己的发布基础架构,只要满足此 RFC 中指定的要求和设计点即可。

更新频道

快速更新的软件包必须支持基于渠道的发布,使用的渠道名称可以不同于用于分发产品其余部分的一组渠道名称。例如,与运行它的产品相比,Chrome 会使用更多或更少的渠道。

这将需要客户端支持来协商正确的渠道分配。

分阶段发布

分阶段发布是对渠道管理的扩展,支持确保用户可以触发基于百分比的发布(例如 1%、10% 等),使应用遵循最佳管理做法。

还应提供紧急更新机制,以便将重要推送快速发布给所有用户。请注意,平台提供了一种紧急更新机制,允许在五小时内完成全部发布。注意:这超出了此 RFC 的范围,但是一项长期要求。

这将需要客户端支持来协商正确的分阶段发布组。

踏脚石

“步进石”是指任何设备在升级到最新可用版本之前都可能需要下载并运行软件包的 build。

目前,产品没有明确要求在短期内支持软件包的踏步机,但此设计将此设计视为长期要求,我们很可能会提出这样的要求。我们不会在此 RFC 的实现中明确为其构建支持,但以后不得排除对它们的支持。

这需要客户端支持来协商要下载的版本。

安全性要求

  • 显然不可使用来自可独立更新的代码库的软件包替换基础集中的软件包 - 请参阅可更新的软件包组的必备属性
  • 为了符合 Fuchsia Verified Execution(FVX) 要求,软件交付堆栈从磁盘加载到内存的任何内容都必须在加载时重新验证,而不是在下载时仅重新验证一次。请参阅有关交叉重新启动验证的部分
  • 使用户可以轻松了解和控制哪些软件包可以包含可执行文件
    • 请参阅可执行性控件。不包含可执行代码的软件包仍然具有安全性,但不在 FVX 实现的范围之内。
  • 让用户能够轻松审核软件包版本的内容。需要根据技术控制措施(而不仅仅是流程控制)对签名进行明确的审批流程。这尤其不在此 RFC 的讨论范围之内,但其他文档也涵盖这些内容。
  • 软件包必须采用经过验证的启动链所涵盖的防回滚机制 - 请参阅有关交叉重新启动验证的部分

设计

本部分将尝试捕获针对包含机器代码的软件包的软件包更新的整体流程。其中包括对可执行性和代码验证进行检查的位置以及开发者体验。我们将重点介绍预期状态与当前功能之间的差异,并据此制定实现策略。

我们将使用 Chrome 作为代表性的应用场景,因为它非常适合上述第一阶段:

  • 它是要更新的单个软件包(例如 chrome
    • 没有必须同步更新的依赖项
  • 软件包包含可执行的机器代码
  • 需要渠道和分阶段发布支持

可更新软件包的前提条件属性

本部分详细介绍了建议的设计在短期内支持的软件包类型。我们预计从长远来看,我们会放宽其中许多限制。

  • 在代码和配置文件中由非 fuchsia.com 网址引用,以避免与基础软件包混淆
  • 必须定位到特定的系统 ABI(依赖于 RFC-0002 的实现)
  • 不能依赖于 config-data 软件包(即,不能期望 config-data 随架构或内容中的软件包一起更改)
  • 可能依赖于结构化配置
  • 必须在 Fuchsia 全球集成和 Fuchsia 版本资格认证之外具备资格认证流程,该流程可在 SDK 包含的内容上运行
  • 必须占据基础映像的 2 个副本剩余空间的一半,而不假定使用基础映像文件进行重复信息删除(以便支持对软件包进行 A/B 更新)
  • 无法依赖于非基础软件包(即其他可更新的软件包)
  • 在完整系统 OTA 之外对软件包进行更新时,不得以向后不兼容的方式更改软件包中的组件公开的 ABI,也不得更改它们从其他非系统软件包使用的服务的预期 ABI(没有针对这些交互的版本控制策略)
    • 可以是单架构软件包,也可以是多架构软件包
  • 软件包的依赖项必须处理该软件包的解析错误(例如,某些具有回退版本的极端情况会尝试降级软件包,而 Fuchsia 将拒绝运行)。

概览

我们提议使用已用于生产系统更新检查的 Omaha 客户端来检查软件包更新。

我们将添加对 omaha-client 的支持,以检查多个软件包以及系统映像的更新。

我们会将 omaha-clientpkg-resolver 基础架构集成,以抽象化开发者在其本地机器上使用真正的 Omaha 服务器还是开发服务器。

我们将添加对可更新软件包的垃圾回收的支持,并采取安全措施来确保软件包更新不会阻止系统 OTA。

我们将与 Omaha 服务器基础架构集成,以协商给定软件包网址的合适版本。

我们将用例分为两个主要事件:更新检查和软件包下载以及软件包解析。前者会按计时器或由开发者手动请求运行,并触发软件包下载和缓存。每当组件框架解析软件包时,后者都会运行。

下文简要介绍了新的软件包解析架构。下文将对此进行详细介绍。

整体架构

更新检查和软件包下载

何时检查更新

omaha-client 运行可按“应用”配置的状态机(对 Omaha 而言,这意味着客户端要检查更新的一组软件)。

生产环境中系统更新的当前 omaha-client 检查间隔时间是五个小时,并且会自动抖动为正负 1 小时。我们提议为系统映像和软件包更新使用相同的更新检查间隔,以减少 Omaha 服务器的负载,并在有可用软件包的更新和可用更新时简化实现。(请注意,我们希望随着时间的推移,会将系统更新的检查与软件包更新的检查分离,无论是在间隔还是托管上。)这是短期的简化,可简化初始实现。)

在何处检查更新(代码库配置)

我们将为每个可立即更新的软件包添加一个新的 Omaha 客户端配置,并针对每个产品编译到 SWD 堆栈的配置中。这将包括系统上的哪些软件包网址应引用该软件包的配置。该配置将包括软件包的名称和网址,及其 Omaha 应用 ID 和其他默认配置选项(例如渠道)。

这样做有一个很好的安全副作用:即刻更新的软件包将无法替换 user\* build 中其他代码库的软件包,因为它们的主机名已硬编码为不同的值。快速更新的软件包永远无法替换基础软件包(注意:回退版本不在基本软件包中),因为在 /system/static_packages 列表中具有名称的软件包永远不会转到网络进行解析。

花瓣和 fuchsia.git 中的代码需要进行更新,从而不再引用软件包的 fuchsia.com 网址,而是引用与新代码库对应的主机名,例如 chrome-fuchsia-updates.googleusercontent.com

我们需要向软件包更新配置添加其他元数据,这些元数据将由软件包解析器解析。具体来说,我们将添加一个布尔值,用于指定软件包是否应包含可执行代码。此布尔值将用于运行时可执行性限制,类似于 pkgfs 中已包含的限制。如需了解详情,请参阅我们的可执行性控件部分。

在启动时,omaha-client 将获得源自 vbmeta 的可更新软件包的配置。目前无法配置运行时;这意味着,为了自动测试 omaha-client,我们需要像在 Omaha E2E 测试中一样继续构建 vbmeta。

文件包协商

我们将软件包协商定义为将软件包网址(例如 fuchsia-pkg://chromium.org/chrome)转换为 (hostname, merkle) 元组的过程。此转换的确切输出受多个变量的影响,包括:

  • 软件包设置为的更新渠道
  • 设备上正在运行的系统版本
  • 是否存在分阶段发布,以及更新检查工具所属的分阶段发布组。

我们建议主要在服务器端进行软件包协商,以便让客户端代码尽可能简单。

omaha-client 将向软件包更新配置中列出的 Omaha 服务器发送请求,并至少包含以下信息:

  • 应用 ID(这是 Omaha 服务的软件包标识符)
  • 软件包更新渠道
  • 软件包更新变种(通常在应用 ID 中编码)
    • 变种用于请求软件包的不同变体,例如包含调试符号或工具的软件包。由于系统将在应用 ID 中对其进行编码,因此在没有系统 OTA 的情况下,正式版设备无法更改在现场运行的变种。
  • 支持的系统 ABI(通常在平台版本字段中编码)
  • 分阶段发布成员资格
  • 当前的软件包版本(采用 A.B.C.D 格式,其中 A-D 是 32 位整数的字符串表示,遵循 Omaha 版本号规范),以便 Omaha 可以防止降级
    • OMCL 将从最近提交的软件包版本的 CUP 元数据或回退版本的 vbmeta 版本元数据中检索此信息,或者如果磁盘上当前没有软件包版本,请将此字段设置为 0.0.0.0
  • 支持的系统架构(例如 x64、arm64 等,与 Fuchsia 支持的架构列表中的名称匹配)

Omaha 服务器将计算设备的正确软件包版本,并发回一个要下载的 blob 的 Merkle 固定网址,其中包含要下载的三元组(软件包主机、软件包名称、Merkle 根)。我们必须先根据 Omaha 提供的 Merkle 检查响应的内容,然后再使用其数据。响应的格式可能与用于系统 OTA 的当前 update 软件包的格式一致。

响应将包含该应用的设备同类群组,其中包括频道信息。由于整个响应都会经过签名,因此我们可以使用此参数来有效地确定软件包在启动时的渠道。响应同类群组也用于更改后续请求的渠道(请参阅下文中有关渠道支持的部分)。

Omaha 响应必须实现客户端更新协议 (CUP),后者会在 Omaha 响应上提供签名。我们将为 Omaha 客户端添加对该协议的支持需要 CUP,这样我们才能将响应保存到磁盘,稍后根据存储在基础软件包中并基于 vbmeta 的公钥重新验证响应。

频道支持

Omaha 协议通过其 cohort 概念支持通道。

由于可更新的软件包使用的渠道集可能与基本系统不同,因此,我们需要为开发者提供一个类似于 fuchsia.update.channelcontrol 的新 API,以便管理处于开发阶段的软件包的渠道设置。我们会将频道信息存储在 CUP 响应中,与在 vbmeta 中存储频道信息以进行系统更新类似。我们打算通过扩展将此 API 作为 CLI 工具提供给 pkgctl,并可能向希望在运行时更改其组件渠道的组件开发者公开。

对于同时具有已提交更新和软件包回退版本的设备,即使回退版本较新(即使提交的版本低于退避版本),我们也应使用已提交更新中的渠道信息,以避免通过更新退避程序版本来覆盖渠道分配。我们仍必须验证已提交响应上的签名,然后才能使用其通道信息。

如果 Omaha 请求包含不存在的渠道,则 Omaha 服务器应重定向到存在的渠道,或者我们应使用回退版本。

使用 Fuchsia 设备上的频道控制 API 请求更改频道时,该设置不得在设备重新启动后保留,除非是带有频道所包含频道的持久签名 CUP 响应。根据政策,我们不想信任未签名数据。下一次更新检查将使用内存通道,在将正确的软件包版本提交到磁盘后,系统会存储该更新检查的响应。

系统映像中的 Omaha 配置应包含用于即时更新的软件包的默认渠道。如果对于某个给定软件包没有持久的 CUP 响应,Omaha 应使用默认渠道。

如果渠道变更会导致返回同一版本的软件包(因为软件包同时存在于两个渠道中),那么下载流程应照常进行。pkg-resolver 不会下载任何新的 blob,而是返回磁盘上已有的 blob。

支持分阶段发布

服务器根据服务器上的随机掷骰结果为设备分配同类群组(这是现有 Omaha 功能),该同类群组 ID 用于跟踪设备是否应接收分阶段发布模式。服务器端的每个应用 ID 的掷骰子结果都是独立的。

持久化所需的大小

Omaha 请求和 CUP 响应非常小,只有数百字节。我们可能需要每个软件包少于 1KB 来保留元数据。

软件包下载

一旦 omaha-client 获得 Omaha 服务器计算的最终 Merkle 固定的软件包网址,它就会使用新协议(可能名为 fuchsia.pkg.cup)触发将软件包下载到 pkg-resolver 组件。如果设备上已有所请求的软件包版本,pkg-resolver 将不会重新下载该版本。

如果设备上没有新版软件包,pkg-resolver 将下载新版本并让其打开所返回的目录(以防垃圾回收)。在将句柄返回到软件包目录之前,pkg-resolver 必须将 CUP 请求/响应对提交到存储空间以供日后重新验证。

pkg-resolver 必须使此目录句柄保持打开状态,以免软件包被垃圾回收(此设计假设我们进行基于开放软件包跟踪的垃圾回收,该功能正在进行中)。它必须将句柄放在旧版本的软件包中。

我们还将在 pkg-resolver 的配置中为 pkg-resolver 下载的每个可更新软件包添加大小限制。下载后,如果软件包大于配置的限制,pkg-resolver 将删除与该软件包关联的所有 blob,并在更新失败并显示适当的错误情况。

此流程示意图如下:

软件包下载流程

如果下载失败,会怎么样?

下载失败必须阻止提交更新检查的 CUP 结果。新的解析将继续使用之前提交的版本。

空间管理和垃圾回收

在下载软件包的更新时,我们需要维护一些与空间相关的不变量:

  • 始终有足够的空间来缓存运行核心产品体验所需的软件包(例如 Chrome)。
  • 系统始终会有足够的空间来安装系统更新,或者我们也可以创建足够的空间来安装系统更新。

如果允许即时更新软件包使用足够的不可回收空间来阻止系统 OTA,则需要 FDR 进行补救。为了防止此错误情况,需要对当前的垃圾回收流程进行更改。

我们的垃圾回收机制目前使用以下条件来保留软件包:

  • 如果软件包位于当前运行的基础软件包集中,请保留它
  • 如果该软件包是非基础软件包的最新解析版本,并且已在当前启动期间已解析,请保留它
  • 否则,请允许删除软件包。

这种策略目前行之有效,但也存在一些值得注意的缺点:

  • 它可能会删除将来正在运行的系统所需的软件包(如果当前启动期间尚未解析这些软件包)
  • 它可能不会删除在当前启动期间已解析但不再需要的软件包(例如,随着时间推移累积的测试软件包)

我们一直难以更改当前的垃圾回收方案,直到弃用 pkgfs 来托管软件包缓存。现在这项工作几乎已完成,我们可以对垃圾回收方案进行更改,使我们能够维护这些不变量,也就是阻止对当前正在使用的软件包进行垃圾回收的方案。

对于即时更新(在我们推出基于包裹跟踪的垃圾回收之前)的短期实现,我们将对系统更新和即时软件包更新采用严格的大小检查。这意味着,可更新的软件包必须适合产品所有者为其预算的预算空间,并且我们将在上传软件包时检查其空间要求(目前集中管理)。从长远来看,更好的垃圾回收实现将具有更高的灵活性。

如果我们的空间用完会发生什么?

尽管我们已尽最大努力在发布时检查软件包大小并确保所有内容均不超出预算,但设备在下载更新时肯定会用尽空间。

为了解决这个问题,我们将增加一个“核选项”来进行空间管理。当我们在 OTA 期间空间不足时,该选项根据新的重新启动原因 (OUT_OF_SPACE_PANIC) 重新启动设备,并在网络开启时立即运行 OTA,而不会等待检查间隔时间。这会触发垃圾回收。系统更新检查工具应阻止即时更新软件包,直到 OTA 完成。

我们将向 system-updater 添加一个配置选项,用于指示它是否应在出现 NO_SPACE 错误时重新启动系统。我们将为产品所有者提供在 user* build 上将此选项设置为 true 的选项,但在 _eng build 中应默认设置为 false 以帮助调试。如果该选项为 true,并且在尝试安装更新时 system-updater 空间不足,则 system-updater 会触发系统重新启动,原因为 OUT_OF_SPACE_PANIC

重新启动后,系统更新检查工具组件(omaha 客户端或 system-update-checker)会在启动时启动。如果更新检查工具组件发现重新启动的原因为 OUT_OF_SPACE_PANIC,则会立即尝试运行 OTA,同时禁止更新软件包。

此外,我们还会为 OUT_OF_SPACE_PANIC 重新启动添加退避时间,以确保如果设备连续多次重新启动,并且 OUT_OF_SPACE_PANIC 不会作为循环重新启动的原因。

从可更新的软件包启动组件

下次通过组件框架解析可更新软件包中的组件时,将会使用该组件。在某些产品上,每夜会话重启将自动处理组件重启,并且可能不需要更新通知。但在其他产品上,组件可能需要更新通知,以便向用户显示“请重启”通知。如需了解详情,请参阅未来工作部分。

新组件的解决流程

所有软件包网址仍将采用 fuchsia-pkg://{hostname}/{package_name} 格式。不过,由于某些软件包网址的协商将由设备上的 Omaha 客户端管理,而有些将由其他方法(如 cache_packagesbase_packages)管理,那么我们需要一个软件包解析流程,使软件包堆栈能够区分应通过 Omaha 管理的软件包和应通过其他元数据管理的软件包。

为此,我们提出了如下所示的解决流程。对于使用 fuchsia.pkg.PackageResolver 协议解析给定软件包网址的调用,pkg-resolver

  1. 应用基本固定规则和重写规则
  2. 确定要解析的重写网址是否由 Omaha + CUP 管理,如果是...
  3. 请求某个内部库来管理与该网址关联的目录句柄的 CUP 软件包。
  4. CUP 库将检索在该网址最近提交的 CUP 响应中缓存的 Merkle 指定的软件包,如果遇到错误且满足使用回退版本的要求,则回退到回退版本。对于它选择的软件包的版本,它会直接转到 pkg-cache 并返回一个目录句柄(以避免在写入新软件包时读取软件包的各种竞态条件)。

此流程示意图如下:

可更新的软件包解析流程

对可执行性的控制

pkg-resolver 是正式版设备上有效解析路径的一部分,这意味着它将需要参与 FVX 强制执行。我们需要信任的密钥列表将包括 CUP 密钥。

我们将在 pkg-resolver 中实现可执行性控件,以确保在 user* build 中,只有以下软件包可以包含可执行代码:

  • 进垒
  • 由 Omaha 管理并在 pkg-resolver 配置中配置为可执行文件
  • 已通过现有 pkgfs_static_packages 许可名单机制列入许可名单,便于执行可执行性控制

这些控制措施的具体实现将在后续文档中进行介绍。

在重新启动时验证下载的元数据

签名验证和可审核性

为了在重新启动后保留可更新的软件包,我们需要记住其 Merkle 根目录。我们将保留 Omaha 请求和 CUP 响应。在查找使用持久化数据的软件包的 Merkle 时,我们会验证:

  • 请求/响应对的签名与可信密钥匹配
  • 请求的效果相当于我们在设备连接到互联网的情况下请求的内容(与 Omaha 应用 ID、渠道和变种匹配),并且
  • 响应与签名参数匹配,并且是一个有效的响应;
  • 响应的预期版本大于或等于系统映像中的备份版本
  • 响应包含的软件包支持正在运行的系统 ABI 版本

如果满足所有这些条件,pkg-resolver 中的 CUP 模块可以从 CUP 响应中返回持久保留的 Merkle。

这种方法的缺点是:我们需要解析持久化的请求和响应,以获取持久化的 Merkle 根的值。不过,我们认为,通过隔离实际执行解析的组件,可以降低实现过程中的这种风险。

防回滚

此解决方案依赖于 vbmeta 提供的回滚保护,该保护使用基于版本的反向阻止。如果 CUP 响应中的预期版本号低于系统映像配置中要求的最低版本,我们会回退到 OTA 映像中包含的软件包版本(如果存在),并且不会提交响应中的版本。

为了通过该方案实现回滚防范的示例,我们假设可更新的软件包 1.0 版有一个漏洞,该漏洞已在 1.1 中得到修补。我们需要指示 Omaha 停止提供 1.0 并开始提供 1.1,然后向机群发出系统 OTA,其中具有新的 Backstop 版本 1.1。这样可以保证,在没有 Replay Protected Memory Block 或同等功能的设备上,这种行为不会比我们目前的单体式应用行为更糟糕。

对于未设置往返版本的软件包,我们将添加基于时间戳的备用,我们确认该备用密钥的存在时间早于该软件包的每个 CUP 响应。必须使用此模式,以便提前更新系统不知道的软件包。退避时间将设置为系统退避时间,该时间目前为生成系统映像的 Fuchsia build 的时间戳。目前,我们对于更新系统映像之前不知道的软件包没有要求,但在设计时应考虑到这些要求。

前期启动所需的软件包回退版本

在启动过程中或设备连接到网络之前可能需要可更新软件包中的某些软件包(例如,它们可以提供用于配置网络的开箱体验 (OOBE) 的界面)。这意味着,如果某些软件包已过时,我们不能依赖网络来下载这些软件包,而我们必须在系统更新映像中包含这些软件包的版本。我们将包含的这些软件包称为“回退版本”,因为它们通常只在没有软件包的其他更新版本时使用。

回退版本存在多种设计压力:

  • 它们必须能够在没有网络访问的情况下运行
  • 它们在恢复出厂设置 (FDR) 后必须保留,因为 OOBE 可能需要它们
  • 它们必须通过系统更新进行分发,并通过系统更新
  • 我们必须尽量减少回退版本使用的空间,尤其是与其他软件包版本一起使用时
    • 在这种情况下,我们必须存储回退版本、当前正在运行的版本以及尚未运行的新版软件包,这意味着我们需要同时存储特定软件包的三个副本(假设我们禁止系统更新和软件包更新同时进行)
  • 我们必须努力避免系统正在运行软件包的版本 N,而系统更新会强制降级到软件包的 N-1 版本,因为对于开发者来说,避免降级是一个很有用的属性。
  • 在防止软件包降级的同时,我们还必须避免由于降级而拒绝系统更新

给定的软件包所有者应该与产品所有者进行协调,决定是否需要其软件包的回退版本。所有可更新的软件包都不需要回退版本。

为了在 OTA 映像中添加回退版本,我们会将回退版本软件包放在系统更新软件包中,并在系统的 system_image 软件包中添加该软件包的版本控制元数据。版本控制元数据(主要是其 Merkle 根)将允许 pkg-resolver 响应针对软件包的请求,即使它没有网络连接也是如此。vbmeta 中包含版本控制元数据这一事实意味着,FDR 不会删除该元数据。

在系统更新期间,我们会对更新后的软件包进行垃圾回收并删除未提交的版本,但我们仍可能需要存储一个软件包的三个版本,其中包含一个回退版本:当前系统映像中的回退版本、已下载且可能正在运行的已提交版本,以及新系统映像中的新回退版本。

何时使用回退版本

当且仅当基于 CUP 元数据时,回退版本比磁盘上最近提交的软件包版本更新(具有更大的版本号),我们的 CUP 模块才会返回回退版本的目录。

如果最近更新的软件包版本已损坏,手动恢复出厂设置将清除存储的 CUP 元数据,并让我们能够在满足条件 1 的前提下重置为回退版本。将来,我们还可以使用 Blob 损坏处理程序来确定更新后的软件包是否已损坏,并使用回退版本(“未来工作”中会提到)。

从技术上讲,新系统映像中的回退版本有可能是从最近提交的软件包降级而来。不过,为了使我们能够使用该降级回退,新的系统映像必须不支持最近提交的软件包所需的 ABI。我们发现,在发布某个系统时,如果系统使用较新的系统 ABI 和较旧的回退版本无法支持较新的系统 ABI,我们仍会考虑添加服务器端检查,以防再次发生此类情况。

如果系统更新将回退版本升级在最近提交的版本之后,我们应根据 CUP 元数据中的版本字符串,优先使用最近更新的软件包版本。

指标

可更新的软件包中的指标

为了给第一方软件包开发者带来好处,我们将添加 Cobalt 指标,这些指标会按生成指标的组件的软件包版本自动汇总。Cobalt 在记录任意字符串(例如可更新软件包的哈希)方面存在一些当前限制。我们不打算直接在软件包格式中嵌入版本号或任意其他版本字符串,而是将这些版本号嵌入客户端的 CUP 元数据中,并据此向工具报告版本。由于 pkg-resolver 有权访问软件包的 CUP 元数据,因此能够向 Cobalt 报告已提交软件包的人类可读版本字符串。

您可以通过多种方法获取 Cobalt 指标,并且所有方法都需要支持版本信息:

  1. 通过组件直接进行日志记录
  2. 代表组件从某个组件进行日志记录,例如采样器,它会读取有关组件的检查数据并将其传播为 Cobalt 指标
  3. 从代表许多组件的组件进行日志记录,例如日志统计信息。这似乎类似于常见的情况 2,但它表示无论我们选择何种解决方案,都需要进行可伸缩性。

Fuchsia 使用 Cobalt 时需要满足一些集成可更新的软件包的要求:

  1. 组件所有者不希望使用按 Merkle root 细分的信息中心,他们更喜欢直观易懂的字符串
  2. 为了通过人类可读的版本字符串准确汇总,我们需要在设备上执行此操作
  3. 我们无法为软件包的每个新版本更新 Cobalt 注册表,因为这样做会破坏独立软件包更新发布流程的意义
  4. 提供给 Cobalt 的组件版本相关数据可能指不再运行的组件版本,如日志统计信息所示
  5. 短期解决方案不需要支持 v1 组件(因为它们无法动态更新),但我们确实需要报告每个可以更新的组件的版本字符串。Cobalt 需要每个软件包的版本字符串,但我们并不要求所有软件包都必须使用新格式才能为任何软件包启用版本报告。

为了将软件包的版本字符串记录到 Cobalt,我们提出了一个系统,通过该系统传播软件包的默克根以及指标,以便跟踪组件的哪个版本发出了给定指标。

我们将修改 Cobalt 日志记录协议,以允许提供可选的软件包标识符字段。Cobalt 组件本身具有与一组特定指标关联的 Merkle 根后,它可以从该 Merkle 根映射到基于字符串的版本字符串,并据此在本地汇总指标。它还可能会将此 Merkle 根与其他来源(如日志记录统计信息或采样器数据)的组件名称组合在一起。

根据 Cobalt 的隐私权要求,我们必须按设备上的版本字符串进行汇总,而不是将其发送到设备以外。考虑到设备上可能存在人类可读的版本信息,因此明智的做法是在设备上而不是后端系统上进行映射。

客户还希望使用人类可读的任意字符串来对其指标进行分区。目前,可以记录其版本的单个软件包支持此功能,但没有任何系统级系统或标准适用于此测试。Android 支持将其用作版本名称

为此,我们提议在 CUP 元数据中再添加一个字段,该字段作为 Omaha 响应的扩展进行传输,也称为 version-name。除了人工处理版本字符串之外,此字段不会用于任何其他用途,SWD 堆栈不保证其唯一性,也不会将其用于防止降级;软件包所有者需要维护自己的版本字符串。

验证版本字符串:新的 SWD API

出于隐私方面的考虑,Cobalt 会限制可记录的字符串类型。具体而言,Cobalt 不能信任不包含个人身份信息 (PII) 的任意字符串(例如版本字符串)。为了允许我们记录组件的版本字符串(Cobalt 无法提前知道版本字符串,因为组件可能会更新为任意版本),Cobalt 需要通过一种方式来验证该可信系统组件已知某个特定版本字符串。

Software Delivery 将与 Cobalt 团队合作验证软件包标识符(Merkle 根目录或软件包网址),并将其与人类可读的版本字符串相关联,以进行遥测。当 Cobalt 尝试根据软件包标识符查找给定软件包的版本时,SWD 堆栈会返回版本名称和版本号(如果存在)。

目前,我们无法保证与系统映像相关联的任意组件的版本信息,但之后我们可能会针对软件包中人类可读的版本信息对编码进行标准化,并在此扩大保证范围。

如果未在日志记录组件中向 Cobalt 提供软件包标识符(日志统计信息等项目可能存在这种情况),Cobalt 应根据其软件包标识符报告当前正在运行的组件的版本。

Software Delivery 堆栈希望 Cobalt 缓存对人类可读版本字符串的请求的响应。Cobalt 组件必须避免针对每个指标调用调用版本字符串 API,否则会产生不必要的资源成本。我们可能会选择为 Cobalt 创建一个定期调用的批量 API,以减少版本字符串的 FIDL 流量。

我们稍后可能决定将版本字符串和版本名称集成到软件包格式中,但目前不打算这样做。

关于更新系统本身的指标

我们还需要报告有关软件包下载过程本身的指标,例如成功/失败指标。如上所述,出于隐私方面的考虑,Cobalt 在记录任意字符串时存在一些限制。

但是,由于 Omaha 还具有后端指标,并且这些指标具有自己的隐私保护策略,因此 Omaha 本身可以提供运行特定版本设备的精确数量指标。与替代 TUF 相比,这是一种优势。

对于与更新流程本身相关的指标,我们计划利用 Omaha 的指标,并与 Cobalt 团队合作,在极少数情况下(通过可信系统组件进行验证)根据任意字符串来报告维度。

最后,我们会将来自更新基础架构的可更新软件包的版本记录到设备快照中,以帮助排查问题。

实现

我们可能会将这项工作拆分为多个阶段,相应的阶段会随着功能的增加而增加。

第一阶段可以包含基于 Omaha 的基本软件包提取协议,该协议需要:

  • SWD
    • omaha-client 中的多应用支持
    • 实现 CUP,集成到软件包解析流程中
    • 用于测试的极简开源 Omaha 服务器
    • 软件包配置,软件包网址与 Omaha 应用 ID 的映射。
  • 服务器端打包团队
    • 服务器端打包基础架构中的版本管理,以及渠道和降级保护等基本软件包协商
    • 用于存储软件包以及与 blob 存储区的集成。
    • 服务器端软件包协商(渠道支持、分阶段发布等)

第二阶段将引入生产环境所需的其他安全机制,以及空间管理所需的安全措施:

  • 西南德

    • 实现 CUP 后退
    • 空间管理:实现开放式包裹跟踪并全面改进垃圾回收,以实现更灵活的软件包管理。
    • pkg-cache 之外的可执行性限制
    • 针对特定应用的资格认证流程
    • 软件包的回退软件包,内置于基础映像中
    • 添加了 Cobalt API,用于验证版本字符串
  • 服务器端打包基础架构

    • 启用 CUP
    • 签名之前,发布时间出处和代码签名检查
    • 增强了软件包协商支持,具有分阶段发布、ABI 修订版本协商等功能。
    • 发布时大小检查
    • 添加了用于上传软件包的接口,以允许指定版本字符串

注意:本部分未介绍 RFC-0002 的实现,该 RFC-0002 是一个依赖项,但有自己的实现计划。

性能

我们预计此项变更不会对运行时性能产生重大影响。在推出此更改时,我们需要密切监控 omaha-client 以及 SWD 堆栈的其他部分的内存和 CPU 使用情况。我们还会监控启动和重新启动的时间,看看是否存在回归问题。

这项变更确实会造成磁盘使用量增加,具体形式是存储软件包所需的额外空间以及存储软件包元数据所需的少量额外空间。聊天室管理部分详细介绍了这些注意事项。

工效学设计

工程工作流

我们将构建新的 _eng 工作流,以使用 omaha-client 测试和验证软件包更新。我们预计大多数使用软件包的开发者在开发过程中都会使用基于 TUF 的工作流,但在我们面向生产环境发布平台更新之前,我们需要构建一个开源 Omaha 服务器来运行自动化测试。

我们预计,叠加在 SDK 工具之上的产品工具最终将在 eng build 中添加对 Omaha 配置的支持。在此之前,产品开发者可能必须直接与 Omaha 配置交互(这是设置软件包测试的工作流的一部分)。

我们还需要扩展命令行工具,以支持在运行时添加新的动态配置可更新软件包。

向后兼容性

这项工作应该向后兼容软件交付堆栈的现有使用情况,这主要是因为我们开发 Omaha 流程时可以保持 TUF 流,而不是更改软件包网址命名空间结构。

安全注意事项

有许多与此解决方案相关的安全注意事项。首先,此设计首次在最终用户设备上引入了间接验证。这是一种本质上风险更高的安全状况,但我们认为这种权衡是值得的,并且可以通过我们所有现有的防御和计划缓解。

以下是与 TUF 替代方案不同的安全敏感任务:

  • 我们需要添加新的可信协议及其解析器:CUP
  • 我们需要将 CUP 公钥添加到 vbmeta。
  • 我们需要将 omaha-client 设为 fuchsia.pkg.PackageResolver 协议的实现者
  • 我们需要在 Omaha 客户端中实施一定程度的可执行性限制,可能类似于“可执行性控件”中详述的 per-repo-config 可执行性位。

所有这些安全任务都易于处理,但需要详细的设计并与安全团队协调。

隐私注意事项

无法停用 Omaha 服务器在服务器端收集的任何指标 - 客户端必须将其执行环境(ABI 修订版本、应用 ID 等)提供给服务器,并且服务器可能会决定在该互动时保留指标。

对于 Google 的 Omaha 服务器,对 Omaha 指标的访问受到严格限制,但可以提供精确数据,了解已下载或正在运行特定版本软件的设备数量。

如需详细了解 Omaha 协议如何保护隐私,请参阅 Omaha 协议规范

测试

相关部分介绍了软件包版本的资格审查。

“实现”部分中详述的更改需要经过广泛测试。具体而言,我们需要添加对树内 Omaha 服务器的支持,以便针对 Omaha 客户端运行集成测试。我们还将投资进行集成测试,测试 Omaha 客户端和软件包解析器之间的新交互。

我们将使用新的 Omaha 服务器对新的更新流程进行端到端测试。

我们还需要严格测试新的垃圾回收机制,以纳入填充磁盘的集成测试,并确保仍可安装系统更新。

最后,我们需要对新增或更改的安全表面进行严格的验证:CUP 响应验证和持久性,以及可执行性限制。

文档

随着这些更改的发布,我们需要更新 fuchsia.dev 上的软件交付文档,至少要包括:

  • 可更新软件包的模型和状态转换图
  • 如何配置更新
  • Omaha 服务器实现需要满足什么条件

后续工作

更新 Notification API

在实现此初始方案之后,我们还将提供一个通知 API,供客户端订阅有关软件包的更新(这样他们就可以根据需要触发组件重启)。更新通知有两个用例:

当其组件被其父组件重新解析(可能是自行重启时或会话重启时)时,软件包解析将使用软件包的新版本。

我们提出了一种新的组件解析器 API,该 API 可以:

  • 解析组件
  • 提供一个渠道,用于获取有关该组件可用更新的通知的渠道
  • 当新渠道下载完成且有新版本可用时,通知渠道另一端的持有者,然后提供自重启机制

我们将通过组件解析器将通知和触发逻辑整合到软件包解析器中,该解析器将提供类似的 API。我们还将添加一项功能,让组件向 framework 发出请求,以通知组件其自身有更新,从而让组件能够触发自己的重启。

有关更新软件包的 Blob 损坏通知

如果某个软件包下载后出现损坏,我们应尝试重新下载损坏的 blob,或者使用该软件包的回退版本(如果存在)(前提是它满足使用回退版本的要求)。

这涉及与现有的 blob 损坏通知 API 集成,以及触发重新下载或擦除已损坏的可更新软件包的缓存 CUP 元数据。

无法启动可更新软件包的前台更新

如果可更新的软件包中的某个组件无法启动,我们应向产品所有者提供相关选项,使其能够在软件包系统重新下载该软件包时显示前台更新屏幕。这样可以防止用户需要通过 FDR 对设备获取正在运行的软件包的回退版本。

更新可更新组件的自检

某些组件可能希望能够自行检查更新并触发下载。我们应调查此功能的客户有哪些,以及我们是否应将其集成到组件框架和软件交付 API 中。

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

替代方案:使用 TUF 进行软件包协商

我们可以使用更新框架来分发软件包版本元数据,并将特定 fuchsia-pkg:// 网址转换为要下载的 blob 的主机名和 blob,而不是使用基于 Omaha 的协议。

其基本设计如下:

  • pkg-resolver 添加“Eager 更新”函数,用于复制当前 omaha-client 状态机的各个方面,并定期检查软件包组的更新。
  • 通过改换 TUF 元数据格式并在 Fuchsia 更新服务器上实现该新格式,实现频道、分阶段发布、步进石等功能。这是一项非常重要的工作。
  • 当 pkg-resolver 收到对特定软件包网址的请求时,让它下载该代码库中所有软件包的新元数据(如果没有网络或没有通向更新服务器的路由,请使用持久性元数据),并根据请求的上下文(包括频道、分阶段发布成员资格等)计算正确的 Merkle 根,所有这些都必须在设备本地保留。这意味着服务器无法根据上下文将其响应更改为客户端,所有计算都将在本地完成。
  • 存储每个紧急更新软件包的已下载元数据,这可能很重要,具体取决于 TUF 代码库中存在多少频道和版本(根据我们的估算,如果采用简单方法,每个软件包大约 3MiB,如果我们要启用 TUF 元数据分片需要更多工作,则每个软件包大约 82KiB)
  • 确定客户可以接受的基于 Cobalt 的指标方法(针对运行特定版本的设备数量等提供足够的精确度),并且不违反 Cobalt 的设计原则
  • 通过 ffx 添加工程工作流以支持即时更新代码库,并更新开发者工具以支持新的 TUF 元数据格式。
  • 实现防降级机制(Omaha 在服务器端提供此功能)。

这种替代方案有几个缺点:

  • 客户端可以“检查其工作”,因为 TUF 元数据持久性意味着证明软件包的正确版本所需的所有数据都位于设备上。
  • 我们已经将 TUF 用于工程流程,因此已经有了很多适用于它的工具。
  • 与 TUF 代码库和软件包名称对应的软件包网址的概念很有用,不需要映射到其他服务专用 ID(如 Omaha 应用 ID)。

使用这种备选方案也有缺点:

  • 我们已经将 Omaha 用于生产环境中的系统更新,因此这将引入运行正式版 Fuchsia 设备所需的第二个更新服务器。
  • 我们必须重新实现 Omaha 已经提供的许多功能,包括分阶段发布、渠道支持和跳跃功能。
  • 如果系统映像中的软件包协商代码有误,我们必须通过 OTA 设备进行修复,而不是在服务器端修复协商逻辑

我们认为,这种方法的缺点大于利弊,并且这两种方法之间的差异几乎可以忽略,因此我们采用了上述基于奥马哈的方法。

早期技术和参考资料

可更新的软件包有很多实现。以下是一些重要因素:

更新元数据协议的参考内容如下:

  • Omaha 规范,很多 Google 产品和非 Google 产品(包括 Fuchsia)均采用该规范。
  • 更新框架规范,此规范目前在某些地方被 Fuchsia 采用。

附录:发布时的大小计算

对于具有严格大小预算的设备,我们不希望占用过多空间来干扰 OTA,因此我们需要为软件包提供可选的大小预算,并在软件包发布时检查软件包。

目前,Fuchsia 会在资源写入磁盘时对其进行流式压缩,并且该压缩级别在系统组装时确定。为了预先计算指定软件包将占用的空间,我们需要知道给定系统版本的压缩级别,我们提议通过将 blobfs 压缩级别和压缩方案设置为系统 ABI 的一部分来实现这一目标。这样,我们就可以在服务器上使用相同的压缩参数来检查软件包是否适合其预算,并将软件包传递到软件包代码库,还是无法发布。

在发布过程中,软件包服务器基础架构将检查应用的压缩大小(使用 SDK 中的目标 ABI 的压缩参数和工具压缩的 blob 集的累计大小)是否符合为目标设备上的软件包配置的预算。

无论在发布过程中我们执行的大小检查效果如何,都有可能出现这样的情况:我们最终会破坏大小检查流程,并且在尝试下载更新时,设备会用尽空间。如需详细了解如何对此进行管理,请参阅我们的空间管理和垃圾回收部分。

除了通过模仿在设备上进行的压缩来检查大小限制,我们还可以实现离线压缩,这样我们就可以确切地知道软件包中的一组给定文件会占用多少空间。不过,这项工作尚未优先处理,可能需要对 SWD 和 Storage 代码库进行重大更改。