RFC-0261:快速高效的用户空间内核模拟 | |
---|---|
状态 | 已接受 |
区域 |
|
说明 | 让 Fuchsia 程序充当不可信的访客代码的安全、高效的监督程序。 |
问题 | |
Gerrit 更改 | |
作者 | |
审核人 | |
提交日期(年-月-日) | 2022-06-07 |
审核日期(年-月-日) | 2024-10-01 |
摘要
本文档提出了一组功能,可让 Fuchsia 程序在不使用 Hypervisor 技术的情况下充当不可信的访客代码的安全、高效的监督程序。主要用例是实现更快速、更高效的 Starnix 运行程序。
这些功能有时统称为“受限模式”。在 Starnix 上下文中,这些功能是 Starnix 执行模型的基础。
本文档重点介绍 Zircon 内核以及新内核功能的引入。本文不详细介绍 Starnix 如何基于这些功能构建。
设计初衷
如需了解背景信息,请参阅 RFC-0082:在 Fuchsia 上运行未修改的 Linux 程序。
我们希望开发能够高效模拟其他操作系统界面的 Fuchsia 程序。具体而言,我们希望在 Fuchsia 上运行未经修改的 Linux 程序,并实现接近原生性能。
利益相关方
教练:davemoore@google.com
审核者:abarth@google.com、adanis@google.com、cpu@google.com、jamesr@google.com、lindkvist@google.com、mvanotti@google.com
咨询了以下人员:abarth@google.com、adanis@google.com、rashaeqbal@google.come、lindkvist@google.com、mcgrathr@google.com、mvanotti@google.com
共享:我们已通过电子邮件、聊天和多次内核演变工作组 (KEWG) 会议,与内核和 Starnix 团队共享此提案。
目标
以下目标指导了此系统的设计。
快速高效的模拟 - 与基于 Zircon 异常的机制相比,这种新机制必须提供更快速、更高效的模拟。
强大的安全边界 - 此机制必须充当强大的安全边界,并允许安全地执行不可信代码。
自然且简化的编程模型 - 此机制提供的编程模型不仅应适合模拟 Linux 内核接口,还应处理大部分繁重工作。自然地,大多数内核都会将内核代码和数据放置在一个共享地址空间中,因此如果这项新机制提供具有共享地址空间的编程模型,将会非常方便(有关详情,请参阅“共享状态”部分)。至于简单性,我们更倾向于基于更高级别的操作系统抽象构建操作系统,而不是基于更低级别的硬件抽象构建操作系统。
将复杂性划分到不同的层级 - 我们希望将对 Zircon 内核的更改划分到不同的层级,以便更好地管理复杂性并保持灵活性。具体而言,这意味着尽可能减少此机制跨内核子系统的程度。
范围
此提案的范围包括模型和一般方法(详见“设计”部分),以及系统调用接口(详见“实现”部分)。
设计
该系统由三个设计元素组成,这些元素应搭配使用。
此设计的主要元素是受限模式、直接访问和共享状态。它们共同提供多进程共享内存编程模型,以便在 Zircon 抽象之上实现用户空间内核。
采用这种架构时,每个来宾线程将由一个 Zircon 线程进行建模,每个来宾进程将由一个 Zircon 进程进行建模。每个 Zircon 进程都将拥有一个地址空间区域,该区域对所有进程都是通用的。对于来宾内核开发者而言,共享区域会使来宾内核看起来更像一个单个多线程程序。
元素 1:受限模式
这是核心元素。
目前,线程在运行时会采用用户模式或内核模式这两种模式之一。当线程发出系统调用时,它会通过模式切换从用户模式转换到内核模式。完成系统调用后,它会切换回用户模式。
注意:为简单起见,除非另有说明,否则本文档中将使用 ARMv8a 术语。
当然,系统调用并不是导致模式切换的唯一事件。内核可能需要处理的任何事件都会触发模式切换到内核模式。这包括页面故障、异常和中断。
我们引入了第三种可选模式,称为“受限模式”。此新模式是可选的,因为系统中的并非所有线程都会进入受限模式。与用户模式比内核模式更受限一样,受限模式比用户模式更受限。
虽然内核模式和用户模式会映射到硬件功能(例如 ARMv8-A 中的 EL1 和 EL0),但受限模式不会。它是一种纯软件结构。硬件对受限模式一无所知。从硬件的角度来看,这只是用户模式。
术语:为避免歧义,在讨论同时具有这三种模式的线程时,我们将非受限用户模式称为“正常模式”。
从概念上讲,受限模式位于正常模式之下。就像在内核模式下执行的代码充当用户模式程序的监督程序一样,在正常模式下执行的代码充当受限模式程序的访客内核或用户模式监督程序。
隔离
受限模式的一项关键功能是隔离。通过在正常模式和受限模式之间切换时更改可访问的地址空间范围和寄存器状态来实现隔离。
通常,进程的地址空间会分为两部分,其中一部分(内核地址空间)仅在线程处于内核模式时可访问,另一部分(用户地址空间)可随时访问。在受限模式下,用户地址空间会进一步划分,总共提供三个区域。
只有在线程处于内核模式时,才能访问最上层区域。中间区域只能通过内核模式或正常模式访问。并且您可以随时访问较低的区域(如需了解详情,请参阅“直接访问”部分)。此较低级别的区域称为受限区域,因为它是受限模式下线程可以访问的唯一区域。
进入受限模式后,Zircon 内核会使用常规模式提供的值加载通用寄存器。清除或加载通用寄存器以外的寄存器是常规模式代码的责任。例如,Zircon 内核不会清除或加载浮点或矢量寄存器。x86 的 fs.base
和 gs.base
、arm64 的 tpidr_el0
和 riscv64 的 tp
都被视为通用寄存器。如需了解详情,请参阅进入部分。
线程进入受限模式后,只能访问其寄存器状态和受限区域中的映射。
在受限模式下,无论系统是从 Zircon vDSO 发出调用还是从其他位置发出调用,Zircon 内核都会将所有系统调用路由到正常模式(下一部分将对此进行详细介绍)。这意味着,即使 vDSO 映射到受限模式,系统也会将系统调用重定向到正常模式,而不会由 Zircon 内核处理。
模式切换和拦截
本部分介绍了线程如何在正常模式和受限模式之间转换。
在转换时传达模式状态
为了提高性能,Zircon 内核和客户机内核使用共享内存(模式状态 VMO)在线程在正常模式和受限模式之间转换时传达寄存器状态和线程的其他详细信息。如需了解详情,请参阅准备。
进入受限模式
Zircon 内核使用内核线程结构上的标志跟踪线程是处于受限模式还是正常模式。通过了解线程是处于受限模式还是正常模式,内核可以正确地将系统调用、故障和异常“路由”到正确的位置(内核或正常模式)。
从正常模式进入受限模式是通过新的专用系统调用 (zx_thread_enter_restricted) 执行的。在调用之前,用户模式监督程序会先填充一个结构体,其中包含要在受限模式下使用的寄存器值。该结构体位于模式状态 VMO 中。
进入过程包括将线程的可访问地址空间从进程的完整地址空间更改为仅限进程的受限区域(用户地址空间的下半部分),从模式状态 VMO 加载寄存器状态,以及简单地恢复执行(请考虑分支到已恢复的 PC)。
为了提升性能,Zircon 内核不会自动保存线程的正常模式寄存器状态。由调用方保存其关心的状态。此外,除了通用(整数)寄存器之外,Zircon 内核不会尝试恢复任何受限模式寄存器状态。是否恢复更多数据取决于调用方。如需了解通用寄存器中包含的确切内容,请参阅进入部分中的 zx_restricted_state_t
。
退出受限模式
在讨论线程如何退出受限模式并返回正常模式之前,我们先简要介绍一下在受限模式执行期间发生三种线程事件时如何处理这些事件,以便建立一些背景信息:
中断和可解析故障 - 中断和可解析故障不会导致线程退出受限模式。硬件和软件中断以及线程处于受限模式时发生的可解析页面故障对线程(包括其用户模式监视器)来说本质上是不可见的。例如,假设 CPU 的平台计时器触发,并且 CPU 进入内核模式。或者设备中断触发。或者,CPU 会发生页面错误,并由 Zircon 内核进行解析。线程(正常模式代码或受限模式代码)不会观察到这些事件。受限模式下的线程可完全抢占,与在正常模式下运行时一样。
任务挂起 - 任务挂起操作不会导致线程退出受限模式。受限模式对暂停操作(例如 zx_task_suspend
)不提供任何“保护”。如果某个线程在收到暂停信号时恰好处于受限模式,则会在受限模式下暂停,并在恢复后继续在受限模式下执行,就好像什么都没发生一样。
调试程序异常 - 如果已附加调试程序,并且在受限模式下执行的线程生成异常(例如命中断点),则系统会照常生成 Zircon 调试异常,而不会强制线程退出受限模式。不过,如果未附加任何调试程序,异常将导致线程退出受限模式,以便由用户模式监督程序进行处理。另请参阅通过异常离开。
了解了这些背景信息后,我们来讨论一下线程如何退出受限模式并返回正常模式。有两种方法,通过系统调用离开和通过异常离开。对所有情况而言,Zircon 内核都会提供受限通用寄存器状态的副本,以及一个原因代码,用于指明线程返回的原因。根据退出受限模式的原因,Zircon 内核可能会提供其他信息。此状态将在线程返回正常模式之前写入模式状态 VMO。
通过系统调用退出
在受限模式下执行系统调用指令会强制线程返回正常模式,以便系统调用可由用户模式监视器处理。
执行系统调用指令时,CPU 会先陷入内核模式。内核中的常规系统调用路径将测试线程是否处于受限模式。如果是,它将采用备用路径,其中包括保存受限通用寄存器状态,并直接返回到正常模式,返回到原始系统调用中传入的地址。然后,正常模式必须恢复其已保存的寄存器状态,然后处理系统调用。从逻辑上讲,控制将从受限模式切换到正常模式,但实际上,它会在整个过程中反复切换到内核模式。示例如下:
- 受限模式发出系统调用
- syscall 来自受限模式,因此内核会返回到正常模式以便处理调用
- 正常模式处理调用,并(逻辑上)通过系统调用返回到受限模式
- 内核处理“enter restricted mode syscall”并跳回受限模式
如前所述,为了提高性能,Zircon 内核执行的寄存器状态保存和恢复操作会降到最低限度,以允许正常模式仅保存/恢复所需的内容。从/到受限模式的转换费用下限取决于架构。例如,在 x86 上,我们需要交换页表根 (CR3
),而在 arm64 上,我们可以使用 TCR_EL1.T0SZ
对地址空间的正常半部分执行更经济的“掩码/取消掩码”操作,从而保留更多对正常模式和受限模式都通用的 TLB 条目。
因异常而退出
如前所述,在受限模式下执行时,Zircon 内核将透明地处理可解析的页面故障。也就是说,虽然线程实际上可能会进入内核模式来解决故障,但不会返回正常模式。从进程的角度来看,就好像从未发生故障一样。
不过,如果没有调试程序,则处理架构异常(包括未处理的页面故障)是用户模式监督程序的责任,而不会由 Zircon 内核处理。例如,如果线程在受限模式下运行时访问没有有效映射的地址,则会生成 Zircon 异常,并且线程会返回到正常模式。从 Zircon 异常处理机制的角度来看,返回正常模式会“处理异常”,并且不会咨询其他异常处理程序(例如线程/进程/作业处理程序)。系统将生成一个标准的 zx_exception_report_t
对象并将其放置在模式状态 VMO 中,以便在返回时,用户模式监督程序可以获得异常详细信息。
通过踢出离开
为了支持实现优雅的任务终止和 POSIX 信号等功能,我们添加了一个新的 Zircon 系统调用 zx_thread_kick_restricted
,用于强制处于受限模式的线程返回正常模式。踢出是一种锁定操作,如果线程在正常模式下被踢出,则其下一次尝试进入受限模式将导致其立即返回状态代码 ZX_RESTRICTED_REASON_KICK
,表示其已被“踢出”。返回此状态代码会有效地解锁踢脚。
广告不会“叠加”。例如,假设目标线程处于正常模式,另一个线程发出了五次踢出。当目标线程进入受限模式时,它会立即返回,有效地处理所有五项 kick 操作。
通常,zx_thread_enter_restricted
的调用方应编写为处理虚假的 kick。
元素 2:直接访问
由于操作系统内核经常需要访问用户模式数据,因此能够高效地访问这些数据非常重要。操作系统内核通常可以直接访问其用户进程的代码和数据。例如,Linux 内核和 Zircon 内核都可以直接访问用户进程的地址空间。
直接访问是此设计的关键元素。
如前所述,在正常模式下,Zircon 线程可以访问其进程的整个地址空间;在受限模式下,它只能访问地址空间的一部分。因此,在正常模式下运行时,Zircon 线程可以充当客户机内核,并且可以访问位于受限区域内的客户机用户代码和数据。
用户地址空间实际上会分为两部分。通过将地址空间一分为二,我们可以在某些架构上利用一些显著的地址空间切换效率(即掩码一半,而不是实际切换,TCR_EL1.T0SZ
)。
由于在 Starnix 下运行的程序可能需要其地址空间“靠近”零地址(例如 MAP_32BIT
),因此我们选择将最低区域设为受限区域。
这种方法存在一些限制。主要原因是,由于受限区域是完整区域的连续子集,因此不得存在冲突的映射。用于访客代码和数据的地址必须与用于用户模式监督程序代码和数据的地址不重叠。由于受限区域位于用户地址空间的“下半部分”,因此用于实现用户模式监视器的 Fuchsia 运行时必须具有灵活性,并且不需要其任何映射位于下半部分。为此,我们不得不对 Fuchsia 进程构建器做出一些更改。sanitize 工具有时对映射的位置有额外的要求,因此可能需要在未来进行工作来支持某些 sanitize 工具。
元素 3:共享状态
内核通常具有在用户任务之间共享且由系统调用操控的数据结构。对于此类系统,内核编程模型类似于多线程程序,其中每个用户任务都是一个线程,具有对共享(内核)地址空间的访问权限,以及对某些特定于用户任务的私有区域的访问权限。共享区域中的代码和数据构成了共享系统映像。
为了能够轻松开发具有共享系统映像的用户空间内核,我们将扩展 Zircon 进程模型,以支持与其他进程共享部分地址空间的新进程。这些进程的集合将同时托管访客用户任务和访客内核。
我们之前介绍了如何掩盖进程的地址空间,以及完整地址空间与受限地址空间的区别。非受限区域将由托管同一系统映像的客人任务的所有 Zircon 进程共享。每个进程都有自己的独立受限区域,该区域仅供该进程使用。因此,当其中一个进程中的线程在正常模式下执行时,它将能够访问整个地址空间,包括共享映射以及受限区域中的私有映射。
除了共享这些非受限映射之外,托管来宾任务的 Zircon 进程都将共享单个句柄表和单个 futex 上下文。共享区域中的任何句柄值(例如 zx_handle_t
)都将可供任何进程使用,而无需执行显式句柄复制或传输操作。通过共享(部分)映射、句柄和 futex 上下文,一组进程看起来和感觉上更像是单个进程中的一组线程,从而简化了编程模型。
在共享区域或受限区域中访问映射时,必须小心谨慎,避免出现数据争用,就像在多线程进程中访问内存一样。
虽然共享进程组中的线程具有不同的进程标识,但它们都共享相同的句柄表、futex 上下文和地址空间的共享区域。这种方法的一个含义是,对于 Starnix,正常模式进程崩溃与内核 panic 类似,因为崩溃的进程可能会使一些共享数据处于不一致的状态。
当然,受限模式崩溃的情况非常不同,因为受限模式无法访问正常模式状态。由于在正常模式下执行的代码充当受限模式代码的监督程序,因此在大多数情况下(例如 Starnix),它应妥善处理受限模式“崩溃”,例如架构异常和未处理的页面故障。
实现
我们首先构建了 x86 支持,因为 Starnix 最初的目标平台就是 x86。后来,我们添加了对 arm64 和 riscv64 的支持。
虽然设计和实现侧重于 Starnix 用例,但这些功能的开发方式是完全独立于 Starnix 的,可以独立于 Starnix 进行使用(和测试)。
@next
vDSO 中新增了系统调用。
系统调用接口和语义
进程创建
为了进入受限模式,线程必须是“共享进程”的一部分,并且具有受限的地址空间区域。共享进程是指可选择与其组中的其他进程共享其地址空间(共享区域)、句柄表和 futex 上下文的进程。之所以说是可选的,是因为共享进程可以存在于一个组中。
进程是常规进程还是共享进程,是在创建进程时确定的。为了创建共享进程,我们扩展了现有的 zx_process_create
系统调用并引入了新的 zx_process_create_shared
。
现有的 zx_process_create
系统调用已更改为接受新选项 ZX_PROCESS_SHARED
。如果存在,由返回的 VMAR 建模的生成进程的地址空间将仅涵盖共享区域(上半部分)。使用 ZX_PROCESS_SHARED
创建的进程可用作创建一组共享部分地址空间的进程的原型。
添加了新的系统调用 zx_process_create_shared
,该调用将与 ZX_PROCESS_SHARED
选项搭配使用。
zx_process_create_shared(zx_handle_t shared_proc,
uint32_t options,
const char* name,
size_t name_size,
zx_handle_t* proc_handle,
zx_handle_t* restricted_vmar_handle);
此新调用与 zx_process_create
类似。是否能够通过 zx_process_create_shared
创建进程取决于调用方的作业政策和 shared_proc
参数。
zx_process_create_shared
会创建一个新进程 proc_handle
和一个新的 VMAR restricted_vmar_handle
。新进程的地址空间由两个区域(共享区域和私有区域)组成,分别由两个单独的 VMAR 提供支持。共享区域由之前的 ZX_PROCESS_SHARED
创建调用返回的现有 VMAR 提供支持。该专用区域由新创建的 VMAR 提供支持,并通过 restricted_vmar_handle
返回。
这样一来,Starnix 运行程序便可以创建一组 Starnix 进程,这些进程在其地址空间的一半中共享映射,但每个进程都有独立的下半部分来托管受限模式代码和数据。
进入/退出受限模式
只有具有受限区域的共享进程中的线程才能进入受限模式。
在进入之前,线程必须使用 zx_thread_prepare_restricted
创建并将模式状态 VMO 绑定到自身,以进行准备。线程和 Zircon 内核将使用模式状态 VMO 来保存/恢复受限模式通用寄存器状态。VMO 会由线程(以及任何显式创建的用户映射)保留,直到线程终止。
准备调用成功后,线程可能会使用 zx_thread_enter_restricted
进入受限模式。退出受限模式后,线程可以重新进入,而无需重新准备,无论它是出于 ZX_RESTRICTED_REASON_SYSCALL
、ZX_RESTRICTED_REASON_EXCEPTION
还是 ZX_RESTRICTED_REASON_KICK
(zx_thread_kick_restricted
) 而退出。
准备
zx_status_t zx_thread_prepare_restricted(uint32_t options, zx_handle_t* out_vmo);
prepare 会创建模式状态 VMO 并将其绑定到当前线程。生成的 VMO 将用于在进入/退出受限模式时保存/恢复受限模式状态。
返回的 VMO 与使用 zx_vmo_create
创建的 VMO 类似,并且生成的句柄具有与使用 zx_vmo_create
且不带选项创建 VMO 时相同的权限。请注意,不支持取消提交、调整大小或创建子 VMO。支持通过 zx_vmo_read
/zx_vmo_write
进行映射、取消映射以及读/写。
一次只能将一个模式状态 VMO 绑定到一个线程。尝试绑定另一个 VMO 会替换已绑定的 VMO。只有在关闭最后一个用户句柄、移除最后一个用户映射,以及通过再次调用 zx_thread_restricted_prepare
或线程终止来替换它之后,绑定的 VMO 才会被销毁。与任何其他 VMO 一样,VMO 映射后,系统会保留其映射,以便调用方可以关闭句柄并直接通过映射访问内存。
调用方的作业政策必须允许 ZX_POL_NEW_VMO
。
此调用可能会失败,并返回以下错误:
ZX_ERR_INVALID_ARGS
-out_vmo
是无效的指针或 NULL,或者options
是 0 以外的任何值。ZX_ERR_NO_MEMORY
- 因内存不足而失败。
注意:如果由于
out_vmo
是无效指针而无法返回新创建的 VMO 的句柄,即使调用返回ZX_ERR_INVALID_ARGS
,VMO 可能仍会绑定到线程。系统会生成政策例外情况 (ZX_EXCP_POLICY_CODE_HANDLE_LEAK
)。假设异常已处理,调用方可以通过使用有效的out_vmo
再次调用 prepare 来从此状态恢复。
Enter
status_t zx_thread_enter_restricted(uint32_t options,
uintptr_t return_vector,
uintptr_t context);
通过将调用线程的可访问地址空间更改为其共享进程的受限区域,并加载模式切换 VMO 中包含的寄存器状态,进入受限模式。在调用之前,线程必须使用 zx_thread_prepare_restricted
准备好模式状态 VMO,并在偏移量为零的位置用 zx_restricted_state_t
结构体填充它。
与常规函数调用不同,成功的 zx_thread_enter_restricted
调用不会返回到调用上下文。相反,在退出受限模式后,线程只会使用 context
和两个通用寄存器中加载的原因代码跳转到 return_vector
中指定的地址。所有其他寄存器(包括栈指针)都将处于未指定状态。return_vector
中的正常模式代码负责使用 context
、原因代码和模式状态 VMO 的内容重建调用系统调用之前的状态。
可能的原因代码包括 ZX_RESTRICTED_REASON_SYSCALL
、ZX_RESTRICTED_REASON_EXCEPTION
和 ZX_RESTRICTED_REASON_KICK
。返回时,模式状态 VMO 的偏移量 0 将包含以下某个结构体:zx_restricted_syscall_t
、zx_restricted_exception_t
或 zx_restricted_kick_t
,具体取决于原因代码。
在 x64 上,context
放置在 rdi
中,原因代码放置在 rsi
中。
在 arm64 上,context
放置在 x0
中,原因代码放置在 x1
中。
在 riscv64 上,context
放置在 a0
中,原因代码放置在 a1
中。
失败的调用可能会返回:
ZX_ERR_INVALID_ARGS
-return_vector
不是有效的用户地址或选项无效。ZX_ERR_BAD_STATE
- 模式状态 VMO 中的受限模式寄存器状态无效,或者没有模式状态 VMO 绑定到调用线程。
结构体定义如下:
typedef struct zx_restricted_syscall {
zx_restricted_state_t state;
} zx_restricted_syscall_t;
typedef struct zx_restricted_exception {
zx_restricted_state_t state;
zx_exception_report_t exception;
} zx_restricted_exception_t;
typedef struct zx_restricted_kick {
zx_restricted_state_t state;
} zx_restricted_kick_t;
请注意,所有三个结构体的第一个元素都是 zx_restricted_state_t
对象。返回时,此状态对象将包含线程返回正常模式时的受限模式通用寄存器状态。保存任何其他受限寄存器状态(例如调试寄存器或 FPU/向量寄存器)是 return_vector
的正常模式的责任。
受限模式通用寄存器状态 zx_restricted_state_t
定义如下:
在 arm64 上,
typedef struct zx_restricted_state {
uint64_t x[31];
uint64_t sp;
uint64_t pc;
uint64_t tpidr_el0;
// Contains only the user-controllable upper 4-bits (NZCV).
uint32_t cpsr;
uint8_t padding1[4];
} zx_restricted_state_t;
在 x64 上,
typedef struct zx_restricted_state {
uint64_t rdi, rsi, rbp, rbx, rdx, rcx, rax, rsp;
uint64_t r8, r9, r10, r11, r12, r13, r14, r15;
uint64_t ip, flags;
uint64_t fs_base, gs_base;
} zx_restricted_state_t;
在 riscv64 上,它只是 zx_riscv64_thread_state_general_regs_t
的 typedef。
typedef zx_riscv64_thread_state_general_regs_t zx_restricted_state_t;
其中 zx_riscv64_thread_state_general_regs_t
为
typedef struct zx_riscv64_thread_state_general_regs {
uint64_t pc;
uint64_t ra; // x1
uint64_t sp; // x2
uint64_t gp; // x3
uint64_t tp; // x4
uint64_t t0; // x5
uint64_t t1; // x6
uint64_t t2; // x7
uint64_t s0; // x8
uint64_t s1; // x9
uint64_t a0; // x10
uint64_t a1; // x11
uint64_t a2; // x12
uint64_t a3; // x13
uint64_t a4; // x14
uint64_t a5; // x15
uint64_t a6; // x16
uint64_t a7; // x17
uint64_t s2; // x18
uint64_t s3; // x19
uint64_t s4; // x20
uint64_t s5; // x21
uint64_t s6; // x22
uint64_t s7; // x23
uint64_t s8; // x24
uint64_t s9; // x25
uint64_t s10; // x26
uint64_t s11; // x27
uint64_t t3; // x28
uint64_t t4; // x29
uint64_t t5; // x30
uint64_t t6; // x31
} zx_riscv64_thread_state_general_regs_t;
踢
zx_status_t zx_thread_kick_restricted(zx_handle_t thread, uint32_t options);
如果线程当前处于受限模式,则将其踢出受限模式;如果线程当前不处于受限模式,则保存待处理的踢出操作。如果目标线程在受限模式下运行,则会通过向 zx_thread_enter_restricted
提供的 return_vector
退出到正常模式,并将原因代码设置为 ZX_RESTRICTED_REASON_KICK
。否则,对 zx_thread_enter_restricted
的下一次调用将不会进入受限模式,而是会使用原因代码 ZX_RESTRICTED_REASON_KICK
调度到所提供的入口点。
对同一线程对象的多次踢出会合并在一起。因此,如果多个线程在同一目标运行或进入受限模式时对其调用 zx_thread_kick_restricted
,则会观察到至少一次(但可能多次)ZX_RESTRICTED_REASON_KICK
返回。建议使用此系统调用的做法是,先记录启动同步数据结构的原因,然后调用 zx_thread_kick_restricted
。调用 zx_thread_enter_restricted
的线程应在每次观察到 ZX_RESTRICTED_REASON_KICK
时都查询此数据结构,并在重新进入受限模式之前处理所有待处理状态。
thread
句柄必须具有正确的 ZX_RIGHT_MANAGE_THREAD
。
此调用可能会失败:
ZX_ERR_WRONG_TYPE
-thread
不是线程。ZX_ERR_ACCESS_DENIED
-thread
没有 ZX_RIGHT_MANAGE_THREAD。ZX_ERR_BAD_STATE
-thread
已过时。
其他系统调用变更
面向 VMAR 的系统调用 - zx_vmar_map
和 zx_vmar_op_range
等系统调用不受此提案的影响。虽然 zx_vmar_root_self
不是系统调用,但会受到此提案的影响。如需了解详情,请参阅后续部分的讨论。
zx_process_read_memory
和 zx_process_write_memory
- 这些系统调用会接受进程句柄,并提供对进程内存的访问权限。目前(在此提案之前),这些调用不会跨越映射边界。也就是说,如果您请求读取跨越两个映射的 8 KiB 区域,则只会收到前 4 KiB。根据此提案,这些调用的 API 和语义不会发生变化。您需要对实现进行一项细微的更改,才能对适当的 VmAspace
(共享的或私有的)执行 FindRegion
调用,具体取决于所提供的 vaddr
。调用方将继续能够读取和写入进程的完整地址空间,包括共享区域和私有区域。
ZX_INFO_TASK_STATS
和 ZX_INFO_PROCESS_MAPS
- 实现已更新为同时包含正常模式和受限模式映射,但 API 保持不变。也就是说,结果不会区分正常和受限。
其他变更
zx_vmar_root_self
- 此提案的最大语义变更之一是,通过 zx_process_create_shared
创建的进程将不会有一个封装其整个地址空间的单个根 VMAR。而是会有两个 VMAR。一个用于共享区域,一个用于私有区域。是否以及如何将此语义更改公开给程序取决于语言运行时。zx_vmar_root_self
用于 C、C++ 和 Rust 运行时。在此提案中,zx_vmar_root_self
的行为更像是“获取默认 vmar”,会返回共享 VMAR,以便现有库(例如 scudo)会“默认”在共享区域上运行。
zx_process_self
- 由于 zx_process_self
会返回全局变量的值,而在共享进程中,该全局变量由共享区域提供支持,因此它实际上并未返回对自身的句柄。而是返回组中第一个共享进程(原型进程)的句柄。我们曾考虑过更改 zx_process_self
以使用线程本地存储空间(而非全局变量),但最终决定暂时搁置该想法(请参阅 https://fxbug.dev/352827964),改为更改 zx_process_self
存在问题的用法。请参阅下文。
pthread_create
- 这是 zx_process_self
使用存在问题的最有趣的情况。为了确保 pthread_create
在正确的进程中创建线程,它需要调用进程的句柄。为解决此问题,我们扩展了 pthread
结构体以包含进程句柄,并更改了 pthread_create
以使用结构体中的句柄,而不是 zx_process_self
返回的句柄。
调试程序 - 调试程序(例如 zxdb 和 fidlcat)可能需要进行更改,以了解某个线程的活动映射不一定与其进程中的所有线程相同。换句话说,我们可能需要创建一种方法,让调试程序能够查询挂起的线程的地址空间。
Fuchsia 进程构建器 - 由于我们更改了一些关于进程地址空间的可访问部分的假设,因此必须对进程构建器进行细微更改。例如,原型流程将没有下半部分。
启动 Starnix 组件 - Starnix 组件由组件管理器启动。与任何 ELF 组件一样,zx_process_create
用于创建银河中的第一个进程。与其他 ELF 组件不同,第一个 Starnix 进程的 VMAR 必须是共享的 VMAR。为此,我们定义了一个新的组件清单选项 is_shared_process
,用于指示组件管理器传递 ZX_PROCESS_SHARED
。这个新进程充当一种原型,负责在星系中创建后续进程。此组件清单选项可最大限度地减少对组件管理器的更改,同时避免 Starnix 本身需要参与 Fuchsia 进程构建。原型创建的每个进程都只是原封不动地共享原型的代码、数据和句柄。
性能
对现有系统调用的最小影响
对某些系统调用可能会产生轻微(但不为零)的性能影响。我们将使用 Zircon 微基准套件来确保任何影响都微不足道。除了运行时性能影响之外,某些内核对象可能会略微增大,导致内存用量略微增加。与运行时性能一样,我们预计内存用量增加幅度微不足道,但仍会进行测量和验证。
模式切换性能
受限模式的模式切换性能将比原生模式切换更耗费资源,因为 CPU 在正常模式和受限模式之间切换时实际上必须通过 EL1。不过,我们预计在 Starnix 中,这笔费用不会很高。
我们将添加新的 Zircon 微基准测试,以测量和跟踪模式切换开销。
安全注意事项
此机制旨在充当安全边界,需要由 Fuchsia 安全团队进行全面审核。
常规
使用这些新 Zircon 设施的用户模式程序本身将充当内核,因此必须小心防范在受限模式下执行的代码对其自身数据和执行的侵害,就像 Zircon 内核防范有 bug 或可能存在恶意的用户模式 Fuchsia 代码一样。
虽然本文档的其他部分已详细介绍了这些内容,但我们还是要重点介绍一些要点:
- 共享句柄 - 共享进程组中的所有进程都使用单个句柄表,因此组中任何进程中以正常模式执行的任何线程都可以使用表中的任何句柄。
- 共享映射 - 共享进程组中的所有进程彼此共享一部分地址空间(共享 VMAR)。在正常模式下执行时,该共享区域中的映射可供组中的所有线程访问。
- 在正常模式下可访问的受限区域 - 在正常模式下执行时,可以访问进程的受限区域。由于此区域包含不可信的代码和数据,因此必须小心谨慎,绝不信任受限区域中的任何代码或数据(例如,先复制出去,然后再进行验证,以避免 TOCTOU)。
- 必须在进入时清除/恢复 - 进入受限模式时,程序必须小心清除、恢复或以其他方式替换
zx_restricted_state_t
指定以外的寄存器状态。这包括浮点、矢量和调试寄存器状态。否则,可能会将数据泄露给不可信的代码,或造成更严重的问题。 - 必须在返回时清除/恢复 - 在返回正常模式时,程序必须小心清除、恢复或以其他方式替换
zx_restricted_state_t
指定之外的寄存器状态。如果不这样做,可能会允许不可信的代码影响或控制用户模式监督程序的执行,从而导致逃逸。
推测执行漏洞
运行不可信的访客代码的用户模式内核需要防范推测性执行攻击,就像 Zircon 内核一样。不过,由于每次从受限模式切换到或切换出受限模式都涉及 Zircon 内核,因此我们有机会简化来宾内核,并可能不受某些推测性执行漏洞的影响。其基本思想是,来宾内核可以依赖 Zircon 内核在内核入口和出口点执行的推测执行缓解措施。当然,此策略需要由 Fuchsia 安全团队进行审核,并且需要让客内核的所有者及时了解每种新发现的推测性执行漏洞。
访问权限受限
在受限模式下,线程只能访问受限地址空间,无法发出 Zircon 系统调用。有两种机制可防止受限模式线程发出 Zircon 系统调用。首先,系统调用拦截实现将所有系统调用路由到正常模式。其次,即使系统将系统调用路由到 Zircon 内核,受限区域中缺少 Zircon vDSO 也会导致系统无法为其提供服务。
值得注意的是,在进入受限模式之前,调用方应注意不要无意中通过寄存器将信息泄露“到”受限模式。
当然,如果受限模式下的代码能够以某种方式破坏同一进程中的普通模式代码(例如通过普通模式代码中的 bug),那么它也可能会破坏构成共享系统映像的所有 Zircon 进程,因为它们都通过共享映射共享了部分代码和数据。
监督器模式访问和执行保护
x86 的 SMAP 和 SMEP 以及 arm64 的 PAN 和 PXN 是硬件功能,用于限制内核代码可以访问或执行的映射。它们旨在提高利用内核 bug 的难度。由于受限模式是一种软件结构,并且访客内核在用户模式下运行,因此我们无法在用户模式监督程序中直接利用这些功能。另请参阅替代方案中关于备选映射的讨论。
隐私注意事项
与任何其他内核功能一样,这些新内核功能本身不会带来隐私问题。不过,未来在基于 Fuchsia 的产品上运行 Linux 二进制文件时,应用这些功能可能需要进行隐私权审核。
确定涉及 Linux 二进制文件的特定端到端产品用例后,我们需要评估该用例对隐私的影响。
未来方向
此设计的几个方面可能会随时间推移而发生变化。本部分介绍了我们近期可能会重新考虑的一些方面。后续文档将对此进行更详细的介绍。
API 分解
此提案集成了以 Starnix 用例为重点的 API 背后的多项功能。例如,在其当前形式下,线程必须位于共享进程中,才能限制其地址空间。虽然这对 Starnix 来说没问题,但这种耦合会限制此系统的适用性。当我们有更多此技术的用例后,应考虑将 API 和功能集分解为更独立的构建块。
一个根 VMAR
关于分解的备注:在日后的提案中,我们应重新考虑一个进程可能具有多个根 VMAR 的概念。在当前提案中,共享 VMAR 和私有 VMAR 都被视为根 VMAR,因为它们都不包含在其他 VMAR 中。这种多根方法是一种实用设计决策,是基于 VmAspace
、VmAddressRegion
和 VmMapping
类的现有实现而做出的。通过重新设计/重构这些类,我们可以消除进程需要有多个根 VMAR 的需求(不过,我们最终可能出于其他原因需要支持多个根)。
改进了寄存器保存功能
我们可能需要改进的一个方面是,在进入/退出受限模式时如何保存、恢复或清零 FPU/矢量寄存器状态。
Starnix 代码或其某个依赖项可能会使用 FPU/矢量寄存器(例如 memcpy),因此 Starnix 必须小心保存/恢复受限的 FPU/矢量状态。目前,Starnix 会在每次受限进入和退出时保存/恢复所有 FPU/矢量状态。为了提高性能,我们可能需要编译在受限退出点上运行的部分 Starnix 逻辑,以推迟或减少需要保存的一组寄存器。
尽管 Starnix 会小心地保存/恢复 FPU/矢量寄存器状态,但从技术层面讲,Zircon vDSO 可能会破坏调用方在进入受限模式之前建立的状态(请参阅 https://fxbug.dev/42062491)。目前,通过验证受限模式 FPU/矢量寄存器状态的测试来缓解此脆弱性。
此外,在某些情况下,我们或许能够通过提供利用某些架构上可用的特权保存/恢复指令的特殊系统调用,或者利用硬件辅助的延迟保存/恢复方案来提升性能。
测试
与内核接口的其他方面一样,我们将实现核心测试和内核内单元测试来验证正确性。Zircon 微基准测试将用于衡量性能。
文档
将添加/更新有关新系统调用和修改后的系统调用的树内文档。
将更新 Zircon 异常的树内文档。
我们将编写一个树内概览文档,介绍这项新机制。
缺点、替代方案和未知情况
缺点
这些更改会影响多个内核组件,并增加内核的复杂性。虽然设计会努力对这种新复杂性进行分隔并将其降到最低,但这仍然会产生非零成本。
替代方案
Hypervisor - 这种纯软件设计的替代方案是利用硬件的 Hypervisor 功能,让“客户机内核”能够直接发出 Zircon 系统调用。有点像极端版本的“准虚拟化”。如需了解详情,请参阅 RFC:适用于虚拟化的直接模式。采用纯软件方法的一个原因是,确保我们可以在无法访问硬件辅助虚拟化的架构和/或设备上应用该方法。
一进程式 - 所提设计使用一个 Zircon 进程对每个来宾进程进行建模。这种方法的一个优点是,每个来宾进程对 Zircon 工具(如 ps
或 kill
)都是“可见”的。另一种设计方案将每个来宾进程建模为单个 Zircon 进程中的线程集合。这种替代设计会将访客进程“隐藏”在 Zircon 用户模式的其余部分中,但在其他方面没有太大差异。
监督程序-监督程序隔离 - 与所提议的设计方案的替代方案包括消除共享区域,并要求每个监督程序都通过 IPC 或某些“手动管理”的显式映射相互协调。这种替代方案的优势在于,使用另一个进程破坏一个进程的难度更高(隔离性更强),但会使实现来宾内核的许多方面变得更加困难(请参阅目标中的“自然且简化的编程模型”)。跨进程协调开销也可能会影响性能。
将客户端和监督程序放入单独的进程中 - 引入新模式的替代方案是将不可信代码放入不含 Zircon vDSO 的单独进程中。然后,客机系统调用将由从不可信进程切换到实现客机内核的 Zircon 进程的上下文切换组成。这种方法可以正常运行,但可能需要为每个系统调用进行“更完整”的上下文切换,并且在运行时性能方面更耗费资源。此外,这可能会导致每个来宾任务都需要两个 Zircon 线程,而受限模式下每个来宾任务只需要一个 Zircon 线程(一个 Zircon 线程用于运行来宾任务,另一个 Zircon 线程用于代表来宾任务实现系统调用)。与受限模式相比,在进行 IPC 以及在来宾和监督程序之间进行上下文切换时,为不可信代码/数据使用单独的进程会有一些性能劣势。
专用读写与模式状态 VMO - 此设计的早期迭代使用专用系统调用来访问模式状态 (zx_restricted_state_read
和 zx_restricted_state_write
)。不过,这些系统调用很快就出现了性能问题,因此被淘汰,取而代之的是模式状态 VMO。
解除绑定 - 在之前的迭代中,有一个显式解除绑定系统调用,用于“撤消”由 zx_thread_prepare_restricted
执行的绑定操作。我们发现解除绑定是多余的,并且只在某些测试代码中使用。我们移除了它,以简化 API。
用于访问和执行保护的备选映射 - 用户模式监督程序可用的一组漏洞利用缓解工具不同于内核模式监督程序可用的工具。一般来说,这两者都可以使用各种基于软件的技术(例如 CFI),但用户模式监督程序无法使用 x86 的 SMAP 或 SMEP,或 arm64 的 PAN 或 PXN 等硬件功能。这些硬件功能提供了一种方法,用于标记在内核模式下执行时可以在不同上下文中访问哪些映射。SMEP/PXN 和 SMAP/PAN 有一些潜在替代方案,可视需要进一步探索。其基本思想是维护一组具有不同保护位数的备用映射,并在正常模式和受限模式之间切换时或在共享区域和受限区域之间执行“用户复制”操作时在这些映射之间进行转换。当然,维护和使用备选映射会带来一些显著的性能和复杂性开销,因此这种替代方案不一定可行。
未知
核心内核对象关系的更改 - 此提案会更改一些核心内核对象关系。例如,它会在句柄表和进程之间建立一对多关系,进程不再只有一个地址空间(或根 VMAR),futex 可以在进程之间共享,给定句柄可以同时“存在于多个进程中”,等等。这些更改可能会导致潜在的设计缺陷。
调试和诊断 - 此提案会影响调试程序和其他以进程为中心的工具(例如内存监视器 [fx mem
])的模型。我们需要与 zxdb 和内存监视器的所有者合作,仔细考虑相关影响。与此相关,我们需要详细了解假定进程只有一个有效地址空间(例如 zx_process_read_memory
)的现有系统调用
在先技术和参考文档
请参阅 RFC-0082:在 Fuchsia 上运行未修改的 Linux 程序及其“先前技术和参考文献”部分。
此设计基于多份 Google 内部文档和大量对话。 另请参阅
- go/zx-unified-aspace
- go/zx-restricted-exceptions
- go/zx-restricted-kick
- go/zx-restricted-roadmap
- go/zx-starnix-faults
- go/zx-starnix-state-transitions
- go/zx-multi-aspace-processes
- go/zx-restricted-mode
受限模式的灵感来自 ARMv8-A 异常级别(“如果我们有 EL-minus-1 级别,会怎么样?”)。