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(顶部-位-忽略)
其他潜在的硬件功能(如 ARM MTE 或 Intel LAM)也支持“忽略”指针某些部分高位的方式。除非另有说明,否则本文档中使用的“TBI”一词表示支持忽略标记(而非仅限于 ARM TBI)的任何通用硬件功能。
设计
内核代码应复制硬件的行为。应以能让内核行为有意义的方式处理标记。
下面列举了一些示例来说明启用 TBI 后系统的行为方式:
zx_channel_write
可以接受已标记的指针,并且调用的行为与未标记指针时的行为相同。当进程在标记的用户指针上发生页面错误时,该页面错误会被解决,就像故障发生在相同的未标记指针上一样,但有一个异常。如果故障生成了 Zircon 异常,异常报告的故障指针将包含原始标记的指针,指向硬件保留的程度。
为了解决 futex 唤醒/等待解决方案,将忽略所提供指针上的所有标记。换言之,唤醒只有标记有所不同的指针时,仍会唤醒所有等待该指针的 waiter,无论它们可能指定了任何标记。
从进程中读取内存时(例如使用
zx_process_read_memory
),内核将接受“address”作为参数来指示要读取的内存块的位置。在结合软件调试的同时,调试程序需要将调试对象指针值显式转换为通过内核 API 读取的地址。
已标记的指针 ABI:标记不敏感,但可保留标记
TBI 启用后,以下各项将保持暂停状态:
内核会忽略从系统调用接收的用户指针上的标记。例如,使用包含标记的缓冲区指针的
zx_channel_read
调用的行为与未标记缓冲区指针时的行为完全相同。在接受地址(即
zx_vaddr_t
)。例如,无法标记传递给zx_process_read_memory
的虚拟地址。在需要地址的情况下使用标记的指针被视为与任何其他无效地址相同。当内核接受被标记的指针(无论是通过系统调用还是故障)时,它会尝试保留该标记,但达到用户代码以后可能会发现的程度。 例如,如果某个标记的用户指针发生用户程序故障,那么生成的 Zircon 异常报告中会包含该标记(如果硬件可以保留该标记)。如果硬件无法保证可以保留此标记,则系统会删除该标记。如果没有让用户空间可以观察该标记的机制,则内核可以自由将其删除,前提是它不会更改系统行为。如果硬件仅保证部分保留标记,则内核只能删除不保证会保留的位。
内核本身永远不会生成带标记的指针。例如,在映射 VMO(通过
zx_vmar_map
)时,内核选择的结果值将是一个不带标记的纯地址。在比较用户空间指针时,内核会忽略任何可能存在的标记。例如,如果某个线程正在(通过
zx_futex_wait
)某个带有标记 A 的指针等待,而另一个线程正在(通过zx_futex_wake
)一个具有相同地址位但同样位于标记 B 的指针上唤醒,那么该 Waiter 将被唤醒。
全面支持 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
。
性能
性能影响应该可以忽略不计,并将使用现有的 Microbenchmark 进行验证。
测试
我们需要测试以下内容:
检查使用具有不同标记的指针的系统调用,并且实际上忽略了这些标记。
唤醒已标记与未标记的指针(标记不敏感行为)。
已标记的指针发生故障会保留异常中的标记(标记保留行为)。
让用户空间知道内核 TBI 的任何行为,例如,存在系统功能,或查询返回顶部位数被忽略的查询。
验证已标记的指针会被需要地址的系统调用拒绝。
如果 TBI 不受支持,则需要跳过这些测试。
文档
此 RFC 中提供了关于已标记指针 ABI 的所有文档。实现此功能后,我们可能需要更新一些 Zircon 文档,以指定哪些系统调用无法接受标记的参数。
需要对保证一定程度保留标记的系统调用进行记录,以指定保留哪些位以及可以剥离的位。
缺点、替代方案和未知情况
TBI 切换粒度
我们可以在两个级别控制 TBI 的范围:全局和每个进程。按进程的方法会涉及某种机制,以允许在进程创建时或启动时切换 TBI。这将需要新的系统调用、参数或位标志,需要进行更多测试,并且可能会引入需要时间才能发现的新 bug 或安全问题。设置进程切换开关可能成本高昂。
全局开关不太复杂,有助于避免必须实现运行时切换的许多“未知未知问题”。如果系统的所有应用都能够感知 TBI 或无法感知 TBI,则更安全,而非同时具有这两种感知能力。
在用户模式下移除标记
这将涉及在系统调用层中的所有标记进入内核之前剥离它们。这样,就不需要内核更改,并且内核可以保持与标记无关。其中一个问题涉及用户空间指针上的故障处理。如果在标记的指针上生成故障,则每个用户空间处理程序会删除标记。
支持其他寻址模式
该方案应足够灵活,以便将涉及“忽略”顶位的其他硬件功能考虑在内。我们近期不打算支持这些功能,但处于开启状态只需进行极少的更改。
ARM 内存标记扩展 (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 也由相同的构建选项控制。