RFC-0143:用户空间 Top-Byte-Ignore

RFC-0143:用户空间 Top-Byte-Ignore
状态已接受
区域
  • 内核
说明

更改了内核 ABI 以支持标记的用户空间指针。

Gerrit 更改
作者
审核人
提交日期(年-月-日)2021-11-30
审核日期(年-月-日)2021-11-30

摘要

本文档提出了对内核 ABI 的更改,以支持标记的用户空间指针。

设计初衷

Top-Byte-Ignore (TBI) 是所有 ARMv8.0 CPU 上的一项功能,会导致在加载和存储时忽略虚拟地址的顶部字节。而是在地址转换之前将第 55 位扩展到第 56-63 位。借助此功能,您可以将(被忽略的)顶部字节用作标记或其他带内元数据。TBI 的直接用途之一是在用户空间中启用硬件辅助的 AddressSanitizer (HWASan),其中标记存储在顶部字节中以进行内存跟踪。

本文档介绍了内核应如何处理标记的用户指针。

虽然 TBI 和 HWASan 是标记指针最相关的用例,但此设计并非仅涵盖这两种用例。还有其他平台具有自己的硬件功能,这些功能类似于支持标记指针,并且其他用户空间程序可以将这些标记位用于自己的特定用例。此设计应足够通用,能够支持其他标记指针实现,而不会对某个用户应用进行任何特定关注。

术语

以下是本提案中将经常用到的术语。有地址,也有指针。这两个概念类似,但处理方式截然不同。从语义上讲,某些系统调用对地址进行操作,而另一些系统调用对指针进行操作。

地址

地址是 64 位整数,表示用户地址空间边界内的某个位置。系统绝不会标记地址。用于操控地址空间的系统调用会对地址进行操作。地址的值始终受限于地址空间的范围(以 ZX_INFO_VMAR 表示)。

指针

指针是一种特定于编程语言的概念,通常表示可解引用内存的位置。每种语言都定义了指针的实现及其转换为硬件的方式。对于 C/C++,在 HWASan 上下文中,指针是一个由标记位和地址位组成的 64 位整数。指针可以带标记(表示非零标记位)或不带标记(表示标记位为零)。访问用户内存的系统调用通常会对指针进行操作。

标记

标记是指指针的上位,通常用于元数据。在启用了 TBI 的 ARM 上,标记的宽度为 8 位,由 56-63 位组成。

TBI(Top-Bits-Ignore)

其他潜在的硬件功能(例如 ARM MTEIntel LAM)也支持“忽略”指针上位某部分的方法。除非另有说明,否则本文档中使用的“TBI”一词代表支持忽略标记的任何通用硬件功能,而不仅仅是 ARM TBI。

设计

内核代码应复制硬件的行为。应处理标记,使内核行为对用户有意义。

以下是启用 TBI 后系统应如何运行的一些示例:

  1. zx_channel_write 可以接受标记的指针,并且调用行为与指针未标记时相同。

  2. 当进程在标记的用户指针上发生页面故障时,系统会像在同一未标记指针上发生故障一样解决页面故障,但有一个例外情况。如果故障生成 Zircon 异常,异常报告的故障指针将包含原始标记指针(在硬件保留该指针的情况下)。

  3. 为了解决 futex 唤醒/等待问题,系统会忽略所提供指针上的任何标记。换句话说,唤醒仅在标记方面存在差异的指针仍会唤醒等待该指针的所有等待器,无论它们可能指定了哪些标记。

  4. 从进程读取内存时(例如使用 zx_process_read_memory),内核会接受地址作为要读取的内存块位置的参数。与软件调试结合使用时,调试程序需要将被调试程序指针值显式转换为地址,以便通过内核 API 进行读取。

标记指针 ABI:对标记不敏感,但会保留标记

启用 TBI 后,将会出现以下情况:

  1. 内核会忽略从系统调用收到的用户指针上的标记。例如,如果 zx_channel_read 调用的缓冲区指针包含标记,则其行为与缓冲区指针未标记时完全相同。

  2. 在接受地址的系统调用(例如 zx_vaddr_t)。例如,传递给 zx_process_read_memory 的虚拟地址无法添加标记。在需要地址的地方使用标记指针将被视为任何其他无效地址。

  3. 当内核接受带标记的指针(无论是通过系统调用还是故障),它会尝试保留标记,以便用户代码稍后可以观察到它。 例如,如果用户程序在标记的用户指针上出错,那么生成的 Zircon 异常报告将包含该标记(前提是硬件可以保留该标记)。如果硬件无法保证能保留代码,系统会移除代码。如果用户空间没有任何机制可以观察标记,内核可以随意剥离该标记,前提是不会改变系统行为。如果硬件仅保证部分保留标记,则内核可能只会剥离无法保证保留的位。

  4. 内核本身绝不会生成标记指针。例如,映射 VMO(通过 zx_vmar_map)时,内核选择的最终值将是没有标记的纯地址。

  5. 比较用户空间指针时,内核会忽略可能存在的任何标记。例如,如果一个线程正在等待(通过 zx_futex_wait)具有标记 A 的指针,而另一个线程正在唤醒(通过 zx_futex_wake)具有相同地址位但标记为 B 的指针,则等待器将被唤醒。

为“全部”启用 ARM64 TBI

TBI 将由内核启动选项控制。启用后,所有用户空间进程都将启用 TBI。

调试软件

调试程序需要支持 TBI。ARM TBI 不允许在调试寄存器上设置标记。调试程序需要先显式对最有意义的 VA 位进行符号扩展,然后才能设置调试寄存器。

实现

启动选项将控制用户地址空间是否启用了 ARM TBI。 通过在转换控制寄存器 (EL1) 中设置 TBI0TBI1 位,可以启用 ARM TBI。

除了启用/停用 TBI 之外,我们还需要确保现有系统调用正确处理指针/地址。内核仅在少数几个位置处理用户指针(例如 user_ptr),因此实现此提案所需的更改相对较少。

我们可以通过新的系统功能向用户空间指明正在运行的 TBI 类型。我们可以引入新的功能类型 ZX_FEATURE_KIND_ADDRESS_TAGGING,此类型可以支持指示地址标记的新功能位,例如适用于 ARM TBI 的 ZX_ARM64_FEATURE_ADDRESS_TAGGING_TBI

性能

性能影响应该可以忽略不计,我们将使用现有的微基准进行验证。

测试

我们需要进行以下测试:

  1. 检查使用不同标记的指针的系统调用,这些标记会被有效忽略。

  2. 在标记的指针上唤醒与在未标记的指针上唤醒(对标记不敏感的行为)。

  3. 在标记的指针上出错会在异常中保留标记(标记保留行为)。

  4. 使内核 TBI 对用户空间可知的任何行为,例如系统功能的存在或返回被忽略的顶部位数的查询。

  5. 验证标记的指针会被预期地址的系统调用拒绝。

如果不支持 TBI,则需要跳过这些测试。

文档

标记指针 ABI 的所有文档均记录在此 RFC 中。实现此功能后,我们可能需要更新一些 Zircon 文档,以指定哪些系统调用的哪些参数无法接受标记。

需要记录可保证一定程度的标记保留的系统调用,以指定要保留哪些位,要剥离哪些位。

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

TBI 切换开关粒度

我们可以通过两种级别控制 TBI 的范围:全局级别和按进程级别。按进程的方法涉及某种机制,该机制允许在进程创建时间或启动时间切换 TBI。这需要新的系统调用、参数或位标志,这需要进行更多测试,并且可能会引入需要花费时间才能发现的新 bug 或安全问题。使用进程切换开关的开销可能很高。

全局开关的复杂性较低,有助于避免因必须实现运行时开关而产生的许多“未知未知”。此外,如果系统的所有应用都支持 TBI 或不支持 TBI,而不是混合使用这两种应用,安全性也可能会更高。

在用户模式下剥离标记

这需要在 syscall 层中的所有标记进入内核之前将其剥离。这样,无需进行任何内核更改,内核可以保持对标记的无知状态。这其中的一个问题涉及用户空间指针的故障处理。如果标记的指针上生成了故障,则每个用户空间处理程序都必须移除标记。

对其他寻址模式的支持

此提案应足够灵活,能够考虑涉及“忽略”顶部位数的其他硬件功能。我们不打算在近期内支持这些功能,但应该已经处于启用某项功能只需进行极少更改的状态。

ARM Memory Tagging Extension (MTE)

MTE 是一项在 TBI 之上运行的功能,用于查找错误的内存访问。内存标记的运作方式是将每个分配和指针与特定标记值相关联。如果指针的标记与其尝试访问的分配不同,则表示由于标记不匹配而导致内存访问错误。使用 MTE 时,此标记是一个 4 位值,存储在指针的顶部字节中。

在此 ABI 下,如果启用 MTE,标记将引用指针的顶部 8 位,但只有 56-59 位会在出现故障时保留,因为硬件仅保证保留这 4 位。

Intel 线性地址掩码 (LAM)

LAM 是 x86 即将推出的一项功能,在加载/存储时,指针中的前 7 位或 16 位会被屏蔽。这通过更改 CR3 寄存器在全球范围内进行控制。与 TBI 不同,LAM 不会保留页面错误的任何标记位。

在先技术和参考文档

AArch64 Linux 中的标记虚拟地址

此提案的大部分设计灵感来自 Linux 中的标记地址 ABI,即接受标记指针时,大多数内核行为应该不会受到影响。一个主要区别是,Linux 支持按线程切换 ABI,而此提案旨在构建/启动时全局切换 ABI。此外,ARM TBI 在 Linux 上始终处于启用状态,而 ARM TBI 也由同一 build 选项控制。