https://lwn.net/Articles/741878/ @文侑

最近安全领域两个很火的词 meltdown 和 spectre,相信很多读者都已经很熟悉了,一部分的原因就是用户空间通过 prefetch 间接 touch 到内核地址空间。本文介绍的 KPTI - “kernel page-table isolation” (从 KAISER 重命名过来),其实也是弱化这种攻击的一种手段。文章假设读者对内存管理单元 (PMU) 以及 linux 内核的实现有一定的了解,对进程地址空间也有一定的理解。译者也不一定按照原文来翻译,会更加强调重点,以及增加译者自己的见解,也不一定合理和完全正确,请广大读者参考。

KPTI 目前仍然在快速迭代中,4.15-rc4, 4.16 … ,具体的进展请读者自行关注邮件列表。KPTI 的核心思想是把进程的用户地址空间和内核地址空间分成两个页表(目前是统一为一个页表),那为啥以前统一为一个页表呢,这里面主要有两方面的原因:

  1. 设计假设是 PMU (乃至 CPU) 是可信的

  2. 性能 (一个页表显然一个明显的好处是 TLB cache miss 会少很多)

如果每个进程地址空间分成内核和用户两个部分(两个页表),意味着 prefetch 只会访问到用户地址空间,从而从某种程度避免内核地址空间的 footprint,不光是这样,KPTI 的设计假设是 PMU 不可信,从而规避由此带来的潜在的一系列安全问题。但是这样做带来的副作用很明显,在切换到内核地址空间的时候需要切换页表, 从而带来 TLB flush,在这种情况下是有性能损耗的,而且这个 overhead 对于很多实际业务来说未必是可以忽略的,而且业务模型不同,这个影响也会有区别 (文中提到大约 5%,译者认为真的需要 case by case)。另外,虽然解决了上面提到的安全问题,文中还提到可能还存在几个安全隐患,例如:切换到内核地址空间的代码本身还是有可能暴露给用户地址空间,比如:中断,陷阱,NMI,系统调用等,另外可能有些应用需要用到 modify_ldt(),例如:wine 这样的东西,可能会引起 LDT 再次被篡改 (LDT 的支持也许完全可以裁掉)。KPTI 目前可以通过 nopti 内核启动参数和 X86_BUG_CPU_INSECURE 来显式关掉,后者针对不同的处理器系列进行区别化处理,类似一个白名单。

下面译者来说说自己的观点:

由于译者最近也在做一些用户态高性能组件的开发(bypass kernel),例如:RDMA/DPDK 等。这种场景下 KPTI 带来的安全效果会被放大,而带来的性能副作用会被减小,原因在于大部分能触发 KPTI 的双页表切换带来 TLB cache miss 的行为(比如:系统调用,中断等)会被大量减少,直接由用户态和硬件交互。所以译者认为,在这种情况下,KPTI 是可以发挥到安全和性能互相平衡的更佳效果,也是未来一个很重要的方向之一,把安全留给内核,把性能留给用户。