LWN:通用的地址空间隔离方案!

关注了就能看到更多这么棒的文章哦~

Generalized address-space isolation

By Jonathan Corbet
March 3, 2022
DeepL assisted translation
https://lwn.net/Articles/886494/

Meltdown 和 Spectre 漏洞的披露,让人们看到了太广泛地地址空间共享会带来的风险。尽管硬件提供的保护机制应该阻止对敏感数据的不应该有的访问,但这些漏洞往往可以被利用来泄露这些数据。因此,从一开始,人们制定的缓解策略(mitigation strategies)就包括减少共享地址空间,但还可以更进一步,而且人们对这个方向也一直很感兴趣。现在,Junaid Shahid 发布的这组 patch set(包含了 Ofir Weisse 的成果,并且受到 Alexandre Chartre 早期 patch 的启发)就展示了为内核创建一个通用的地址空间隔离(ASI)机制所需要做什么。

Protecting data with ASI

当 CPU 在预测执行模式下被欺骗去访问不该访问的内存区域,从而绕过代码中现存的防止这种访问的检查时,这就是推测性执行(speculative-execution)漏洞了。每当发现 CPU 预测错误时,预测执行的影响就应该被消除,但在各种硬件 cache 中还是留下了痕迹。恶意代码可以找到这些痕迹,并利用它们来窃取出数据,如果没有这些痕迹的话攻击者就无法获得这些数据。

然而,这些攻击无法对攻击时完全不能访问的内存起作用。这就是为什么内核页表隔离(kernel page-table isolation)对 Meltdown 可以有效果的原因;如果内核的内存在攻击者的代码可以运行时完全没有被 map (映射),那么它就不可能在预测执行时被泄露出去。只要保持内核的地址空间不被 map,就足以防止在用户空间运行时受到 Meltdown 漏洞影响。

相反,Spectre 攻击通常在系统运行于内核模式时来攻击系统,并且此时所有的内核内存都被 map 了,所以我们当前的 kernel page-table isolation 的实现方式并没有能够保护这种情况。但是,内核几乎不会需要访问它所拥有的所有地址空间,而且不访问才是常态。因此,只要能够更多地使用地址空间隔离,就能带来这些好处:ASI 可以将内核与它不需要使用的内存区域隔离开来,从而消除许多可能的攻击手段。

还有其他一些阻止 Spectre 攻击的方法,其中一些现在已经在内核中实现了,但是目前的 Spectre 缓解措施中大多都开销很大,而且并不完整。例如,在每次返回用户空间时把内存对应的 cache 内容 flush 掉就可以阻止许多种类的攻击,但这会导致运行时间增长非常多。管理员还必须禁用同步多线程(SMT, simultaneous multi-threading),这可能会严重损害性能,要么就需要承担可能来自兄弟 CPU 的攻击风险。如果 ASI 能够足够有效地阻止攻击,就可能可以去除掉目前的缓解措施,从而增加 CPU 性能表现。

更好的地址空间隔离,可以在虚拟化领域带来更好的优势。在 KVM 下运行的虚拟机经常需要返回到 host 内核来执行各种任务,但这些请求通常可以在不访问内核的大部分地址空间的情况下就处理完毕。由于虚拟机很可能在运行恶意代码,而且它们可能配置成了 mixed-tenant 系统(简单来说就是多个互不知晓的用户存在于同一个系统)运行方式,因此长期以来,人们一直非常关注如何防止来自这种场景的 Spectre 攻击。因此,不出意外,目前的 ASI patch 就是来源于云计算供应商(最初是甲骨文,现在是谷歌),并特别针对 KVM,尽管该机制的整体设计上更加通用,可以支持其他场景。

Sensitive and non-sensitive memory

这套 patch 的核心思想是一个新增的 "address-space-isolation classes"的概念,每个 class 都描述了一种特定的安全环境。unrestricted class 是针对可以完全访问整个地址空间的内核的。换句话说,也就是当前版本代码中内核的工作方式。restricted class 定义为 unrestricted class 的一个子集。存在于 restricted class 中的任何一个 page-table mapping 都跟 unrestricted class 中的同一个 mapping 是完全相同的,但 restricted class 中不包含非受限类中所 map 的许多敏感数据的 mapping。

采用了 ASI 运行的系统,在用户空间代码运行的时候,都是在一个 restricted class 中运行。例如,在把控制权交给在 guest 系统中运行的内核之前,都会进入 KVM 特有的 ASI class 里。kernel page-table isolation class(如果它存在的话,这个 patch set 描述了这种可能性,但没有包含相关代码实现)则是在返回到 host 系统的用户空间之前进入的。但 ASI 的一个重要功能是,如果不需要访问敏感数据,那么在内核中运行时也可以使用 restricted class。因此,内核也就可以不用离开 KVM ASI class 就能够处理许多与 KVM 有关的任务了。

对于存储在内存中的数据,在此 patch set 中定义了三个敏感级别:

  • "sensitive" 内存,永远不应该被泄露出内核。

  • "globally non-sensitive" 内存,如果泄露给当前运行的进程是无害的,但不能允许进一步泄露。

当使用了地址空间隔离时,敏感内存只会在内核运行时才被 map 上来,甚至只有在内核真正需要它时才被 map。globally non-sensitive memory 可以一直保持 mapped 状态。与其他两类不同,locally non-sensitive memory 对每个进程来说都是不一样的;它可以在当前进程运行时被映射,但不会被映射到其他任何一个进程的地址空间。

这几种内存分类适用于所有 restricted ASI class。如果有许多这种 class,它们都会以同样的方式来限制内核的地址空间。换句话说,在一个 restricted ASI class 中判定为敏感信息(sensitive)的内存,在所有的 ASI class 中都是敏感信息。ASI class 之间的区别在于有多少用户空间被 map,以及当内核进入或退出其中一个 class 时,会运行一组钩子函数。例如,进入 KVM class 时,需要 flush 内存 cache,以避免可能在虚拟机中运行的任何一个 Spectre 攻击。如果当前的 kernel page-table isolation 机制在这个方案中被实现,那么在进入这个 class 时就不需要进行 cache flush 了,因为只要从内核中删除相关地址空间映射就足够了。

这组 patch 中有一个有趣的决定:如果内核在 restricted ASI class 下运行时试图访问 sensitive 数据,就会触发处理器的 trap。这时,一个可能的选择就是让 kernel oops,当然这肯定会阻止对该数据的预测执行攻击了。不过 ASI patch set 的做法是在这种情况下退出当前的 ASI class,进入 unrestricted 模式,允许继续访问。这种做法应该足以阻止对该数据的预测执行访问了(因为预测执行会直接停止,而不是处罚 trap),同时,也不会妨碍合法的内核访问。

采取这种方法的一个原因应该是相当清楚的:内核的地址空间是巨大的,没有人指望可以正确地确定内核使用的每一个数据结构的敏感性,并对其进行正确的标记。除此之外,必须有人找到内核里面在处理敏感数据之前必须需要退出 restricted class 的所有代码。哪怕有人真做到了,也没人指望能永远保持下去。因此,这个架构体现了一种承认现实的态度,现实就是永远不可能正确标记所有的数据以及所有必须使用敏感数据的位置,所以无论如何,总是需要让工作能正常进行下去的。

Classifying memory

然而,至少必须要把最敏感和最频繁访问的数据都正确标记出来。这个 patch set 有 47 个 patch 组成,其中大部分内容都集中在这项任务上。对于动态分配的内存来说,有一组新增的 GFP 标志(__GFP_GLOBAL_NONSENSITIVE 和__GFP_LOCAL_NONSENSITIVE)用于标记不包含敏感内容的那些内存分配动作。调用 page 或 slab allocator 进行分配时可以使用这些 flag,就能将得到的内存归入所需的 class;用 vmalloc() 分配的内存也可以用这种方式归到正确的 class 里。

在 kernel 内部,内核为自己的地址空间维护了两套 page-table。其中 unrestricted table 跟当前内核中使用的 page-table 是相同的,其中包括了 "direct map",也就是使所有的物理内存在内核的地址空间中都可用。restricted page table 在最开始时几乎不包含任何 mapping。每次进行 globally non-sensitive 内存分配时,就会把分配出来的 page 相关的 mapping 复制到第二套 page table 中的相同地址上。当在 restricted 模式下运行时,第二套 page table 就会被激活来取代第一套 page table,提供对 non-sensitive 数据的访问权限,但不会可以访问任何 sensitive 数据。

处理 locally non-sensitive 的内存分配则比较棘手。当内核在 restricted 模式下运行时,direct map 中只有全局 non-sensitive 这部分分实际存在于当前 page table 中。因为这些数据是 globally non-sensitive 的,所以有唯一一个 restricted table 会用来给所有进程。但是,locally non-sensitive mapping 对每个进程来说必须是唯一的,所以它们不能存放在这个个唯一的、global page table 里面。这就产生了一个问题,即在地址空间中,这些 mapping 应该放在哪里。

我们选择的解决方案是把 direct map 重新复制一份,所以现在内核对所有的物理内存会有两个完整映射。在 restricted 模式下,第一个映射中包含了 globally non-sensitiive 的数据,就像以前一样。在分配 locally sensitive 数据空间时,则使用第二个 mapping 来设置。再次提醒,只有那些被特地指定了是作为 locally non-sensitiive 来分配的 page 才在这个 restricted 版本的 mapping 中进行映射,并且每个进程都有自己专有的一套 mapping。因此,正在运行的进程的 locally non-sensitiive 数据哪怕在 restricted 模式下也是能访问的,但它不能被其他任何进程访问。

这就解决了这个问题,但代价是内核可以管理的物理内存数量就减少了一半,因为每个物理 page 现在都必须被映射两次。

对于静态变量会有一组单独的 flag,名称就类似于 __asi_not_sensitive 和__asi_not_sensitive_readmostly,可以把这些 flag 添加到变量声明中。人们很容易可以理解,不能将静态变量声明为 locally non-sensitiive。static per-cpu 变量又带来了一些麻烦,需要在用来声明的 macro 里面添加上一些简洁明了的描述,比如 DEFINE_PER_CPU_SHARED_ALIGNED_ASI_NOT_SENSITIVE()。

开发者并没有试图去把每一个分配和声明都标记准确。如上所述,这不是一个正常人会去尝试的做法,哪怕是内核开发者也不会。但是内核开发者们已经花了一些时间来尝试这组 patch set,并记录了有哪些访问触发了 trap 并迫使系统退回到 unrestricted 模式中。通过确认并标记出那些最繁忙的、nonsensitive 数据结构,从而能够减少这个新增机制对整体性能的影响。

Isolating KVM

有了所有这些基础设施之后,再添加一个机制来允许把用户空间的内存可空地映射到 restricted 环境,就有可能专门为 KVM 设置 address-space isolation,从而可以关闭一些开销巨大的 Spectre 的保护措施。每当内核将控制权交给 guest 系统时,它就会确保 KVM ASI 模式被启用;在返回内核时,可能会退出到 unrestricted 模式,也可能不会,这取决于内核打算要做什么。如果这次可以避免退出到 unrestricted 模式的话(因为不需要访问 sensitive 数据),那么就可以避免 cache flush 带来的开销了。同时一般来说,即使是恶意的 guest 系统也无法利用 host 内核的 Spectre 漏洞了。

不过,为了把这种保护做完整,内核必须保护好自己,不仅要防备 guest,还要防备运行在 SMT 兄弟 CPU 之上的系统中的恶意进程。在完整实现中,如果要避免简单地禁用 SMT 所引入的代价,那就意味着当内核在不受限制的地址空间中运行时,需要 "stunning (击晕)" 兄弟 CPU,也就是暂停其执行。当 kernel 返回到 KVM 时,兄弟 CPU 必须要被恢复("unstunned")出来。内核还必须要 flush memory cache 来避免数据泄漏。这里提到的把兄弟 CPU 打晕的这种精彩做法并没有包含在当前 patch set 中。跟现实世界中的同胞相残的事件一样,必须要先考虑清楚这会带来的后果。目前还没有完成这个 stunning 动作以及调度器之间的互动的 review,所以这项工作还没有被发布。

上面这些内容仅是对一种新形式的 ASI 的初步了解,它可以提高使用 KVM 的 guest 系统的安全性和性能,而且还可以扩展到其他任何可能需要 ASI 的情况。截至目前,这组 patch set 还没有收到任何 review,这可能是由于整个工作的规模很大、很复杂造成的。它确实实现了很有意思的权衡:它可以提高性能和安全性,但代价是大量的代码改动,以及今后需要持续维护这些 sensitivity annotation 标注。人们可能希望在不久之后,硬件将不再包含 Spectre 漏洞,那时这些新带来的成本就可能不太必要了。相反,如果我们相信 Spectre 可能会在很长一段时间内继续困扰我们,那么这个做法的优势就很明显了。

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

46f0456e606b99eaf39c01455542a2bc.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值