RFC-0145:Eager 软件包更新

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

对系统巨石之外的软件包进行提前更新。

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

摘要

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

设计初衷

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

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

利益相关方

教员hjfreyer@google.com

Reviewers:

  • 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.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 所定义,该 RFC 是本工作的依赖项),并且只能由支持所需 ABI 修订版本的 Fuchsia 平台版本下载。

指标

软件包更新系统必须支持与系统更新流程类似的指标,并向软件包所有者提供指标,例如成功更新次数、下载次数、更新大小(目前系统更新不存在此指标),等等。

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

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

也可以理解为“不要让会话重启等事件运行缓慢”。如果我们在会话重启或重新启动时检查软件包更新,并且没有使用提前更新检查器,会很容易使会话重启速度变慢。这是因为设备可能必须下载整个软件包或一组软件包才能完成会话重启。

重启系统不应使成功应用的更新的效果发生逆转

如果设备在重新启动时没有网络访问权限,我们不得在重新启动时回退到软件包的旧版本;如果网络不稳定,我们不得阻止应用启动,除非该应用过时。

我们可能会制定政策,规定“过时”的具体含义,并在应用更新之前拒绝发布

如果应用版本已严重过时,并且已知有更新可用,我们日后可能会进入“强制更新模式”,以防止启动已知存在漏洞或已知存在问题的软件版本。

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

许多开发者不想或无法在其依赖的软件包有可用更新时每次都更改代码或清单。模块化配置中就有一个这样的示例,它会为模块化组件编码特定的组件网址。如果开发者每次更改依赖项时都需要更改这些配置,这对开发者来说并不理想,而且在其他情况下,这也不可能。

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

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

长期而言,我们不接受需要包含兼容版本的单个全局清单的解决方案

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

将软件包下载分布在一天中的所有时间

将软件包下载限制为特定时间(例如在会话重启时刻附近)意味着,设备可能会在尝试更新期间长期处于离线状态。更新应在一天中的任何空闲时间下载。

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

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

软件包代码库

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

如果产品希望提供自己的发布基础架构,只要符合本 RFC 中指定的要求和设计要点,就可以这样做。

更新渠道

急速更新的软件包必须支持基于渠道发布,使用的渠道名称可能与用于分发其余产品的一组渠道不同。例如,Chrome 可能希望使用的通道数量比其所运行的产品使用的通道数量多或少。

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

分阶段发布

分阶段发布是渠道管理的延伸,此支持功能可确保用户能够根据百分比(例如 1%、10% 等)触发应用的分阶段发布,以遵循最佳管理实践。

还应提供紧急更新机制,以便将重要推送内容快速推送给 100% 的用户。请注意,平台提供了紧急更新机制,可在 5 小时内实现 100% 的发布。注意:这超出了本 RFC 的范围,但是一项长期要求。

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

堆叠的石雕

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

目前,我们没有明确要求产品短期内支持软件包的 Stepping Stone,但此设计将其视为我们可能会提出的长期要求。我们不会在实现此 RFC 时明确支持这些功能,但我们不能排除日后支持这些功能的可能性。

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

安全性要求

  • 使系统无法使用来自可独立更新的代码库的软件包替换基础组合中的软件包 - 请参阅可更新软件包组的必备属性
  • 为了符合 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-clientpkg-resolver 基础架构集成,以提取开发者在本地机器上使用的是真实的 Omaha 服务器还是开发服务器。

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

我们将与 Omaha 服务器基础架构集成,以便在给定特定软件包网址的情况下协商软件包的适当版本。

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

下面将全面介绍新的软件包解析架构。下面几部分将对此进行详细讨论。

整体架构

更新检查和软件包下载

何时检查更新

omaha-client 会运行一个状态机,该状态机可按“应用”(对于 Omaha,这意味着客户端正在检查更新的一组软件)进行配置。

生产环境中系统更新的当前 omaha-client 检查间隔时间为 5 小时,并会自动抖动到正负 1 小时。我们建议为系统映像和软件包更新使用相同的更新检查间隔时间,以减少 Omaha 服务器上的负载,并在同时有系统更新和软件包更新可用时简化实现。(请注意,我们预计会逐步将系统更新检查与软件包更新检查分离,无论是间隔时间还是托管方面。这只是一种短期简化方法,目的是简化初始实现。)

在哪里检查更新(代码库配置)

我们将为每个急切可更新软件包添加新的 Omaha 客户端配置,并按产品将其编译到 SWD 堆栈的配置中。其中将包含系统上哪些软件包网址应引用该软件包的配置的映射。该配置将包含软件包的名称和网址,以及其 Omaha 应用 ID 和渠道等其他默认配置选项。

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

需要更新 petals 和 fuchsia.git 中的代码,使其不再引用软件包的 fuchsia.com 网址,而改为引用与新代码库对应的主机名,例如 chrome-fuchsia-updates.googleusercontent.com

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

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

文件包协商

我们将软件包协商定义为将软件包网址(例如 fuchsia-pkg://chromium.org/chrome)转换为(主机名、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,其中包含新的后备版本 1.1。这保证在没有重放攻击防范内存块或等效功能的设备上,性能不会比我们当前的单体行为差。

对于未设置回退版本的软件包,我们将添加基于时间戳的回退版本,并确认该版本比该软件包的每个 CUP 响应都旧。此模式需要支持更新系统事先不知道的软件包。后备时间将设置为系统后备时间,目前为生成系统映像的 Fuchsia build 的时间戳。我们目前没有针对更新系统映像之前不认识的软件包的要求,但我们应在设计时考虑到这些软件包。

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

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

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

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

给定软件包所有者应与产品所有者协商,决定是否需要其软件包的回退版本。并非所有可更新软件包都需要回退版本。

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

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

何时使用后备版本

只有当回退版本比磁盘上最近提交的软件包版本更高级(版本号更大)时,我们的 CUP 模块才会返回回退版本的目录。

如果软件包的最新更新版本已损坏,手动恢复出厂设置将清除存储的 CUP 元数据,并允许我们通过满足条件 1 来重置为回退版本。未来,我们还可以使用 blob 损坏处理程序来确定更新后的软件包是否损坏,并使用回退版本(“未来工作”部分中提及了这一点)。

从技术层面讲,新系统映像中的回退版本可能会是从最近提交的软件包降级而来。不过,为了使用该降级回退,新系统映像必须不支持最近提交的软件包的必需 ABI。我们认为,发布一个同时支持较新系统 ABI 和较旧回退版本的系统是不可行的,但我们会考虑添加服务器端检查,以防止出现这种情况。

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

指标

可更新软件包的指标

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

指标可以通过多种方法传送到 Cobalt,所有这些方法都需要支持版本信息:

  1. 从组件直接进行日志记录
  2. 代表其他组件从某个组件进行日志记录,例如 Sampler,它会读取有关组件的检查数据并将其传播为 Cobalt 指标
  3. 代表多个组件(例如日志统计信息)从一个组件进行日志记录。这可能看起来像是 2. 的一般情况,但表明我们选择的任何解决方案都需要可伸缩性。

Fuchsia 使用 Cobalt 时,对于可更新软件包的集成,有以下几项要求:

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

为了将软件包的版本字符串记录到 Cobalt,我们提出了一个系统,该系统会将软件包的 Merkle 根与指标一起传播,以跟踪发出了给定指标的组件版本。

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

为了遵守 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 服务器实现的要求

后续工作

Update Notification API

在实现此初始方案后,我们还将提供一个通知 API,以便客户订阅有关软件包的更新(以便在需要时触发组件重启)。更新通知有以下两种用例:

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

我们提出了一种新的组件解析器 API,其具有以下特点:

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

我们将通过组件解析器将通知和触发逻辑连接到软件包解析器,后者将提供类似的 API。我们还将添加一项功能,供组件向 framework 请求通知组件自身有更新,以便组件触发自己的重启。

针对更新后的软件包的 Blob 损坏通知

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

这需要与现有的blob 损坏通知 API 集成,并针对损坏的可更新软件包触发重新下载或清除缓存的 CUP 元数据。

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

如果可更新软件包中的组件无法启动,我们应为产品所有者提供在软件包系统重新下载软件包时显示前台更新界面的选项。这样可以防止用户需要 FDR 设备才能运行软件包的回退版本。

更新了可更新组件的自检查

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

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

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

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

其基本设计如下:

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

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

  • 客户端可以“检查其工作”,因为 TUF 元数据持久性意味着证明软件包版本正确所需的所有数据都实际位于设备上。
  • 我们已经在工程流程中使用 TUF,因此已经有了许多相关工具。
  • 与 TUF 代码库和软件包名称对应的软件包网址非常有用,并且不需要与其他服务专用 ID(例如 Omaha 应用 ID)进行任何映射。

这种替代方案也有缺点:

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

我们认为这种方法的缺点大于优点,并且这两种方法之间的差异在很大程度上可以抽象化,因此我们采用了上述基于 Omaha 的方法。

在先技术和参考文档

可更新软件包有很多实现方式。其中一些重要因素包括:

更新元数据协议的引用如下:

  • Omaha 规范,许多 Google 产品和非 Google 产品(包括 Fuchsia)都使用了该规范。
  • 更新框架规范,Fuchsia 目前在某些地方使用该规范。

附录:发布时大小计算

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

Fuchsia 目前会在资源写入磁盘时对其进行流式压缩,并且该压缩级别是在系统组装时确定的。为了预计算给定软件包将占用多少空间,我们需要知道给定系统版本的压缩级别,我们建议通过将 blobfs 压缩级别和压缩方案纳入系统 ABI 来实现这一点。这样,我们就可以在服务器上使用相同的压缩参数来检查软件包是否符合预算,并将软件包传递到软件包仓库或失败发布。

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

无论我们在发布过程中的大小检查有多好,都有可能最终会破坏我们的大小检查流程,并且设备在尝试下载更新时会耗尽空间。如需详细了解如何管理此问题,请参阅空间管理和垃圾回收部分。

作为通过模仿设备上的压缩来检查大小限制的替代方案,我们可以实现离线压缩,以便准确了解软件包中的一组给定文件将占用多少空间。不过,这项工作尚未列入优先事项,并且可能需要对 SWD 和 Storage 代码库进行重大更改。