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

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

针对 BootFS 和启动分辨率引入了软件包。

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

总结

此 RFC 提议在 bootfs 中引入软件包。这样一来,延迟用户模式就可以享受到软件包隔离和命名空间的优势,从而在前期启动过程中受益,并消除用于第三方驱动程序开发的阻碍因素。

设计初衷

前期启动可执行组件和沙盒在组件框架具有可靠的打包架构之前就问世了,但到目前为止,我们还没有投入资源将现在可供后期用户空间使用的工具纳入前期启动。因此,用户空间引导发现自己面临着引入打包的几个问题,例如进程沙盒、可验证的内容和变量库版本等问题。

这些前期启动问题体现在与 bootfs 映像的运行时和构建时交互方面出乎意料的复杂性和低效,可以通过在前期启动中使用打包方法在很大程度上消除。前期启动的打包除了解决现有问题之外,还可以为改善系统运行状况提供机会。通过对整个用户空间中的可执行文件和库的内容标识符进行标准化,我们能够重复使用以前不相交的存储(例如 bootfs 和 blobfs)中的等效数据的副本。

组件框架有一个“软件包”概念,它是 fuchsia-pkg 的抽象概念;严格来说,组件框架和打包系统是独立的,但它们密切相关,以至于组件化和打包世界的目标都是协同合作的。

近期的工作(例如,作为第一个用户启动后进程执行的组件管理器)正在推动“组件一直向下”架构的数量日益增加。即使是文件系统和设备驱动程序等前期启动可执行文件,也会作为 Fuchsia 组件发布,或者正迁移到该型号。现在是时候将这种系统的组件化与“软件包全程式”系统组装相匹配,并将促使打包系统的所有价值引入用户空间引导。

具体而言,在 bootfs 中缺少软件包名称会成为树外驱动程序开发的阻碍,因为它无法生成有效的紫红色映像,而 bootfs 中编码的可执行文件在共享 lib 依赖项中具有版本偏差。此外,目前的 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. 我们已经在运行时计算了 Merkles 来验证内容,速度也足够快了,因此在 zbi 创建等过程中(也会压缩图片)执行该操作无关紧要。
  2. 此方案所需的新图片构建流程已在实际构建图片步骤之前确定内容标识,以便填充 meta.far 文件。这意味着,基于内容的重复信息删除主要发生在仅生成清单以驱动映像构建步骤的过程中。只需确保我们在此阶段已完成所有可能的去重操作,应该非常简单。

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

bootfs 中名为“pkg_map”的新文件将维护从直观易懂的软件包名称到对软件包编码的 meta.far 的 Merrkle 根目录的映射。

bootfs 大小会因新添加的 meta.fars 和新的 pkg_map 文件而增大。 所有其他操作都相当于已存在的 bootfs 文件的新目录条目。保守地说,在 x64 架构上压缩后,bootfs 会增加约 70KiB。

/boot 更改

系统会在 /boot 中引入一个新的子目录,名为 /blob。bootfs 映像中名称以 /blob 前缀的所有文件都将放在该文件中。在将 bootfs 中的所有组件迁移到软件包的操作完成后,顶层目录将仅包含内核 vmos、Shell 脚本以及 /boot 成为组件管理器的“命名空间”所需的文件。

组件管理器命名空间最初是 /boot 的子目录,按照其所有依赖项的预期,“布局”了该子目录。最终,我们的目标是将组件管理器都整合到一种依赖于 SWD 堆栈进行解析的 meta.far 编码中。

BootResolver 变更

软件包名称空间 bootfs 组件的核心工作与软件包解析器完成的工作大量重叠。人类可读的软件包名称必须映射到通过内容 ID 进行标识的 meta.far,必须解码 meta.far,并且必须使用其内容文件来构建命名空间。因此,该设计旨在尽可能多地重复使用现有的软件包解析器逻辑。

要重复使用软件包解析器的逻辑,最简单的入口点是 package-directory::serve。此入口点接受 BlobFS 和标识 meta.far 的内容 ID,打开 meta.far,解析其元/内容文件,然后构建和提供在文件中编码的命名空间。只要我们能够将由 bootfs 支持的 blob 目录作为 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 变量中定义的软件包中收集软件包清单。

接下来,映像汇编程序会从构建遍历中获取清单,并使用它来调用 zbi 等工具,以将 build 目录中的文件打包到映像中。软件包清单格式包含“blob”对象的列表,在创建映像时,这些对象将用于将 blob/<merkle_root> 已命名的文件添加到 bootfs 映像中。

在迭代这些 blob 对象时,我们将检查哪个 blob 是软件包 meta.far 的标识符,并将从软件包名称到 meta.far 的 Merkle 根目录的映射添加到映射。在 bootfs 映像创建结束时,该映射将以 json 格式写入“pkg_map”文件,如上面的 bootfs 更改部分所述。

我们选择在图片组装级别实现这种转换,原因如下:

  • ProductAssembly 只是合并了不同软件包中的软件包列表。

  • ImageAssemblyConfig 验证仍然很简单。

  • 在 Product Assembly 中,“packages”的验证继续对“packages”进行。

实现

功能实现

映像组件更改、bootfs 更改、BootResolver 更改和 BootUrl 更改可以同时完成。

为了保证不会在实现过程中向 bootfs_package_labels 引入软件包,我们将从 Image Assembly 更改开始。我们会在 bootfs 软件包清单聚合之前实现此功能,并在构建时检查该集是否为空。

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 会在 module_manager 启动时快速解析到目录中。迁移后,我们最终会将相当于在 /boot 中设置组件命名空间的解析工作推迟到该组件启动为止。

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

ZBI 已签名,因此目前我们不会验证 zbi 中 bootfs 文件的内容,而只会验证 zbi 本身。如果我们不对 bootfs 中的 blob 进行运行时验证,那么我们所处的安全位置将与目前完全一样。

如今,bootfs 可能出现的一种潜在错误状态是,组件可能会错误地将源文件放在错误的 bootfs 链接下。bootfs 中存在 blob 意味着,我们可以根据需要对 bootfs blob 执行运行时验证来避免发生这种情况。我们最初并不打算这样做。

构建时的影响

zbi 构造的构建时性能保持不变;迁移之后,我们会遍历软件包清单以编码 blob,而不是遍历 bootfs 中包含的每个依赖项的目标条目清单。在实际操作中,我们处理的工件数量完全相同。

向后兼容性

此方案中涉及的许多更改都是独立的,位于 bootfs 中。更新 bootfs 内的执行单元时,不会比一次性更新整个 bootfs 的粒度要小。因此,如果系统提供了已打包的 bootfs 组件,则不存在未完成的 bootfs 打包实现的风险。

向后兼容性问题的一个潜在根源是映像编译器以某种方式发现自己添加了旧版 bootfs 映像,且该映像带有同一 zbi 中的新组件管理器。如果发生这种情况,一旦迁移完成且不允许在 bootfs 中使用非打包组件,则可能会引入错误。不过,鉴于产品组件构造 bootfs 的方式,目前这种状态不可能出现。

同样,对功能实现中所述的 BootUrl 语义的更改也不会与功能实现或迁移有所不同,因为它也包含在 bootfs 中。

安全注意事项

软件包命名空间/进程沙盒可减少进程有权访问的工件,从而严格提高运行时安全性。

bootfs 映像已签名且处于只读状态,因此主要的安全问题是映像编译器是否可信,这与映像的格式无关。如果我们确定 bootfs 映像应该可以以增量方式更新,在 bootfs 中提供可执行文件的 Merkle-root 身份将有望进一步提升安全性。

隐私注意事项

无。

测试

组件解析器和产品组装中的测试实践详尽载明,并且将进行扩展以涵盖新功能。

在语义更改的 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)的存在解读为一项声明,以将 fuchsia_package 编码为 bootfs。

如果我们现在启用 bootfs 命名空间功能,那么大约 20 个驱动程序最终将作为未打包的依赖项放在 bootfs 中,并由关联的 meta.far 和关联 blob 在 bootfs 中编码;在针对 drive_manager 的运行程序发生更改之前,未使用驱动程序的 alt.far 编码。我们希望不要使用未使用的打包导致 bootfs 膨胀,而我们希望从 bootfs 中清除所有 fuchsia_package,然后逐步将目标迁移到正确的打包方式。我们选择引入一个新的 gn 元数据屏障,阻止 list_package_manifests 传递给紫红色驱动程序,从而清除 fuchsia_driver_packages 的 package_manifests。这样做,而不是将每个使用 fuchsia_driver_package 的驱动程序拆分成一个 bootfs 目标和一个 blobfs 目标,因为它可以显著降低驱动程序产品组装逻辑的复杂性。此外,build 中有一个软件包定义,这在最终结果中可以同时达到任一目的,这样可以避免因拆分每个映像并移除所有“仅限旧的 bootfs”伪软件包这一操作而造成的流失。

遗憾的是,这具有单独的复杂性,例如引入对真实 fuchsia_package 目标的阴影,对用户隐藏其名称。这会导致与其他现有和持续性工作不兼容,例如用于验证设备上存在的所有软件包清单是否按名称预期,或者产品组装工作要求生成软件包索引的标签仅直接依赖于软件包目标本身。

因此,侵扰度较低的方法是直接添加新标签,在将前期启动中的组件迁移到软件包时,将它们从旧标签移到新标签。最终,我们需要合并这些列表,如果驱动程序尚未迁移到适当的组件,则需要修复 fuchsia-driver-package 模板的复杂性。

在产品或组装操作中实现

  • 解析 bootfs 组件的软件包清单应在产品组装级别还是映像组装级别进行?换句话说,我们是否应该等到我们即将调用 zbi 工具实际构建 bootfs 时再解析清单?
    • ProductAssembly(ffx 组装产品)
    • 优点:
      • ImageAssembly 一直非常专注于生成图片文件本身(zbi、blobfs 等)
      • ImageAssemblyConfig 包含包含所有 bootfs 文件的简单列表。
    • 缺点:
      • 需要进行更复杂的验证,即用于生成旧版汇编输入软件包的 ImageAssemblyConfig 是否与 ProductAssembly 创建的 ImageAssemblyConfig(或者,需要舍弃/弱化该验证)匹配。
      • 在产品组装结束时进行的验证需要知道在哪里为 bootfs 中的组件找到“软件包”(例如,为结构化配置完成的值文件存在性验证)。
    • ImageAssembly(ffx 汇编创建系统)
    • 优点:
      • ProductAssembly 只是合并了不同软件包中的软件包列表。
      • ImageAssemblyConfig 验证仍然很简单
      • “软件包”的产品组件中的验证继续在“软件包”上进行。
    • 缺点:
      • ImageAssemblyConfig 不再包含 bootfs 文件的完整内容。
      • 在创建 zbi 之前,Image Assembly 需要改为执行软件包 -> 到条目的映射。

在 bootfs 中对 meta.fars 编码,或者在构建 bootfs 映像时设置命名空间吗?

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

在 bootfs 中编码 meta.fars 时,我们在启动解析器中引入了对 SWD 软件包解析库的依赖关系,从而增加了前期启动的依赖项。SWD 软件包解析库旨在解读元.far 格式,并且需要人员和维护,以便在元.far 格式发生变化时继续执行此操作。

在映像构建期间执行命名空间设置时,我们会教产品组合如何解读 package-manifest/meta.far 格式,并编写代码以将 meta.far 格式转换为 bootfs 映像内的软件包命名空间,从而增加产品组装的复杂性。这意味着要创建新代码,第二个团队将参与解释 meta.fars 并负责与其产品保持同步。SWD 团队曾表示需要让新团队采用 meta.far 格式的依赖项,这可以通过在单个软件包编码上收敛并重复使用由 SWD 维护的工具与此格式进行交互来完全避免。

这两种策略对资源的影响几乎等效(KiB 的顺序)。

最后,软件包不仅仅是它们编码的命名空间。平台版本控制等运行时功能需要 meta.far 中的元数据(如软件包版本)。如果未使用 meta.fars,我们需要引入一些新方法,以便在基于 bootfs 的命名空间中对“额外信息”进行编码,并指导组件管理器等服务对软件包元数据进行这种额外的编码。