核心调度

核心调度支持允许用户空间定义可以共享核心的任务组。这些组可以为安全用例(一组任务不信任另一组任务)或性能用例(某些工作负载可能受益于在同一核心上运行,因为它们不需要共享核心的相同硬件资源,或者如果它们确实共享硬件资源需求,则可能更喜欢不同的核心)指定。本文档仅描述安全用例。

安全用例

跨 HT 攻击涉及攻击者和受害者在同一核心的不同超线程上运行。MDS 和 L1TF 就是此类攻击的示例。跨 HT 攻击的唯一完整缓解措施是禁用超线程 (HT)。核心调度是一种调度程序功能,可以缓解一些(并非全部)跨 HT 攻击。它允许通过确保只有用户指定的受信任组中的任务才能共享核心来安全地打开 HT。核心共享的增加也可以提高性能,但是不能保证性能始终会提高,尽管在许多实际工作负载中都看到了这种情况。理论上,核心调度的目标是至少与禁用超线程时一样好。在实践中,情况大多如此,但并非总是如此:因为在核心中的 2 个或更多 CPU 上同步调度决策会涉及额外的开销,尤其是在系统负载较轻时。当 total_threads <= N_CPUS/2 时,额外的开销可能会导致核心调度比禁用 SMT 的情况执行得更差,其中 N_CPUS 是 CPU 的总数。请始终测量工作负载的性能。

用法

通过 CONFIG_SCHED_CORE 配置选项启用核心调度支持。使用此功能,用户空间定义可以在同一核心上共同调度的任务组。核心调度程序使用此信息来确保不在同一组中的任务永远不会同时在核心上运行,同时尽力满足系统的调度要求。

可以通过 PR_SCHED_CORE prctl 接口启用核心调度。此接口为创建核心调度组以及从创建的组中添加和删除任务提供支持

#include <sys/prctl.h>

int prctl(int option, unsigned long arg2, unsigned long arg3,
        unsigned long arg4, unsigned long arg5);
选项

PR_SCHED_CORE

arg2

操作命令,必须是一个

  • PR_SCHED_CORE_GET -- 获取 pid 的 core_sched cookie。

  • PR_SCHED_CORE_CREATE -- 为 pid 创建一个新的唯一 cookie。

  • PR_SCHED_CORE_SHARE_TO -- 将 core_sched cookie 推送到 pid

  • PR_SCHED_CORE_SHARE_FROM -- 从 pid 中拉取 core_sched cookie。

arg3

应用操作的任务的 pid

arg4

应用操作的 pid_type。它是 PR_SCHED_CORE_SCOPE_ 前缀的宏常量之一。例如,如果 arg4 是 PR_SCHED_CORE_SCOPE_THREAD_GROUP,则此命令的操作将对 pid 的任务组中的所有任务执行。

arg5

用户空间指针,指向一个 unsigned long long,用于存储 PR_SCHED_CORE_GET 命令返回的 cookie。对于所有其他命令,应为 0。

为了使进程能够将 cookie 推送到进程或从进程中拉取 cookie,它需要对进程具有 ptrace 访问模式:PTRACE_MODE_READ_REALCREDS

构建任务层次结构

构建共享 cookie 从而共享核心的线程/进程层次结构的最简单方法是依赖于核心调度 cookie 在 forks/clones 和 execs 之间继承这一事实,因此为“初始”脚本/可执行文件/守护程序设置 cookie 将使每个生成的子项都位于同一核心调度组中。

设计/实现

每个被标记的任务在内核中都被分配一个 cookie。如用法中所述,具有相同 cookie 值的任务被假定为彼此信任并共享一个核心。

基本思想是,每个调度事件都尝试为核心的所有同级选择任务,以便在任何时间点在核心上运行的所有选定任务都是受信任的(相同的 cookie)。内核线程被认为是受信任的。空闲任务被认为是特殊的,因为它信任一切,而一切都信任它。

在核心的任何同级上发生 schedule() 事件期间,如果该同级已将任务排队,则选择该同级核心上优先级最高的任务并将其分配给调用 schedule() 的同级。对于核心中的其余同级,如果它们的各自运行队列中有一个可运行的任务,则选择具有相同 cookie 的优先级最高的任务。如果没有具有相同 cookie 的任务可用,则选择空闲任务。空闲任务是全局信任的。

一旦为核心中的所有同级选择了任务,就会向选择了新任务的同级发送 IPI。接收到 IPI 的同级将立即切换到新任务。如果为同级选择了空闲任务,则认为该同级处于强制空闲状态。也就是说,它可能在其运行队列中有要运行的任务,但仍然必须运行空闲。下一节将对此进行更多介绍。

超线程的强制空闲

调度器会尽力寻找彼此信任的任务,以便所有被选定调度的任务都在核心中具有最高优先级。然而,某些运行队列可能存在与核心中最高优先级任务不兼容的任务。为了优先考虑安全性而不是公平性,如果最高优先级任务相对于核心范围内的最高优先级任务不可信,则可能会强制一个或多个兄弟线程选择较低优先级的任务。如果兄弟线程没有可信任的任务运行,调度器将强制其进入空闲状态(调度空闲线程运行)。

当选择最高优先级任务运行时,会向兄弟线程发送重新调度的进程间中断 (IPI) 以强制其进入空闲状态。这导致需要考虑 4 种情况,具体取决于 VM 或常规用户模式进程是否在任一 HT 上运行。

       HT1 (attack)            HT2 (victim)
A      idle -> user space      user space -> idle
B      idle -> user space      guest -> idle
C      idle -> guest           user space -> idle
D      idle -> guest           guest -> idle

请注意,为了获得更好的性能,我们不会等待目标 CPU(受害者)进入空闲模式。这是因为发送 IPI 会使目标 CPU 立即从用户空间进入内核模式,或者在虚拟机的情况下进入 VMEXIT。充其量,这只会泄露一些可能不值得保护的调度器元数据。在某些架构上,也可能延迟收到 IPI,但在 x86 的情况下未观察到这种情况。

信任模型

核心调度通过为任务分配相同的 Cookie 值标签来维护任务组之间的信任关系。当启用核心调度的系统启动时,所有任务都被认为彼此信任。这是因为核心调度器在用户空间使用上述接口来传达信任关系之前,不会获取有关信任关系的信息。换句话说,所有任务的默认 Cookie 值均为 0,并被视为系统范围可信。还会避免强制运行 Cookie-0 任务的兄弟线程进入空闲状态。

一旦用户空间使用上述接口对任务集进行分组,则此类组内的任务被认为彼此信任,但不信任组外的任务。组外的任务也不信任组内的任务。

核心调度的局限性

核心调度试图保证只有受信任的任务才能在核心上同时运行。但是,在很短的时间窗口内,可能会存在不受信任的任务同时运行的情况,或者内核可能与不受内核信任的任务同时运行。

IPI 处理延迟

核心调度仅选择受信任的任务一起运行。IPI 用于通知兄弟线程切换到新任务。但是,在某些架构上(在 x86 上,未观察到这种情况),接收 IPI 可能存在硬件延迟。这可能导致攻击者任务在其兄弟线程收到 IPI 之前开始在 CPU 上运行。即使在进入用户模式时刷新了缓存,兄弟线程上的受害者任务也可能在攻击者开始运行后将数据填充到缓存和微架构缓冲区中,这可能导致数据泄漏。

核心调度无法解决的公开的跨 HT 问题

1. 对于 MDS

核心调度无法防止在用户模式下运行的兄弟线程和其他在内核模式下运行的线程之间发生 MDS 攻击。即使所有兄弟线程都运行彼此信任的任务,当内核代表任务执行代码时,它也不能信任在兄弟线程中运行的代码。对于兄弟 CPU 模式(主机或访客模式)的任何组合,都可能发生此类攻击。

2. 对于 L1TF

核心调度无法防止 L1TF 访客攻击者利用访客或主机受害者。这是因为访客攻击者可以制作无效的 PTE,这些 PTE 由于易受攻击的访客内核而不会被反转。唯一的解决方案是禁用 EPT(扩展页表)。

对于 MDS 和 L1TF,如果将访客 vCPU 配置为彼此不信任(通过单独标记),则访客到访客的攻击将消失。或者,系统管理员策略可能将访客到访客的攻击视为访客问题。

解决这些问题的另一种方法是使系统上的每个不受信任的任务都不信任其他每个不受信任的任务。尽管这可能会降低不受信任任务的并行性,但它仍然可以解决上述问题,同时允许系统进程(受信任的任务)共享一个核心。

3. 保护内核 (IRQ、syscall、VMEXIT)

不幸的是,核心调度不能保护在兄弟超线程上运行的内核上下文彼此隔离。缓解措施的原型已发布到 LKML 以解决此问题,但是否可以实际利用此类窗口,以及原型的性能开销是否值得(更不用说增加的代码复杂性)仍有争议。

其他用例

核心调度的主要用例是缓解启用 SMT 时出现的跨 HT 漏洞。此功能还有其他用例

  • 隔离需要整个核心的任务:示例包括实时任务、使用 SIMD 指令的任务等。

  • 组调度:可以使用核心调度来实现需要一起调度的任务组的要求。一个示例是 VM 的 vCPU。