RFC-0261:快速高效地进行用户空间内核模拟

RFC-0261:快速高效的用户空间内核模拟
状态已接受
区域
  • 内核
说明

使 Fuchsia 程序能够充当不受信任的访客代码的安全高效的监督者。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2022-06-07
审核日期(年-月-日)2024-10-01

摘要

本文档提出了一组功能,使 Fuchsia 程序能够在不使用虚拟机监控器技术的情况下充当不受信任的访客代码的安全高效的监督者。主要用例是实现更快、更高效的 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 抽象层之上实现用户空间内核。

在此架构中,每个 guest 线程将由一个 Zircon 线程建模,每个 guest 进程将由一个 Zircon 进程建模。每个 Zircon 进程都将拥有一个对所有进程通用的地址空间区域。对于客户内核开发者而言,共享区域将使客户内核看起来更像是一个单线程程序。

元素 1:受限模式

这是核心元素。

目前,线程运行时会处于两种模式之一:用户模式或内核模式。当线程发出系统调用时,它会通过模式切换从用户模式转换到内核模式。完成系统调用后,它会转换回用户模式。

注意:为简单起见,本文档将使用 ARMv8a 术语,除非另有说明。

通过系统调用进行模式切换

当然,系统调用并非唯一会导致模式切换的事件。内核可能需要处理的任何事件都会触发模式切换到内核模式。这包括页面错误、异常和中断。

我们引入了可选的第三种模式,称为受限模式。此新模式是可选的,因为系统中的并非所有线程都会进入受限模式。与用户模式相比,内核模式是限制较少的执行环境;与用户模式相比,受限模式是限制较多的执行环境。

虽然内核模式和用户模式会映射到硬件功能(例如 ARMv8-A 中的 EL1 和 EL0),但受限模式不会。它是一种纯软件结构。硬件不知道受限模式。从硬件的角度来看,它只是用户模式。

术语:为避免混淆,在讨论包含所有三种模式的线程时,我们将非受限用户模式称为“正常模式”。

从概念上讲,受限模式低于正常模式。正如在内核模式下执行的代码充当用户模式程序的监督者一样,在正常模式下执行的代码充当受限模式程序的 guest 内核或用户模式监督者。

受限模式低于普通模式

隔离

受限模式的一项关键功能是隔离。在正常模式和受限模式之间切换时,通过更改可访问的地址空间范围和寄存器状态来实现隔离。

通常,进程的地址空间分为两半,一半(内核地址空间)仅在线程处于内核模式时可访问,另一半(用户地址空间)始终可访问。在受限模式下,用户地址空间会进一步划分,总共提供三个区域。

只有当线程处于内核模式时,才能访问最上层区域。中间区域只能从内核模式或正常模式访问。较低区域可随时访问(有关此方面的更多信息,请参阅“直接访问”部分)。 此较低区域称为受限区域,因为它是受限模式下的线程可以访问的唯一区域。

进入受限模式后,Zircon 内核将使用正常模式提供的值加载通用寄存器。清除或加载超出通用寄存器的寄存器是正常模式代码的责任。例如,Zircon 内核不会清除或加载浮点或矢量寄存器。x86 的 fs.basegs.base、arm64 的 tpidr_el0 以及 riscv64 的 tp 都被视为通用寄存器。如需了解详情,请参阅输入部分。

线程进入受限模式后,只能访问其寄存器状态和受限区域中的映射。

在受限模式下,Zircon 内核会将所有系统调用路由到正常模式,无论它们是否是从 Zircon vDSO 发出的(下一部分会详细介绍这一点)。这意味着,即使 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。

通过系统调用离开

在受限模式下执行 syscall 指令会强制线程返回到正常模式,以便用户模式主管处理 syscall。

当执行系统调用指令时,CPU 将首先陷入内核模式。内核中的常规系统调用路径将测试线程是否处于受限模式。如果如此,它将采取替代路径,该路径涉及保存受限的通用寄存器状态,并直接返回到正常模式,返回地址为原始系统调用中传入的地址。然后,正常模式必须恢复其保存的寄存器状态,然后处理系统调用。从逻辑上讲,控制权将从受限模式传递到正常模式,但实际上,在传递过程中的每个阶段,控制权都会在内核模式下跳动。示例如下:

受限模式过渡

  1. 受限模式会发出系统调用
  2. 系统调用来自受限模式,因此内核返回到正常模式以允许其处理该调用
  3. 正常模式处理调用,并通过系统调用(逻辑上)返回到受限模式
  4. 内核处理“进入受限模式系统调用”,并跳回受限模式

如前所述,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。返回此状态代码可有效解除启动锁定。

踢腿不会“叠加”。例如,假设目标线程处于正常模式,而另一个线程发出了 5 次踢操作。当目标线程进入受限模式时,它会立即返回,从而有效处理所有五个踢出操作。

一般来说,zx_thread_enter_restricted 的调用方应编写为能够处理虚假踢出。

元素 2:直接访问

由于操作系统内核经常需要访问用户模式数据,因此高效地完成此操作非常重要。操作系统内核通常可以直接访问其用户进程的代码和数据。例如,Linux 内核和 Zircon 内核都可以直接访问用户进程的地址空间。

直接访问是此设计的一个关键要素。

如前所述,在正常模式下,Zircon 线程可以访问其进程的完整地址空间;而在受限模式下,它只能访问地址空间的一部分。因此,在正常模式下运行时,Zircon 线程可以充当客户机内核,并且可以访问受限区域中的客户机用户代码和数据。

用户地址空间实际上分为两部分。通过将地址空间分为两部分,我们可以在某些架构上利用一些显著的地址空间切换效率(即屏蔽一半而不是实际切换,TCR_EL1.T0SZ)。

由于在 Starnix 下运行的程序可能要求其地址空间“接近”零地址(例如 MAP_32BIT),因此我们选择将最低区域设为受限区域。

这种方法存在一些限制。主要原因是,由于受限区域是完整区域的连续子集,因此不得存在冲突的映射。用于访客代码和数据的地址必须与用于用户模式监控程序的代码和数据的地址不相交。由于受限区域位于用户地址空间的“下半部分”,因此用于实现用户模式监控程序的 Fuchsia 运行时必须灵活,并且不要求其任何映射位于下半部分。为此,我们不得不对 Fuchsia 进程构建器进行一些更改。清理器有时对映射的位置有额外要求,因此可能需要进行后续工作才能支持某些清理器。

元素 3:共享状态

内核通常具有在用户任务之间共享并由系统调用操作的数据结构。对于此类系统,内核编程模型类似于多线程程序,其中每个用户任务都是一个线程,可以访问共享(内核)地址空间,也可以访问一些私有的、特定于用户任务的区域。共享区域中的代码和数据构成共享系统映像。

为了能够自然地开发具有共享系统映像的用户空间内核,我们将扩展 Zircon 进程模型,以支持一种新的进程,该进程可与其他进程共享其地址空间的一部分。这些进程的集合将托管访客用户任务和访客内核。

我们之前介绍了如何屏蔽进程的地址空间(完整与受限)。非受限区域将在托管同一系统映像的 guest 任务的所有 Zircon 进程之间共享。每个进程都将拥有自己的独立受限区域,该区域对相应进程是私有的。 因此,当这些进程中的某个线程以正常模式执行时,它将有权访问完整的地址空间,包括共享映射以及受限区域中的私有映射。

除了共享这些不受限制的映射之外,托管 guest 任务的 Zircon 进程还将共享单个句柄表和单个 futex 上下文。共享区域中的任何句柄值(例如 zx_handle_t)都可供任何进程使用,而无需显式句柄复制或转移操作。通过共享(部分)映射、句柄和 futex 上下文,一组进程看起来和感觉起来更像单个进程内的一组线程,从而简化编程模型。

在访问共享区域或受限区域中的映射时,必须注意避免创建数据竞争,就像在多线程进程中访问内存时一样。

虽然共享进程组中的线程具有不同的进程身份,但它们都共享相同的句柄表、futex 上下文和地址空间的共享区域。这种方法的一个含义是,对于 Starnix,正常模式进程崩溃类似于内核恐慌,因为崩溃的进程可能留下了一些处于不一致状态的共享数据。

当然,受限模式崩溃的情况非常不同,因为受限模式无法访问正常模式状态。由于在正常模式下执行的代码充当受限模式代码的监督者,因此在大多数情况下(例如 Starnix),它应能妥善处理受限模式的“崩溃”,例如架构异常和未处理的页面错误。

实现

我们首先构建了 x86 支持,因为这是 Starnix 最初的目标。 后来,我们添加了 arm64 和 riscv64 支持。

虽然设计和实现侧重于 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 并将其绑定到自身来做好准备。模式状态 VMO 将由线程和 Zircon 内核用于保存/恢复受限模式通用寄存器状态。VMO 由线程(以及任何明确创建的用户映射)保留,直到线程终止。

成功调用 prepare 后,线程可以使用 zx_thread_enter_restricted 进入受限模式。离开受限模式后,线程可能会重新进入,而无需重新准备,无论它是因 ZX_RESTRICTED_REASON_SYSCALLZX_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 一样,一旦 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 结构体填充该 VMO。

与常规函数调用不同,成功的 zx_thread_enter_restricted 调用不会返回到调用方上下文。相反,在离开受限模式时,线程将仅跳转到 return_vector 中指定的地址,其中 context 和原因代码已加载到两个通用寄存器中。所有其他寄存器(包括堆栈指针)都将处于未指定状态。return_vector 处的正常模式代码负责使用 context、原因代码和模式状态 VMO 的内容来重建系统调用前状态。

可能的原因代码包括 ZX_RESTRICTED_REASON_SYSCALLZX_RESTRICTED_REASON_EXCEPTIONZX_RESTRICTED_REASON_KICK。返回时,模式状态 VMO 的偏移量 0 将包含以下结构之一,具体取决于原因代码:zx_restricted_syscall_tzx_restricted_exception_tzx_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_restrictedreturn_vector 退出到正常模式,并将原因代码设置为 ZX_RESTRICTED_REASON_KICK。否则,对 zx_thread_enter_restricted 的下一次调用将不会进入受限模式,而是会调度到提供的入口点,并附带原因代码 ZX_RESTRICTED_REASON_KICK

同一线程对象上的多次 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_mapzx_vmar_op_range 等系统调用不受此提案的影响。虽然 zx_vmar_root_self 不是系统调用,但会受到此提案的影响。如需了解详情,请参阅下一部分中的讨论。

zx_process_read_memoryzx_process_write_memory - 这些系统调用会获取进程句柄,并提供对进程内存的访问权限。目前(在此提案之前),这些调用不会跨越映射边界。也就是说,如果您请求读取跨越两个映射的 8KiB 区域,您只会获得前 4KiB。根据此提案,这些调用的 API 和语义不会发生变化。需要对实现进行小幅更改,以便根据提供的 vaddr 对相应的 VmAspace(共享的或私有的)执行 FindRegion 调用。调用者将继续能够读取和写入进程的完整地址空间,包括共享区域和私有区域。

ZX_INFO_TASK_STATSZX_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 用于创建 Galaxy 中的第一个进程。与其他 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 指定范围的寄存器状态。如果不这样做,不受信任的代码可能会影响或控制用户模式监控程序的执行,从而导致逃逸。

推测执行漏洞

运行不受信任的 guest 代码的用户模式内核需要像 Zircon 内核一样防范推测性执行攻击。不过,由于每次进入或退出受限模式都需要经过 Zircon 内核,因此我们有机会简化 guest 内核,并使其可能不受某些推测性执行漏洞的影响。其理念是,guest 内核可以在内核入口点和出口点依赖 Zircon 内核执行的推测执行缓解措施。当然,此策略需要由 Fuchsia 安全团队审核,并且每当发现新的推测性执行漏洞时,都需要让 guest 内核的所有者及时了解情况。

有限访问

在受限模式下,线程只能访问受限的地址空间,无法发出 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 中。这种多根方法是一种实用的设计决策,其动机是 VmAspaceVmAddressRegionVmMapping 类的现有实现。重新设计/重构这些类将使我们无需让进程拥有多个根 VMAR(尽管我们最终可能出于其他原因而希望支持多个根)。

改进了寄存器保存

我们可能需要改进的一个方面是,在进入/离开受限模式时,如何保存、恢复或清零 FPU/向量寄存器状态。

Starnix 代码或其某个依赖项可能会使用 FPU/向量寄存器(例如 memcpy),因此 Starnix 必须注意保存/恢复受限的 FPU/向量状态。目前,Starnix 会在每次受限的进入和退出时保存/恢复所有 FPU/矢量状态。为了提高性能,我们可能需要编译在受限退出时运行的部分 Starnix 逻辑,以延迟或减少需要保存的寄存器集。

尽管 Starnix 会尽力保存/恢复 FPU/矢量寄存器状态,但从技术上讲,Zircon vDSO 可能会覆盖调用方在进入受限模式之前建立的状态(请参阅 https://fxbug.dev/42062491)。目前,通过验证受限模式 FPU/向量寄存器状态的测试来缓解此脆弱性。

此外,在某些情况下,我们或许可以通过提供特殊的系统调用来提高性能,这些系统调用可以利用某些架构上提供的特权保存/恢复指令,或者利用硬件辅助的延迟保存/恢复方案。

测试

与内核接口的其他方面一样,我们将实现核心测试和内核内单元测试来验证正确性。Zircon 微基准将用于衡量性能。

文档

将添加/更新有关新系统调用和修改后的系统调用的树内文档。

将更新 Zircon 异常的树内文档。

我们将编写一个树内概览文档来介绍这一新机制。

缺点、替代方案和未知因素

缺点

这些更改会影响多个内核组件,并增加内核的复杂性。虽然该设计力求将这种新的复杂性进行划分并尽可能降低其影响,但仍会产生一定的成本。

替代方案

Hypervisor - 除了这种纯软件设计之外,还可以利用硬件的 Hypervisor 功能,让“guest 内核”直接发出 Zircon 系统调用。有点像准虚拟化的极端版本。如需了解详情,请参阅 RFC:虚拟化的直接模式。采用纯软件方法的一个原因是,确保我们可以在无法访问硬件辅助虚拟化的架构和/或设备上应用该方法。

全部在一个进程中 - 所呈现的设计使用一个 Zircon 进程来对每个 guest 进程进行建模。此方法的一个优点是,每个 guest 进程对 pskill 等 Zircon 工具都是“可见的”。另一种设计方案是将每个 guest 进程建模为单个 Zircon 进程中的线程集合。这种替代设计会向 Zircon 用户模式的其余部分“隐藏”guest 进程,但除此之外,并没有太大的不同。

监督程序-监督程序隔离 - 建议设计方案的替代方案是消除共享区域,并要求每个监督程序进程通过 IPC 或一些“手动管理的”显式映射相互协调。这种替代方案的优势在于,可以更难使用一个进程来入侵另一个进程(隔离性更强),但会更难实现 guest 内核的许多方面(请参阅目标中的“自然且简化的编程模型”)。跨进程的协调开销也可能会影响性能。

访客和主管在单独的进程中 - 引入新模式的替代方案是将不受信任的代码放入没有 Zircon vDSO 的单独进程中。然后,访客系统调用将包括从不受信任的进程到实现访客内核的 Zircon 进程的上下文切换。这种方法可以正常运行,但可能需要为每个系统调用进行“更完整”的上下文切换,并且在运行时性能方面成本更高。此外,与受限模式下每个 guest 任务需要一个 Zircon 线程(一个 Zircon 线程用于运行 guest 任务,一个用于代表 guest 任务实现系统调用)相比,它可能会导致每个 guest 任务需要两个 Zircon 线程。与受限模式相比,在 IPC 和访客与监督者之间的上下文切换方面,为不受信任的代码/数据使用单独的进程有一些性能劣势。

专用读/写与模式状态 VMO - 此设计的早期迭代版本使用专用系统调用来访问模式状态(zx_restricted_state_readzx_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 内部文档和大量对话。 另请参阅

受限模式的灵感来自 ARMv8-A 异常级别(“如果我们有 EL-minus-1 会怎么样?”)。