| RFC-0145:急切的软件包更新 | |
|---|---|
| 状态 | 已接受 |
| 区域 |
|
| 说明 | 对系统单体之外的软件包进行急切更新。 |
| 问题 | |
| Gerrit 更改 | |
| 作者 | |
| 审核人 | |
| 提交日期(年-月-日) | 2021-10-15 |
| 审核日期(年-月-日) | 2021-12-13 |
摘要
一种用于在完整系统更新之外更新软件包并在重新启动后保持这些软件包的机制,同时考虑了组件框架交互和更新软件包的验证流程。
设计初衷
我们需要为软件包所有者提供一种机制,以便他们向设备上的软件发布更新,而无需进行单一的全局集成流程。这样一来,Fuchsia 平台和基础系统就可以与各个软件包体验(例如 Web 浏览器或支持数据)分开发布。
我们的目标是支持第一方 (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- Cobalt
已咨询:
列出应审核 RFC 但无需批准的人员。
aaronwood@google.comabarth@google.combryanhenry@google.comddorwin@google.comgstai@google.com- 软件交付团队
共同化:
此 RFC 已作为文档在内部进行了多轮意见征集,征集对象包括审核者、软件交付团队和潜在客户。
RFC 格式定义
本文档中的关键字“必须”“不得”“必需”“会”“不会”“应”“不应”“建议”“可以”和“可选”应按照 IETF RFC 2119 中的描述进行解释。
要求
Fuchsia 平台的要求
软件包更新
软件包可以独立于 Fuchsia 系统的其余部分进行更新。软件包可以在不重新启动系统的情况下进行更新,这意味着软件包更新不能仅作为系统更新的一部分提供。
组件关系
软件包所有者必须能够独立于基本系统来确定多软件包组的发布版本,并仅针对该组中的软件包和组件推送更新。
软件包关系应可修改(或可设为 null),以仅支持单个软件包更新。
注意:我们不会在此 RFC 中完全满足此要求。它对应于下一阶段的工作,我们将在后续设计中解决此问题。此设计中的任何内容都不应妨碍我们在未来满足此要求。
A/B 更新:仅当软件包中的所有 blob 都已更新时,才使用软件包的更新版本
在提交更新之前,应先完全下载并验证软件包。
系统版本依赖项
急切更新的软件包可能会声明对 Fuchsia 平台的特定 ABI 修订版本(如 RFC-0002 所定义,这是此工作的依赖项)的依赖关系,并且只能由支持其所需 ABI 修订版本的 Fuchsia 平台版本下载。
指标
软件包更新系统必须支持与系统更新流程类似的指标,并向软件包所有者提供指标,例如成功更新、下载时间、更新大小(以字节为单位)(系统更新目前没有此指标)等。
我们必须允许软件包所有者根据其组件的版本(基于正在运行的版本,不一定是最近提交到磁盘的版本)获取组件的指标。
在下载更新时,不阻止组件启动
也可解读为“不要让会话重启等事件变得非常缓慢”。如果我们在会话重启或重新启动时检查软件包的更新,并且没有急切的更新检查器,则很容易导致会话重启速度变慢很多。这是因为设备可能必须下载整个软件包或一组软件包才能完成会话重启。
重新启动系统不应还原成功应用更新的效果
如果设备在重新启动时无法访问网络,我们不得在重新启动时回退到软件包的先前版本;如果网络不稳定,我们不得阻止应用启动,除非该应用过时严重。
我们可以定义政策,规定软件包“过时”的程度,并拒绝启动,直到软件包更新为止
如果应用严重过时,并且已知有更新可用,我们未来可能会进入“强制更新模式”,以防止启动已知存在漏洞或已知存在问题的软件版本。
如果客户端代码中的软件包包含单个网址,则必须能够根据设备上下文包含不同的代码或数据
许多开发者不希望或无法在每次有依赖软件包的可用更新时都更改代码或清单。一个示例是模块化配置,它会为模块化组件编码特定的组件网址。如果每次依赖项发生更改都需要更改这些配置,开发者体验会很糟糕,而且在某些情况下,这种做法根本不可行。
这意味着,单个软件包网址必须表示软件包的潜在不同版本,具体取决于使用该网址的位置和时间(不过,给定软件包的更新规则也可以嵌入系统 ABI 等要求)。
由于单个组件网址不足以确定正在运行的软件的确切版本,因此我们需要添加指标和反馈集成,以便获取该信息以用于调试流程。
从长远来看,需要使用兼容版本的单个全局清单的解决方案是不可接受的
许多客户可能希望在不与 Fuchsia 或产品所有者协调或集成的情况下发布软件包。如果解决方案需要从服务器预定义的元组中获取所有软件包版本的单个清单,则可能无法满足此要求。(这也会违反软件交付目标,即平台应能够生成可安装任意软件的产品,而不是在产品构建时已知的软件)。
将软件包下载分散到一天中的各个时间段
将软件包下载限制在一天中的特定时间(例如,会话重启前后)意味着设备在尝试更新的时间窗口内可能会长期处于离线状态。更新应在一天中的任何可用时段下载。
软件包托管和发布流程的要求
其中一些要求还会导致平台功能支持其服务器端实现。
软件包代码库
急切更新的软件包必须单独托管在发布基础架构上,并且独立于 Fuchsia 系统更新软件包和 blob。这样可保证托管 Fuchsia 平台软件包的代码库不受应用所做的任何软件包更改的影响。这意味着,目前平台软件包不会急切更新,不过我们日后可能会放宽此限制。
如果产品希望提供自己的发布基础架构,只要符合本 RFC 中指定的要求和设计要点,就可以这样做。
更新渠道
热切更新的软件包必须支持基于渠道的发布,使用可能与用于分发产品其余部分的渠道集不同的渠道名称。例如,Chrome 可能希望使用比其运行所在产品所用通道更多或更少的通道。
这需要客户端支持来协商正确的渠道分配。
分阶段发布
分阶段发布是渠道管理功能的扩展,该支持服务可确保用户能够为其应用触发基于百分比的发布(例如 1%、10% 等),从而遵循最佳管理实践。
还应提供紧急更新机制,以便快速向 100% 的用户推出重要推送。请注意,平台提供紧急更新机制,可在 5 小时内实现 100% 的发布。注意:此要求不在本 RFC 的范围内,但属于长期要求。
这需要客户端支持来协商正确的分阶段发布群组。
垫脚石
“跳板”是指软件包的 build,任何设备在升级到最新可用版本之前可能都需要下载并运行该 build。
目前,我们没有明确的产品要求在短期内支持软件包的垫脚石,但此设计将它们视为长期要求,我们很可能会收到相关请求。我们不会在此 RFC 的实现中明确构建对这些功能的支持,但不得排除日后对这些功能的支持。
这需要客户端支持来协商要下载的版本。
安全性要求
- 确保无法使用来自可独立更新的 repo 的软件包替换基本集中的软件包 - 请参阅可更新的软件包组的必备属性
- 为了符合 Fuchsia 验证执行(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-client 与 pkg-resolver 基础架构集成,以抽象化开发者是在使用真实的 Omaha 服务器还是本地机器上的开发服务器。
我们将添加对可更新软件包进行垃圾回收的支持,并采取安全措施来确保软件包更新不会阻止系统 OTA。
我们将与 Omaha 服务器基础架构集成,以根据特定软件包网址协商合适的软件包版本。
我们将使用情形分为两个主要事件:更新检查和软件包下载以及软件包解析。前者将在计时器上运行或在开发者手动请求时运行,并触发软件包下载和缓存。后者将在组件框架解析软件包时运行。
下面将全面概述新的软件包解析架构。以下部分将对此进行详细讨论。

更新检查和软件包下载
何时检查更新
omaha-client 运行一个状态机,该状态机可针对每个“应用”(对于 Omaha,这意味着客户端正在检查更新的一组软件)进行配置。
目前,正式版中系统更新的 omaha-client 检查间隔为 5 小时,并且会自动抖动到正负 1 小时。我们建议对系统映像和软件包更新使用相同的更新检查间隔,以减少 Omaha 服务器的负载,并在同时有系统更新和软件包更新可用时简化实现。(请注意,我们预计随着时间的推移,系统更新检查将与软件包更新检查分离,无论是在间隔还是托管方面。这是一种短期简化措施,旨在简化初始实现。)
在哪里检查更新(代码库配置)
我们将为每个可热切更新的软件包添加新的 Omaha 客户端配置,并按产品将这些配置编译到 SWD 堆栈的配置中。这包括系统上哪些软件包网址应引用相应软件包的配置的映射。该配置将包含软件包的名称和网址,以及其 Omaha 应用 ID 和其他默认配置选项(例如渠道)。
这会产生良好的安全副作用:由于主机名硬编码为不同的值,因此在 user\* build 中,急切更新的软件包将无法覆盖来自其他代码库的软件包。急切更新的软件包永远无法替换基础软件包(注意:回退版本不在基础软件包中),因为名称在 /system/static_packages 列表中的软件包永远不会通过网络进行解析。
需要更新 petal 中的代码以及 fuchsia.git 中的代码,使其不再引用软件包的 fuchsia.com 网址,而是引用与新代码库对应的主机名,例如 chrome-fuchsia-updates.googleusercontent.com。
我们需要向软件包更新配置添加额外的元数据,这些元数据将由软件包解析器解析。具体而言,我们将添加一个布尔值,用于指定软件包是否应包含可执行代码。此布尔值将用于运行时可执行性限制,类似于 pkgfs 中已包含的限制。如需了解详情,请参阅可执行性控制部分。
启动时,omaha-client 将获得来自 vbmeta 的可更新软件包的配置。目前无法进行运行时配置;这意味着,为了对 omaha-client 进行自动化测试,我们需要像在 Omaha E2E 测试中那样继续构建 vbmeta。
套餐协商
我们将软件包协商定义为将软件包网址(例如 fuchsia-pkg://chromium.org/chrome)转换为(主机名、Merkle)元组的过程。此转换的精确输出受多种变量影响,包括:
- 相应软件包所设置的更新渠道
- 设备上运行的系统版本
- 是否有正在进行的分阶段发布,以及更新检查器属于哪个分阶段发布组。
我们建议主要在服务器端进行软件包协商,以尽可能简化客户端代码。
omaha-client 将向软件包更新配置中列出的 Omaha 服务器发送请求,其中至少包含以下信息:
- 应用 ID(这是 Omaha 服务的软件包标识符)
- 软件包更新渠道
- 软件包更新版本(通常在应用 ID 中编码)
- Flavor 用于请求软件包的不同变体,例如包含调试符号或工具的变体。由于它将编码在应用 ID 中,因此正式版设备在现场无法更改其运行的 build 变种,除非通过系统 OTA
- 支持的系统 ABI(通常编码在平台版本字段中)
- 分阶段发布会员资格
- 当前软件包版本(格式为 A.B.C.D,其中 A-D 是 32 位整数的字符串表示形式,遵循有关版本号的 Omaha 规范),以便 Omaha 可以执行降级预防
- OMCL 将从软件包的最新提交版本的 CUP 元数据或回退版本的 vbmeta 版本元数据中检索此信息,如果磁盘上当前没有软件包的任何版本,则将此字段设置为
0.0.0.0
- OMCL 将从软件包的最新提交版本的 CUP 元数据或回退版本的 vbmeta 版本元数据中检索此信息,如果磁盘上当前没有软件包的任何版本,则将此字段设置为
- 支持的系统架构(例如 x64、arm64 等,与 Fuchsia 支持的架构列表中的名称一致)
Omaha 服务器将计算设备的正确软件包版本,并返回一个要下载的 blob 的 Merkle 固定网址,其中包含一个要下载的(软件包宿主、软件包名称、Merkle 根)三元组。在使用 Omaha 提供的数据之前,我们必须对照 Omaha 提供的 Merkle 检查响应的内容。响应的格式可能与当前用于系统 OTA 的 update 软件包的格式一致。
响应将包含相应应用的设备的同类群组,其中包括渠道信息。由于整个响应都将签名,因此我们可以使用此功能在启动时有效确定软件包的渠道。响应群组还用于更改后续请求的渠道(请参阅下文有关渠道支持的部分)。
Omaha 响应必须实现 Client Update Protocol (CUP),该协议可提供 Omaha 响应的签名。我们将向 Omaha 客户端添加对该协议的支持。需要 CUP,以便我们可以将响应持久保存到磁盘,并稍后根据存储在基本软件包中且以 vbmeta 为根的公钥重新验证该响应。
渠道支持
Omaha 协议通过其cohort概念支持渠道。
由于可更新的软件包可能使用与基本系统不同的渠道集,因此我们需要一个类似于 fuchsia.update.channelcontrol 的新 API,以便开发者管理开发中软件包的渠道设置。我们将以与在 vbmeta 中存储渠道信息以进行系统更新类似的方式,在 CUP 响应中存储渠道信息。我们打算通过 pkgctl 的扩展程序以 CLI 工具的形式公开此 API,并可能向希望在运行时更改其组件渠道的组件开发者公开此 API。
对于既有已提交的更新又有软件包的回退版本的设备,即使回退版本较新(即使已提交的版本比截止版本旧),我们也应使用已提交的更新中的渠道信息,以避免通过更新截止版本来覆盖渠道分配。我们仍必须先验证已提交响应中的签名,然后才能使用其渠道信息。
如果 Omaha 请求包含不存在的渠道,Omaha 服务器应重定向到存在的渠道,或者我们应使用回退版本。
在 Fuchsia 设备上使用渠道控制 API 请求更改渠道时,该设置不应在重启后保持不变,除非以包含相应渠道的持久签名 CUP 响应的形式保持不变。根据政策,我们不希望信任未签名的内容。下一次更新检查将使用内存中的渠道,并且在将正确的软件包版本提交到磁盘后,系统会存储对该更新检查的响应。
系统映像中的 Omaha 配置应包含急切更新的软件包的默认渠道。如果给定软件包没有持久性 CUP 响应,Omaha 应使用默认渠道。
如果渠道更改导致返回相同版本的软件包(因为该软件包存在于两个渠道中),则下载流程应正常进行。pkg-resolver 将不会下载任何新 blob,而是返回磁盘上已有的 blob。
分阶段发布支持
服务器会根据服务器上的随机掷骰结果(这是现有的 Omaha 功能)将设备分配给一个群组,然后使用该群组 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_packages 或 base_packages)管理,我们需要一个软件包解析流程,使软件包堆栈能够区分应通过 Omaha 管理的软件包与应通过其他元数据管理的软件包。
为此,我们建议采用以下问题解决流程。对于使用 fuchsia.pkg.PackageResolver 协议解析给定软件包网址的调用,pkg-resolver 将
- 应用基本网址固定和重写规则
- 确定要解析的重写网址是否由 Omaha + CUP 管理,如果由其管理...
- 向管理 CUP 软件包的内部库询问与相应网址关联的目录句柄。
- CUP 库将检索相应网址的最新已提交 CUP 响应中缓存的 Merkle 指定的软件包,或者在遇到错误且满足使用回退版本的条件时回退到回退版本。对于所选软件包的版本,它将直接进入
pkg-cache并返回目录句柄(以避免在写入新软件包时读取软件包的各种竞态条件)。
此流程的示意图如下:

可执行性控制
pkg-resolver 作为生产设备上有效分辨率路径的一部分,意味着它需要参与 FVX 强制执行。我们需要信任的密钥列表将包含 CUP 密钥。
我们将在 pkg-resolver 中实现可执行性控制,以确保在 user* build 上,唯一可以包含可执行代码的软件包是:
- 以相应进制表示
- 由 Omaha 管理,并在软件包解析器配置中配置为可执行
- 通过现有的
pkgfs_static_packages许可名单机制列入许可名单,以实现可执行性控制
这些控制的具体实现将是后续文档的主题。
验证在重新启动后下载的元数据
签名验证和可审计性
为了在重启后保留可更新的软件包,我们需要记住其 Merkle 根。我们将同时保留 Omaha 请求和 CUP 响应。当我们使用持久性数据查找软件包的 Merkle 时,会验证以下内容:
- 请求/响应对的签名与可信密钥匹配
- 该请求相当于设备有互联网连接时会发出的请求(在 Omaha 应用 ID、渠道和 build 变种方面匹配),并且
- 响应与签名参数匹配且是有效响应,并且
- 相应版本的预期版本大于或等于系统映像中的后备版本
- 响应包含支持正在运行的系统 ABI 版本的软件包
如果满足所有这些条件,pkg-resolver 中的 CUP 模块可以从 CUP 响应中返回持久性 merkle。
这种方法有一个缺点:我们需要解析持久性请求和响应,才能获取持久性 Merkle 根的值。不过,我们认为可以通过隔离实际执行解析的组件来降低实现中的这种风险。
回滚预防
此解决方案依赖于 vbmeta 提供的回滚保护,使用基于版本的后备。如果 CUP 响应中的预期版本号低于系统映像配置中的最低要求版本,我们将回退到 OTA 映像中包含的软件包版本(如果存在),并且不会提交响应中的版本。
为了演示如何使用此方案实现回滚预防,我们假设可更新软件包的版本 1.0 存在一个漏洞,该漏洞已在版本 1.1 中修复。我们需要指示 Omaha 停止提供 1.0 并开始提供 1.1,然后向整个设备群组发布系统 OTA,其中包含新的后备版本 1.1。这可确保在没有 Replay Protected Memory Blocks 或等效功能的设备上,提供不逊于当前单体行为的保证。
对于未设置后备版本的软件包,我们将添加基于时间戳的后备版本,并确认该版本比相应软件包的每个 CUP 响应都旧。此模式是支持更新系统事先不知道的软件包所必需的。回溯时间将设置为系统回溯时间,目前是生成系统映像的 Fuchsia build 的时间戳。我们目前没有关于更新系统映像之前未知的软件包的要求,但在设计时应考虑到这些软件包。
早期启动所需的软件包回退版本
可更新软件包中的某些软件包可能在启动过程中或在设备连接到网络之前是必需的(例如,它们可能为开箱体验 (OOBE) 提供用于配置网络的界面)。这意味着,如果某些软件包已过时,我们就无法依赖网络来下载这些软件包,而必须在系统更新映像中包含这些软件包的版本。我们将这些随附的软件包称为“后备版本”,因为它们通常仅在没有其他更新版本的软件包时才会使用。
后备版本面临着多种设计压力:
- 必须能够在没有网络访问权限的情况下运行
- 它们必须在恢复出厂设置 (FDR) 后保持不变,因为 OOBE 可能需要它们
- 必须随系统更新一起分发,并随系统更新一起进行资格认证
- 我们必须尽量减少回退版本使用的空间,尤其是在与其他版本的软件包结合使用时
- 如果我们必须存储软件包的后备版本、当前运行版本和尚未运行的新版本,则意味着我们需要同时存储特定软件包的 3 个副本(假设我们禁止同时进行系统更新和软件包更新)
- 我们必须努力避免以下情况:系统正在运行软件包的版本 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,所有方法都需要支持版本信息:
- 直接从组件记录日志
- 代表其他组件(例如 Sampler)记录日志,Sampler 会读取组件的相关检查数据并将其作为 Cobalt 指标进行传播
- 代表许多组件进行日志记录,例如记录统计信息。这可能看起来像是第 2 点的一般情况,但它表示无论我们选择哪种解决方案,都需要可伸缩性。
Fuchsia 使用 Cobalt 时,对可更新软件包的集成有以下几项要求:
- 组件所有者不希望使用按 Merkle 根细分的版本信息中心,他们更喜欢使用直观易懂的字符串
- 为了按直观易懂的版本字符串准确汇总,我们需要在设备上执行此操作
- 我们无法针对软件包的每个新版本更新 Cobalt 注册表,因为这会破坏独立软件包更新发布流程的意义
- 提供给 Cobalt 的组件版本相关数据可能指的是不再运行的组件版本,例如日志统计信息
- 对于短期解决方案,我们不需要支持 v1 组件(因为它们无法动态更新),但我们需要报告每个可以更新的组件的版本字符串。Cobalt 希望每个软件包都有版本字符串,但我们不会强制要求所有软件包都必须使用新格式,然后才能为任何软件包启用版本报告。
为了将软件包的版本字符串记录到 Cobalt,我们提出了一种系统,通过该系统,软件包的 Merkle 根会随指标一起传播,以便跟踪组件的哪个版本发出了给定的指标。
我们将修改 Cobalt 日志记录协议,以允许使用可选的软件包标识符字段。一旦 Cobalt 组件本身具有与特定指标集关联的 Merkle 根,它就可以通过该 Merkle 根映射到基于字符串的版本字符串,从而在本地汇总指标。它还可以将此 Merkle 根与来自其他来源(例如日志记录统计信息或 Sampler 数据)的组件名称相结合。
我们必须按设备上的版本字符串进行汇总,而不是将其发送到设备外部,以符合 Cobalt 的隐私权要求。鉴于我们可能会在设备上提供人类可读的版本信息,因此在设备上(而非在后端系统上)进行映射是明智之举。
客户还希望使用人类可读的任意字符串来划分指标。目前,各个软件包可以记录其版本,但没有系统范围的系统或标准来支持此功能。Android 将此支持称为版本名称。
为此,我们建议在 CUP 元数据中添加另一个字段,该字段作为 Omaha 响应的扩展进行传输,也称为 version-name。此字段将仅用于供人工处理版本字符串,SWD 堆栈不会保证其唯一性,也不会将其用于防止降级;软件包所有者需要自行维护其版本字符串。
验证版本字符串:新的 SWD API
出于隐私保护方面的原因,Cobalt 会限制可记录的字符串类型。特别是,Cobalt 无法信任任意字符串(例如版本字符串)不包含个人身份信息 (PII)。为了允许我们记录来自组件的版本字符串(Cobalt 无法提前知道这些字符串,因为组件可能会更新到任意版本),Cobalt 需要一种方法来验证来自受信任的系统组件的特定版本字符串是否为该受信任的系统组件所知。
软件交付团队将与 Cobalt 团队合作,验证软件包标识符(Merkle 根或软件包网址),并将其与人类可读的版本字符串相关联,以用于遥测。当 Cobalt 尝试根据软件包标识符查找给定软件包的版本时,如果存在版本名称和版本号,SWD 堆栈将返回这些信息。
目前,我们不会保证与系统映像关联的任意组件的版本信息,但我们日后可能会标准化软件包中人类可读的版本信息的编码,并在此处扩大保证范围。
如果日志记录组件未向 Cobalt 提供软件包标识符(例如日志统计信息),Cobalt 应根据当前正在运行的组件的软件包标识符报告其版本。
软件交付堆栈将期望 Cobalt 缓存对人类可读版本字符串的请求的响应。Cobalt 组件必须避免在每次调用指标时都调用版本字符串 API,否则会产生不必要的资源费用。我们可以选择为 Cobalt 创建一个批量 API,以便定期调用该 API 来减少版本字符串的 FIDL 流量。
我们日后可能会决定将版本字符串和版本名称集成到软件包格式中,但目前不打算这样做。
有关更新系统本身的指标
我们还需要报告有关软件包下载过程本身的指标,例如成功/失败指标。如上所述,出于隐私保护方面的考虑,Cobalt 在记录任意字符串方面存在一些限制。
不过,由于 Omaha 还具有自己的后端指标,这些指标有自己的隐私保护策略,因此 Omaha 本身可以提供有关运行特定版本的设备的精确数量的指标。与 TUF 替代方案相比,这是此解决方案的一项优势。
对于与更新过程本身相关的指标,我们计划利用 Omaha 的指标,并与 Cobalt 团队合作,在非常有限的情况下(由受信任的系统组件验证)报告基于任意字符串的维度。
最后,我们会将可更新软件包的版本从更新基础架构记录到设备快照,以帮助进行问题排查。
实现
我们可能会将此工作分为多个阶段,每个阶段对应于功能方面的改进。
第一阶段可能包含基于 Omaha 的基本软件包提取协议,该协议需要:
- SWD
omaha-client中的多应用支持- 实现 CUP,集成到软件包解析流程中
- 用于测试的最小开源 Omaha 服务器
- 软件包配置,将软件包网址映射到 Omaha 应用 ID。
- 服务器端打包团队
- 服务器端封装基础架构中的版本管理,以及基本软件包协商(例如渠道和降级保护)
- 用于存储软件包并与 blob 存储区集成。
- 服务器端软件包协商(渠道支持、分阶段发布等)
第二阶段将引入生产使用所需的其他安全机制,以及空间管理所需的安全措施:
SWD
- 实现 CUP 后备
- 空间管理:实现开放式软件包跟踪并全面改进垃圾回收,以实现更灵活的软件包管理。
- pkg-cache 之外的可执行性限制
- 应用专用资格审查流程
- 内置于基础映像中的软件包的后备软件包
- 添加了用于验证版本字符串的 Cobalt API
服务器端打包基础架构
- 启用 CUP
- 在签名之前检查发布时间来源和代码签名
- 增强了软件包协商支持,包括分阶段推出、ABI 修订协商等功能。
- 发布时大小检查
- 向软件包上传添加了接口,以允许指定版本字符串
注意:本部分未涵盖 RFC-0002 的实现,该 RFC 是一个依赖项,但有自己的实现方案。
性能
我们预计此项更改不会对运行时性能产生重大影响。
在推出此变更时,我们需要密切监控 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 客户端中实现某种程度的可执行性限制,可能类似于“可执行性控制”中详细介绍的每个代码库配置可执行性位。
所有这些安全任务都是可行的,但需要详细的设计并与安全团队协调。
隐私注意事项
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 进行软件包协商
我们可以使用 The Update Framework(更新框架)来分发软件包版本元数据,并将特定的 fuchsia-pkg:// 网址转换为要下载的 blob 的主机名和 Merkle。这样一来,我们就无需使用基于 Omaha 的协议。
此功能的初步设计如下:
- 向
pkg-resolver添加“急切更新”功能,该功能可复制当前omaha-client状态机的各个方面,并定期检查软件包组的更新。 - 通过全面改进 TUF 元数据格式并在 Fuchsia 更新服务器上实现该新格式,实现渠道、分阶段发布、跳板等功能。这需要大量工作。
- 当软件包解析器收到特定软件包网址的请求时,让它下载相应代码库中所有软件包的新元数据(如果没有网络或更新服务器的路由,则使用持久性元数据),并根据请求的上下文(包括渠道、分阶段发布会员资格等)计算要下载的正确 Merkle 根,所有这些都必须在设备上本地持久保存。这意味着服务器将无法根据上下文更改其对客户端的响应,所有计算都将在本地完成。
- 存储每个热切更新的软件包的下载元数据,这可能非常重要,具体取决于 TUF 代码库中存在多少渠道和版本(根据我们的估计,如果采用简单方法,每个软件包约为 3 MiB;如果我们进行更多工作以启用 TUF 元数据分片,每个软件包约为 82 KiB)
- 确定一种基于 Cobalt 的指标方法,该方法既能让客户接受(可提供足够精确的运行特定版本的设备数量等信息),又不会违反 Cobalt 的设计原则
- 通过
ffx添加了工程工作流,以支持热切更新代码库,并更新了开发者工具,以支持新的 TUF 元数据格式。 - 实现降级防范机制(Omaha 在服务器端提供此机制)。
这种替代方案有以下几个优点:
- 由于 TUF 元数据持久性,客户端可以“检查自己的工作”,这意味着证明软件包正确版本所需的所有数据都实际位于设备上。
- 我们已将 TUF 用于工程流程,因此已经有很多相关工具。
- 与 TUF 代码库和软件包名称对应的软件包网址这一概念非常有用,并且不需要映射到任何其他特定于服务的 ID(例如 Omaha 应用 ID)。
不过,这种替代方案也有缺点:
- 我们已在生产环境中使用 Omaha 进行系统更新,因此这将引入第二个更新服务器,该服务器是运行生产 Fuchsia 设备所必需的。
- 我们必须重新实现 Omaha 已经提供的许多功能,包括分阶段发布、渠道支持和跳板。
- 如果我们系统映像中的软件包协商代码有误,就必须通过 OTA 更新设备来修复,而不是在服务器端修复协商逻辑
我们认为,这种方法的缺点大于优点,并且这两种方法之间的差异可以大部分被抽象掉,因此我们采用了上述基于 Omaha 的方法。
在先技术和参考资料
可更新的软件包有很多种实现方式。以下是一些重要因素:
- Debian 打包模型
- Android 应用模型,包括 APEX 和 App Bundle。
用于更新元数据协议的参考资料如下:
附录:发布时的规模计算
对于大小预算严格的设备,我们不希望因使用过多空间而干扰 OTA,因此我们需要为软件包提供可选的大小预算,并在软件包的发布时进行检查。
Fuchsia 目前在将资源写入磁盘时对其进行流式压缩,并且压缩级别是在系统组装时确定的。为了预先计算给定软件包将占用多少空间,我们需要知道给定系统版本的压缩级别,为此,我们建议将 blobfs 压缩级别和压缩方案纳入系统 ABI。这样,我们就可以在服务器上使用相同的压缩参数来检查软件包是否在预算范围内,并将软件包传递到软件包代码库或使其发布失败。
在发布过程中,软件包服务器基础架构会检查应用的压缩大小(使用 SDK 中目标 ABI 的压缩参数和工具压缩的一组 blob 的累积大小)是否符合为目标设备上的软件包配置的预算。
在发布过程中,无论我们的大小检查做得多么好,最终都有可能会破坏大小检查流程,导致设备在尝试下载更新时空间不足。如需详细了解如何管理此问题,请参阅我们的空间管理和垃圾收集部分。
作为一种替代方案,除了通过模拟设备上完成的压缩来检查大小限制之外,我们还可以实现离线压缩,以便准确了解软件包中一组给定文件将占用多少空间。不过,这项工作尚未列为优先事项,可能需要对 SWD 和存储代码库进行重大更改。