KPTI：内核页表隔离的当前的发展 
============================================================

在十月底的时候，[KAISER][8] 补丁集被披露了；它做了一项工作，将内核空间与用户空间使用的<ruby>页表<rt>page tables</rt></ruby>进行了隔离，以解决 x86 处理器上向攻击者透露内核布局的安全漏洞。这些补丁是自它们被公布以来，这一星期中最值关注的事情，但是，它们似乎正在接近最终的状态。这应该是再次审视它们的合适机会。

这项工作被重命名为 “<ruby>内核页表隔离<rt>kernel page-table isolation</rt></ruby>” （KPTI），但是目的是一样的：分割页表，将现在被用户空间和内核空间共享使用的这张表分成两套，内核空间和用户空间各自使用一个。这对内核的内存管理产生了根本性的变化，并且，这也是这些年来为此问题进行争论的人所希望看到的，尤其是考虑到它的性能影响的时候。不过，KPTI 仍然处于快速发展的轨道上。[一组预备补丁][2] 已被被合并到 4.15 - rc4 之后的主版本线上了 — 一般情况下仅重要的修复才被允许这样做 — 并且其余的似乎被确定进入 4.16 版的合并窗口中。许多内核开发者都在这项工作上投入了大量的时间，并且 Linus Torvalds [要求][3] 将这项工作回迁到长期稳定内核中。

也就是说，KPTI 已经在最后期限的压力下安全补丁的所有标记都已经就绪了。对于任何基于 ARM 的读者，在这里值的注意的是，在这项工作中有一个 [为 arm64 的等效补丁集][4]。

### 51 个补丁乃至更多

在这篇文章中，x86 补丁系统在 [163 版本][5]。它包含 51 个补丁，因此，我们应该感谢那些没有公开的版本。最初的补丁集，由 Dave Hansen 发布，由 Thomas Gleixner、Peter Zijlstra、Andy Lutomirski、和 Hugh Dickins 根据许多其它人的建议，做了大量的修订。任何还存在于这项工作中的错误都不会是由于代码方面缺乏经验导致的。

在现代系统中，页表是以一个树形结构进行组织的，这样可以高效地存储稀疏内存映射和支持巨页特性；查看[这篇 2005 年的文章][6] 了解更多细节以及它是怎么工作的示意图。在一个有四级页面表的系统上（目前的大多数大型系统都是这样），顶级是页面全局目录（PGD）。紧接着是页面上层目录（PUD）、页面中层目录（PMD）和页面表条目（PTE）。有五级页面表的系统是在 PGD 下面插入了一层（称为 P4D）。

页面故障解析通常遍历整个树去查找所需的 PTE，但是，巨页可以被更高层级的特定条目所表示。例如，一个 2MB 的内存<ruby>块<rt>chunk</rt></ruby>既可以由 PMD 层级的一个单个的巨页条目表示，也可以由一个单页 PTE 条目的完整页面表示。

在当前的内核中，每个处理器有一个单个的 PGD；在 KPTI 系列补丁中所采取的第一步的其中一个措施是，去创建一个第二个 PGD。当内核运行时，原来的仍然在使用；它映射所有的地址空间。当处理器运行在用户空间时，（在打完该系列补丁之后）第二个被激活。它指向属于该进程的页面的相同目录层次，但是，描述内核空间（位于虚拟地址空间的顶端）的部分通常都不在这里。

页表条目包含权限描述位，它记录了内存该如何被访问；不用说都知道，这些位是用来设置阻止用户空间访问内核页面的，即便是通过那些被映射到该地址空间的页面访问。不幸的是，一些硬件级的错误允许用户空间的攻击者去确定一个给定的内核空间地址是否被映射，而不管那个页面上映射的地址是否被允许访问。而那个信息可以被用于击败内核地址空间布局随机化，可以使一个本地的攻击者更易于得逞。在 KPTI 背后的核心思想是，切换到一个没有内核空间映射的 PGD，将会使基于这个漏洞的攻击失效，而到现在为止，我们还没有看到这些攻击。

### 细节

这个想法很简单，但是，就像经常发生的那样，有很多麻烦的细节，这个简单的想法变成了一个 51 个部分的系列补丁。最初的问题是，如果处理器在用户模式运行时响应一个硬件中断，处理中断需要的内核代码将在地址空间中不存在。因此，必须有足够的内核代码映射在用户模式中，以能够切换回到内核 PGD，使剩余的代码也可用。对于 traps、非屏蔽中断、和系统调用，也存在一个相似的情况。这个代码很小而且可以与其它部分隔离，但是，在处理安全且有效地切换时，涉及到一些很复杂的细节。

另一个难题来自 x86 本地描述符表（LDT）的构成，它可以被用于去改变用户空间的内存布局。它可以使用鲜为人知的 [`modify_ldt()`][7] 系统调用来做微调。例如，在 Linux 上早期的 POSIX 线程实现，使用了 LDT 去创建一个本地线程存储区域。在现在的 Linux 系统上，LDT 几乎不再使用了，但是，一些应用程序（比如，Wine）仍然需要它。当它被使用时，LDT 必须能够被用户空间和内核空间都可以访问到，但是，它必须一直处于内核空间中。KPTI 补丁集清理内核附近的内存，在 PGD 级别上为 LDT 保留一个完全的条目；因此，`vmalloc()` 调用的可用空间收缩到仅有 12,800TB。那是一个非常巨大的 LDT 空间数，可以满足有很多 CPU 的系统需要。这种变化的其中一个结果是，LDT 的位置是固定的，并且已知道用户空间 — 一个潜在的问题是，因此可以通过覆写 LDT 来很容易地破坏整个系统。在这个系列的最终的补丁是映射为只读 LDT，以阻止此类攻击。

另一个潜在的漏洞是，如果内核一直可以被操纵返回到用户空间而无需切换到已净化的 PGD。因为内核空间 PGD 也映射用户空间内存，这种疏忽可能被忽视一段时间。对此的响应是将用户空间的虚拟地址部分映射为内核 PGD 的非可执行部分。用户空间使用了一个错误的页面表来开始运行，将会立即崩溃。

最后，虽然所有已存在的 x86 处理器似乎都会受到已经披露的漏洞信息的影响，但是，以后的处理器可能不会受此影响。KPTI 有一个可测量的运行时成本，估计在 5%。这个成本不应该由用户去承担，尤其是他们拿到了没有这个问题的新处理器时。将会有一个 `nopti` 内核命令行选项，可以在机器引导时去禁用它。这个补丁系列也增加了一个新的“特性”标识（`X86_BUG_CPU_INSECURE`）去标识有漏洞的 CPU；它被设置现在的所有 x86 CPU 上，但是在以后的硬件上可能没有。如果没有该特性标识，页面隔离将自动被关闭。

在 4.16 版的合并窗口打开之前剩下将近一个月。在这段时间里，KPTI 补丁集毫无疑问将通过一些更多的修改来解决一些必然的小问题。一旦稳定之后，这些代码将被合并，并且将会尽快回迁到稳定内核。显然，我们期待一个缓慢更新的 — 但是更安全的 — 内核，作为我们的新年礼物。

--------------------------------------------------------------------------------

via: https://lwn.net/SubscriberLink/741878/eb6c9d3913d7cb2b/

作者：[Jonathan Corbet][a]
译者：[qhwdw](https://github.com/qhwdw)
校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出

[a]:https://lwn.net/SubscriberLink/741878/eb6c9d3913d7cb2b/
[1]:https://lwn.net/Promo/slink-trial2-2/claim
[2]:https://git.kernel.org/linus/64a48099b3b31568ac45716b7fafcb74a0c2fcfe
[3]:https://lwn.net/Articles/741882/
[4]:https://lwn.net/Articles/740393/
[5]:https://lwn.net/Articles/741883/
[6]:https://lwn.net/Articles/117749/
[7]:http://man7.org/linux/man-pages/man2/modify_ldt.2.html
[8]:https://lwn.net/Articles/738975/


