RFC-0167:早期用户空间引导中的软件包

RFC-0167:早期用户空间引导中的软件包
状态已接受
区域
  • 组件框架
说明

将软件包引入 BootFS 和启动分辨率。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2022-05-10
审核日期(年-月-日)2022-06-13

摘要

此 RFC 提议将软件包引入到 bootfs。这将使早期用户模式能够享受到后期用户模式的软件包隔离和命名空间优势,并消除第三方驱动程序开发方面的障碍。

设计初衷

早期启动可执行程序程序集和沙盒是在组件框架具有可靠的封装架构之前就已存在,到目前为止,我们尚未投资将现在可供后续用户空间使用的工具引入早期启动。因此,用户空间引导加载程序发现自己面临着打包引入的几个问题;例如进程沙盒、可验证的内容和可变的库版本。

这些早期启动问题表现为与 bootfs 映像的运行时和 buildtime 交互中存在意外的复杂性和低效性,通过在早期启动中采用软件包化,可以很大程度上消除这些问题。早期启动的软件包化不仅可以解决现有问题,还为改进系统健康状况提供了机会。在整个用户空间中标准化可执行文件和库的内容标识符,可让我们重复使用来自之前不相交的存储空间(如 bootfs 和 blobfs)的等效数据副本。

组件框架具有“软件包”概念,可作为 fuchsia-pkg 的抽象层;虽然组件框架和封装系统严格来说是分开的,但它们紧密交织在一起,因此组件化和封装世界的目的是相互协作的。

最近的工作(例如,组件管理器作为第一个 post-userboot 进程执行)正在推动“组件一路向下”的架构不断发展。 即使是文件系统和设备驱动程序等早期启动的可执行文件,也以 Fuchsia 组件的形式启动,或者目前正在向该模型迁移。现在,我们应将这种系统组件化与“从上到下”的软件包系统组装相匹配,并将促使打包的所有价值带到用户空间引导。

更具体地说,由于无法生成有效的 Fuchsia 映像(其中 bootfs 中编码的可执行文件在共享库依赖项中存在版本偏差),bootfs 中缺少软件包命名空间将成为树外驱动程序开发的阻碍。此外,今天的 bootfs 映像正在泄露 ABI。驱动程序可以在启动时根据是否存在其他驱动程序来改变其行为,例如选择使用较新的库,并且可以在不明确定义对其他驱动程序的依赖关系的情况下执行此操作。同样,由于驱动程序最终都位于 bootfs 中的同一文件夹内,因此驱动程序的名称本身就创建了一个 ABI。这些类型的意外 ABI 存在的时间越长,就越难弃用,而且之前也出现过这种行为(Windows 驱动程序和视频游戏 DRM 驱动程序会公开这种意外 ABI)。

利益相关方

主持人:hjfreyer@google.com

审核者:geb@google.com、mcgrathr@google.com、surajmalhotra@google.com、aaronwood@google.com、galbanum@google.com、wittrock@google.com、jfsulliv@google.com

已咨询

社会化:通过设计文档与利益相关者探讨了该主题,然后在 RFC 之前向 tq-eng 开放以进行一般性讨论。

设计

此变更涉及对 bootfs 和 /boot 目录、BootResolver、BootUrl 以及产品组装的变更。

bootfs 更改

图片更改

Fuchsia 启动文件系统 (bootfs) 软件包将由 bootfs 中的 meta.far 文件表示,该文件在 bootfs 目录条目中将命名为 blob/<merkle root of meta.far>。软件包清单中的每个 blob 都将在 bootfs 中获得一个新的目录条目,即 blob/<merkle root of the dependency>

  1. 我们已在运行时计算 Merkle 以验证内容,并且速度足够快,因此在 zbi 创建等也会压缩映像的流程中这样做无关紧要。
  2. 此提案要求的新映像构建程序已在实际映像构建步骤之前确定内容身份,以便填充 meta.far 文件。这意味着,在生成用于驱动映像构建步骤的清单的过程中,基于内容的重复数据删除操作大多已经完成。只需确保我们在此阶段已完成所有可能的去重操作,应该非常简单。

当我们讨论在 /blob 下向 bootfs 添加新条目时,我们仅指添加引用同一底层文件的新 bootfs 目录条目(概念上为硬链接)。

bootfs 中的一个名为“pkg_map”的新文件将维护从人类可读的软件包名称到对软件包进行编码的 meta.far 的 Merkle 根的映射。

bootfs 大小将因新添加的 meta.fars 和新的 pkg_map 文件而增加。 其他所有内容都只是针对已存在的 bootfs 文件的新目录条目。在 x64 架构上,bootfs 的压缩大小将保守地增加约 70KiB。

/boot 更改

/boot 中将引入一个名为 /blob 的新子目录。名称带有 /blob 前缀的 bootfs 映像中的所有文件都将放置在此处。当 bootfs 中所有组件的迁移都完成时,顶级目录将仅包含内核 vmo、shell 脚本以及 /boot 成为组件管理器“命名空间”所需的文件。

组件管理器命名空间最初将是 /boot 的子目录,其布局方式与所有依赖项的预期一致。最终,我们希望将组件管理器也整合到元 .far 编码中,该编码依赖于 SWD 堆栈进行解析。

BootResolver 更改

软件包命名空间化 bootfs 组件的核心工作与软件包解析器完成的工作高度重叠。必须将易懂的软件包名称映射到包含 content-id 的 meta.far,必须对 meta.far 进行解码,并且必须使用其内容文件来构建命名空间。因此,该设计旨在尽可能多地重用现有的软件包解析器逻辑。

重用软件包解析器逻辑的最简单入口点是 package-directory::serve。此入口点接受 BlobFS 和标识 meta.far 的 content-id,打开 meta.far,解析其 meta/contents 文件,并构建和提供文件中编码的命名空间。只要我们能够将 blob 的 bootfs 支持的目录作为 BlobFS 客户端提供给 API,就可以按原样重复使用此库。

    let (proxy, server) =
        fidl::endpoints::create_proxy().map_err(ResolverError::CreateEndpoints)?;
    let () = package_directory::serve(
        package_directory::ExecutionScope::new(),
        <some blobfs::Client-like view on top of the bootfs blobs>,
        <meta.far hash>,
        fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_EXECUTABLE,
        server,
    )
    .await
    .map_err(ResolverError::ServePackageDirectory)?;

blobfs::Client 只是封装了 fio::DirectoryProxy,这是从组件管理器公开 bootfs 的方式。我们可以简单地使用 Blobfs 客户端封装此 bootfs,并将其传递到 package_directory::Serve 调用中。需要进行一些小更改,以确保某些 blobfs::Client API 在位于 bootfs 上时能够正常失败(即需要可变性的 API;例如 open_blob_for_write、delete_blob),但在 package_directory::serve 执行期间不需要可变性。

BootUrl 变更

BootUrl 目前不使用其网址的主机或路径部分,因为没有要编码的代码库或软件包。这意味着,我们无需引入新的网址方案和解析器,而是可以在现有的 BootResolver 中使用现有的 fuchsia-boot 方案引入新的组件加载。网址中是否存在软件包路径将成为是否应使用“BootResolver 更改”中所述的新解析途径的指示。

示例:

fuchsia-boot:///#my_component.cm 启动解析器会将此网址解读为命名空间已在 /boot 目录中正确设置的未封装组件。

fuchsia-boot:///my_package#my_component.cm 启动解析器会将此网址解读为封装的组件,其中存在从“my_package”到 my_package 的 meta.far 的 Merkle 根的映射,并且应使用该映射来构建特定于“my_package”命名空间的命名空间。

产品/图片组装变更

Bootfs 映像构建是通过分两个阶段执行工作来完成的,这两个阶段分别由构建系统和映像组装系统完成。

首先,我们将引入一个名为 bootfs_packages 的新 gn 变量。 此列表将在 product.gni 中声明,并分配给名为 bootfs_package_labels 的调用方变量,该变量会传递给任何将 build/input:bootfs 分配给 bootfs_labels 变量的 assemble_system 调用。

当我们将 bootfs 组件从无软件包编码迁移到 bootfs 软件包时,我们会将其从 bootfs_labels 依赖项集中包含它的组依赖项中移除,并将其添加到 bootfs_packages 集中。

接下来,在生成映像程序集配置时,我们将使用现有的 list_package_manifests 模板从调用者的 bootfs_package_labels 变量中定义的软件包收集软件包清单。

接下来,映像组装会从 build 遍历中获取清单,并使用该清单调用 zbi 等工具,将 build 目录中的文件打包到映像中。软件包清单格式包含一个“blob”对象列表,在创建映像时,该列表将用于将 blob/<merkle_root> 命名的文件添加到 bootfs 映像中。

在遍历这些 blob 对象时,我们将检查哪个 blob 是软件包 meta.far 的标识符,并将从软件包名称到 meta.far 的 Merkle 根的映射添加到地图中。在启动文件系统映像创建结束时,该映射将以 JSON 格式写入上述 bootfs 更改部分中所述的“pkg_map”文件。

我们之所以选择在图片组装级别实现此转换,有以下三个原因:

  • ProductAssembly 只是合并了来自其各种软件包的软件包列表。

  • ImageAssemblyConfig 验证仍然很简单。

  • 商品组装中的“软件包”验证继续对“软件包”进行操作。

实现

功能实现

映像组装更改、bootfs 更改、BootResolver 更改和 BootUrl 更改可以同时进行。

为了确保在实现过程中不会看到将软件包引入到 bootfs_package_labels 中,我们将从映像组装更改开始。我们将实现此功能,直至 bootfs 软件包清单聚合,并放置一个 build-time 检查,以确保该集合为空。

BootUrl 的语义将在 3 个 CL 中发生变化。首先,在将第一个 bootfs 组件迁移到软件包命名空间之前,我们将更改 BootUrl,使其在包含软件包路径或代码库时被视为无效(以确保我们保留软件包路径的可用性,作为解析策略的指示器)。其次,随着第一个 bootfs 组件迁移到 fuchsia_package,我们将允许在 fuchsia-boot 网址中使用软件包路径。第三,随着最后一个 bootfs 组件迁移到 fuchsia_package,我们将禁止使用不包含软件包的 fuchsia-boot 网址。

Migration

在完全实现这些功能后,我们将逐步迁移 bootfs 组件。给定组件的迁移如下所示:

  1. 我们将找到组件的未打包依赖项(.cml 文件、二进制文件等)添加到 bootfs_labels 依赖项的位置。
  2. 我们将把该未打包的依赖项集合转换为 fuchsia_package GN 目标。
  3. 我们将从现有的 bootfs_labels 组中移除该软件包,并将其添加到 bootfs_package_labels 组中。
  4. 我们将更新 bootstrap.cml 文件中组件的网址,以包含软件包名称。

性能

系统大小

在 x64 架构上,bootfs 映像的压缩大小将保守地增加约 70KiB。这是因为,一旦所有组件都迁移到软件包,bootfs 大小就会因新添加的 meta.fars 和新的 pkg_map 文件而增加。其他一切都只是对打包前 bootfs 文件的目录条目进行重命名。

运行时影响

现在,整个 bootfs 在 component_manager 启动时会被急切地解析到目录中。迁移后,我们最终会将相当于在 /boot 中设置组件命名空间的解析工作推迟到该组件启动时进行。

除了之前仅解析 bootfs 标头之外,还有一些额外的工作;我们必须解析 meta.far。

ZBI 已签名,因此目前我们不验证 zbi 中 bootfs 文件的内容,只验证 zbi 本身。如果我们不对 bootfs 中的 blob 进行任何运行时验证,那么我们的安全性将与现在完全相同。

启动文件系统 (bootfs) 目前可能处于的一种潜在错误状态是,汇编可能会错误地将源文件放置在错误的 bootfs 链接下。bootfs 中存在 blob 意味着,如果我们愿意,可以通过对 bootfs blob 进行运行时验证来防范这种情况。我们最初并不打算这样做。

构建时影响

zbi 构建的 buildtime 性能保持不变;迁移后,我们不再为 bootfs 中包含的每个依赖项遍历目标条目清单,而是遍历软件包清单来编码 blob。在实践中,我们遍历的制品数量完全相同。

向后兼容性

此提案涉及的许多更改都包含在 bootfs 中。在 bootfs 中,无法以比整个 bootfs 更小的粒度更新执行单元。因此,不会出现以下情况:不完整的 bootfs 封装实现所提供的封装 bootfs 组件存在风险。

向后兼容性问题的一个潜在来源是,如果映像组装器以某种方式发现自己将旧版 bootfs 映像与新组件管理器包含在同一 zbi 中,就会出现问题。如果发生这种情况,一旦迁移完成且 bootfs 中不允许使用非打包组件,就可能会引入错误。不过,鉴于产品组装构建 bootfs 的方式,目前无法出现此状态。

同样,功能实现中描述的 BootUrl 语义的更改也不会与功能实现或迁移相背离,因为该更改也包含在 bootfs 中。

安全注意事项

通过减少进程可访问的制品,软件包命名空间/进程沙盒严格提高了运行时安全性。

bootfs 映像已签名且为只读,因此主要的安全问题是映像组装器是否受信任,这与映像的格式无关。如果决定让 bootfs 映像可增量更新,那么在 bootfs 中提供可执行文件的 Merkle 根身份信息,可能会进一步提升安全性。

隐私注意事项

无。

测试

组件解析器和产品组装中的测试实践已得到充分记录,并将扩展到涵盖新功能。

将在语义更改的 3 个阶段中的每个阶段添加 BootUrl 解析器测试,以表明正在强制执行网址的预期行为。

文档

需要更新用户空间引导的 API 文档以及有关 bootfs 映像组装的文档。

缺点、替代方案和未知因素

重复使用 bootfs_labels 与引入新标签。

目前,如果意外将软件包包含在 bootfs_labels 列表的依赖项列表中,则不会产生任何影响。进行此更改后,软件包的存在将具有语义意义。因此,我们需要从 bootfs_labels 命名空间中“清理掉”意外包含的软件包。

这里唯一的复杂之处在于 fuchsia_driver_packages。fuchsia_driver_package 是一种独特的 build 模板;产品组装与 fuchsia_driver_package 的互动方式因驱动程序是放置在 bootfs 中还是 blobfs 中而异。如果放置在 bootfs_labels 中,产品组装会遍历驱动程序软件包,将驱动程序视为一组依赖项;如果放置在 blobfs 中,驱动程序会通过 meta.far 正确打包。这样做是为了让单个驱动程序目标可以根据产品在 bootfs 和 blobfs 之间切换。过去之所以可行,是因为 bootfs 产品组件不会对软件包在其依赖关系图中的存在赋予任何意义。不过,借助 bootfs 软件包命名空间,我们将把 fuchsia_package(及其关联的 package_manifest)的存在视为将编码了软件包命名空间的 meta.far 放置到 bootfs 中的声明。

如果我们今天实现 bootfs 命名空间功能,大约 20 个驱动程序最终会同时作为未打包的依赖项放置在 bootfs 中,并由关联的 meta.far 和关联的 blob 编码到 bootfs 中;在更改驱动程序管理器的运行程序之前,驱动程序的 meta.far 编码将不会使用。我们希望清除 bootfs 中的所有 fuchsia_packages,然后逐步将目标迁移到适当的封装,而不是用未使用的封装来膨胀 bootfs。我们选择通过引入新的 GN 元数据屏障来清除 fuchsia_driver_packages 的 package_manifests,该屏障可防止 list_package_manifests 遍历到 Fuchsia 驱动程序。之所以这样做,而不是将每个使用 fuchsia_driver_package 的驱动程序拆分为 bootfs 目标和 blobfs 目标,是因为这样可以显著降低驱动程序产品组装逻辑的复杂性。此外,构建中的单个软件包定义可以实现这两种用途,因此避免了拆分每项内容然后移除所有“仅限旧版 bootfs”伪软件包的 churn。

遗憾的是,这会带来一些复杂性,例如引入真实 fuchsia_package 目标的影子,向用户隐藏其名称。这会与其他现有和正在进行的工作(例如用于验证设备上所有软件包清单是否按名称预期的黄金测试)或产品组装工作(要求生成软件包索引的标签仅直接依赖于软件包目标本身)产生不兼容性。

因此,一种干扰性较小的方法是,只需添加一个新标签,并在将早期启动中的组件迁移到软件包时,将它们从旧标签移到新标签中。最终,我们需要合并这些列表,如果驱动程序尚未迁移到合适的组件,我们将需要重新评估 fuchsia-driver-package 模板的复杂性。

在产品或图片组装操作中实现

  • 是否应在产品组装或映像组装级别完成对 bootfs 组件的软件包清单的解析?换句话说,我们是否应该推迟清单的解析,直到我们即将调用 zbi 工具来实际构建 bootfs 时才进行解析?
    • ProductAssembly(FFX 组装产品)
    • 优点:
      • ImageAssembly 始终专注于生成映像文件本身(zbi、blobfs 等)
      • ImageAssemblyConfig 包含一个更简单的所有 bootfs 文件列表。
    • 缺点:
      • 需要进行更复杂的验证,以确保用于生成旧版程序集输入软件包的 ImageAssemblyConfig 与 ProductAssembly 创建的 ImageAssemblyConfig 相匹配(或者需要舍弃/弱化该验证)。
      • 在产品组装结束时进行的验证需要知道在 bootfs 中查找组件的“软件包”的位置(例如针对结构化配置进行的值文件存在性验证)。
    • ImageAssembly(ffx assembly create-system)
    • 优点:
      • ProductAssembly 只是合并了来自其各种软件包的软件包列表
      • ImageAssemblyConfig 验证仍然很简单
      • 商品组装中的“软件包”验证继续对“软件包”进行操作。
    • 缺点:
      • ImageAssemblyConfig 不再包含 bootfs 文件的完整内容。
      • 在创建 zbi 之前,映像组装需要执行软件包到条目的映射。

在 bootfs 中对 meta.fars 进行编码,还是在 bootfs 映像构建时进行命名空间设置?

另一种替代方法是在 bootfs 中简单地对“命名空间”进行编码,方法是构建映像,使 /boot 下的每个组件都有自己的子目录,组件管理器将该子目录用作相应组件的命名空间根目录。

在 bootfs 中对 meta.fars 进行编码时,我们通过在启动解析器中引入对 SWD 软件包解析库的依赖项来增加早期启动的依赖项,该库用于解释 meta.far 格式,并且有人负责维护,以便在 meta.far 格式发生变化时继续执行此操作。

在映像构建期间执行命名空间设置时,我们通过教导产品如何解读 package-manifest/meta.far 格式,并编写代码将 meta.far 格式转换为 bootfs 映像中的软件包命名空间,从而增加了产品组装的复杂性。这意味着需要编写新代码,并且第二个团队需要“参与”解读 meta.far,并负责确保其产品与 meta.far 同步。SWD 团队曾表示担心新团队会依赖于 meta.far 格式,而通过收敛到单一软件包编码并重用 SWD 维护的用于与该格式交互的工具,可以完全避免这种情况。

这两种策略的资源影响几乎相当(KiB 级)。

最后,软件包不仅仅是它们编码的命名空间。运行时功能(例如平台版本控制)需要 meta.fars 中的元数据(例如软件包版本)。如果不使用 meta.fars,我们就需要在基于 bootfs 的命名空间中引入某种新的方式来编码这种“额外信息”,并让组件管理器等服务了解这种额外的软件包元数据编码方式。