| RFC-0143:用户空间最高字节忽略 | |
|---|---|
| 状态 | 已接受 |
| 区域 |
|
| 说明 | 更改了内核 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(高位忽略)
其他潜在的硬件功能(例如 ARM MTE 或 Intel LAM)也支持“忽略”指针的部分高位。除非另有说明,否则本文档中使用的“TBI”一词表示支持忽略标记的任何通用硬件功能,而不仅仅是 ARM TBI。
设计
内核代码应复制硬件的行为。应妥善处理标记,以便内核行为对用户有意义。
以下是一些示例,说明了在启用 TBI 时系统应如何运行:
zx_channel_write可以接受带标记的指针,并且调用行为与指针未带标记时相同。当进程在标记的用户指针上发生缺页中断时,缺页中断的解决方式与在同一未标记的指针上发生缺页中断时相同,但有一种例外情况。如果故障生成 Zircon 异常,则异常报告的故障指针将包含原始标记的指针,具体取决于硬件的保留程度。
为了解决 futex 唤醒/等待问题,系统会忽略所提供指针上的任何标记。换句话说,唤醒仅在标记上不同的指针仍会唤醒等待该指针的任何等待者,无论他们可能指定了什么标记。
从进程读取内存时(例如使用
zx_process_read_memory),内核会接受一个地址作为要读取的内存块的位置实参。除了软件调试之外,调试器还需要将被调试进程的指针值显式转换为地址,以便通过内核 API 进行读取。
已加标记的指针 ABI:对标记不敏感,但会保留标记
启用 TBI 后,将出现以下情况:
内核将忽略从系统调用接收到的用户指针上的标记。例如,包含标记的缓冲区指针的
zx_channel_read调用与未标记的缓冲区指针的zx_channel_read调用行为完全相同。在接受地址的系统调用(即
zx_vaddr_t)。例如,传递给zx_process_read_memory的虚拟地址无法添加标记。在需要地址的地方使用带标记的指针,将像处理任何其他无效地址一样处理。当内核通过系统调用或故障接受带标记的指针时,它会尽力保留标记,以便用户代码稍后可以观察到该标记。 例如,如果用户程序在标记的用户指针上出现故障,那么如果硬件可以保留标记,则生成的 Zircon 异常报告将包含该标记。如果硬件无法保证标记能够保留,则会剥离该标记。如果用户空间无法观察到该标记,则内核可以随意剥离该标记,前提是不会改变系统行为。如果硬件仅保证部分保留标记,则内核可能只会剥离无法保证保留的位。
内核本身永远不会生成标记指针。例如,在映射 VMO(通过
zx_vmar_map)时,内核选择的结果值将是没有标记的纯地址。比较用户空间指针时,内核会忽略可能存在的任何标记。例如,如果一个线程正在等待(通过
zx_futex_wait)标记为 A 的指针,而另一个线程正在唤醒(通过zx_futex_wake)具有相同地址位但标记为 B 的指针,则等待线程将被唤醒。
已为所有内容启用 ARM64 TBI
TBI 将由内核启动选项控制。启用后,所有用户空间进程都将启用 TBI。
调试软件
调试器需要能够识别 TBI。ARM TBI 不允许在调试寄存器上设置标记。调试器需要在设置调试寄存器之前,明确地对最重要的 VA 位进行符号扩展。
实现
启动选项将控制用户地址空间是否已启用 ARM TBI。
通过在转换控制寄存器 (EL1) 中设置 TBI0 和 TBI1 位,可以启用 ARM TBI。
除了启用/停用 TBI 之外,我们还需要确保现有系统调用能够正确处理指针/地址。内核处理用户指针(例如 user_ptr)的地方不多,因此实现此提案所需的更改相对较小。
我们可以通过新的系统功能向用户空间指示正在运行的 TBI 的类型。我们可以引入一种新的功能类型 ZX_FEATURE_KIND_ADDRESS_TAGGING,这种类型可以支持新的功能位来指示地址标记,例如 ARM TBI 的 ZX_ARM64_FEATURE_ADDRESS_TAGGING_TBI。
性能
性能影响应该可以忽略不计,并且将使用现有的微基准进行验证。
测试
我们需要测试以下方面:
检查使用具有不同标记的指针的系统调用,这些标记会被有效忽略。
在已标记的指针与未标记的指针上唤醒(标记不敏感行为)。
对带标记的指针进行故障处理会在异常中保留标记(保留标记的行为)。
任何将内核 TBI 向用户空间公开的行为,例如系统功能的存在或返回忽略的最高位数的查询。
验证了预期地址的系统调用会拒绝带标记的指针。
如果不支持 TBI,则需要跳过这些测试。
文档
有关标记指针 ABI 的所有文档均记录在此 RFC 中。实现此功能后,我们可能需要更新一些 Zircon 文档,以指定哪些系统调用的哪些实参不能接受标记。
保证一定程度的标记保留的系统调用需要记录下来,以指定哪些位被保留,哪些位可以被剥离。
缺点、替代方案和未知因素
TBI 切换粒度
我们可以在两个层级控制 TBI 的范围:全局和按进程。按进程方法将涉及某种机制,该机制允许在进程创建时或启动时切换 TBI。这需要新的系统调用、实参或位标志,而这些都需要更多测试,并且可能会引入新的 bug 或安全问题,需要花费时间才能发现。拥有进程切换功能可能成本高昂。
全局开关的复杂程度较低,有助于避免因必须实现运行时开关而产生的许多“未知未知”。如果系统中的所有应用要么都支持 TBI,要么都不支持 TBI,而不是两者混用,那么系统也可能会更安全。
在用户模式下剥离标记
这需要在系统调用层中剥离所有标记,然后才能将其传递到内核中。这样一来,就不需要进行任何内核更改,并且内核可以保持对标记的不可知性。此方法存在一个问题,即用户空间指针上的故障处理。如果标记的指针上生成了故障,则每个用户空间处理程序都必须剥离该标记。
支持其他寻址模式
此提案应足够灵活,以考虑涉及“忽略”高位的其他硬件功能。我们近期内不打算支持这些功能,但我们应该处于这样一种状态:开启其中一项功能只需进行极少的更改。
ARM Memory Tagging Extension (MTE)
MTE 是一项基于 TBI 的功能,用于查找错误的内存访问。内存标记的运作方式是将每个分配和指针与一个特定的标记值相关联。如果指针的标记与它尝试访问的分配不同,则表示存在因标记不匹配而导致的内存访问错误。借助 MTE,此标记是一个 4 位值,存储在指针的最高字节中。
在此 ABI 下,如果启用了 MTE,标记将引用指针的最高 8 位,但由于硬件仅保证保留这 4 位,因此仅会保留故障的第 56-59 位。
Intel 线性地址掩盖 (LAM)
LAM 是一项即将推出的 x86 功能,可在加载/存储时屏蔽指针中的前 7 位或 16 位。通过更改 CR3 寄存器,可以全局控制此功能。与 TBI 不同,LAM 不会在发生页面错误时保留任何标记位。
在先技术和参考资料
AArch64 Linux 中的标记虚拟地址
此提案的许多设计灵感都来自 Linux 的标记地址 ABI,即在接受标记指针时,大多数内核行为应保持不受影响。一个主要区别是,Linux 支持按线程切换 ABI,而此提案旨在在构建/启动时全局切换 ABI。此外,ARM TBI 在 Linux 上始终处于启用状态,而 ARM TBI 也受同一 build 选项控制。