核心调度

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

安全用例

跨 HT 攻击涉及攻击者和受害者在同一核心的不同超线程 (Hyper Thread) 上运行。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

指向一个无符号长长整型的用户空间指针,用于存储由 PR_SCHED_CORE_GET 命令返回的 cookie。对于所有其他命令,应为 0。

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

构建任务层次结构

构建共享 cookie 从而共享核心的线程/进程层次结构的最简单方法是依赖于核心调度 cookie 在 fork/clone 和 exec 之间继承的事实,从而为“初始”脚本/可执行文件/守护进程设置 cookie 将使所有派生的子进程都位于同一核心调度组中。

设计/实现

每个被标记的任务都在内核内部分配一个 cookie。正如用法中提到的,具有相同 cookie 值的任务被认为是相互信任并共享一个核心的。

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

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

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

超线程的强制空闲

调度器会尽力寻找相互信任的任务,以确保所有被选择调度的任务在核心中都具有最高优先级。然而,有些运行队列可能包含与核心中最高优先级任务不兼容的任务。为了安全而非公平,如果最高优先级任务与核心范围内的最高优先级任务不被信任,则一个或多个同级可能会被强制选择较低优先级的任务。如果一个同级没有受信任的任务可运行,它将被调度器强制空闲(空闲线程被调度运行)。

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

       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。