Generalizing address-space isolation
By Jonathan CorbetNovember 5, 2019
OSS EU
原文来自:https://lwn.net/Articles/803823/
Linux系统一直以来都是让user space和kernel space运行在同一个地址空间(a single address space)。在Meltdown漏洞爆发之后,大家期望改变这种做法,因此在2017年底合入了kernel page-table isolation (KPTI)。不过Mike Rapoport在2019 Open Source Summit Europe上提醒大家,这并不能完全解决地址空间隔离问题。他介绍了一个例子需要增强地址空间的隔离性,不过看起来需要对kernel的内存管理部分做很多改动才能实现。
目前来说,Linux系统仍然是运行在同一套地址空间内的,至少在kernel mode是如此。让这些数据对所有人可见,能够更加高效和方便,不过如果能把地址空间隔离开的话,就能更加改善系统安全架构,毕竟没有映射的内存内容对于攻击者来说会非常难以窃取。这个方向的第一步动作就是KPTI。不过这个方案性能表现不好,尤其是在user space和kernel space之间切换的时候。鉴于当时没有任何其他方案能解决Meltdown问题,因此还是合入了。对很多人来说,他们勉为其难接受了KPTI,不愿意做更多保护,不过Rapoport仍在努力扩展它的应用范围。
Beyond KPTI
近来他一直希望能利用system-call address-space isolation(系统调用地址空间隔离)来进一步增强安全性,这个方案会对系统调用实现一套访问受限的地址空间。每当有系统调用发生的时候,kernel里大多数代码和数据段起初都是未进行映射的,任何对这些地址的访问都会触发page fault(缺页异常),然后kernel会判断这个访问是否安全,如果检查通过,则对这块地址进行映射,否则的话调用系统调用的进程会被直接杀死(killed)。
这种保护会有一些应用场景,不过最直接的目的是为了组织return-oriented programming (ROP,针对返回时编程)攻击。如果某次跳转指令的目标地址是kernel的一个已知symbol(这里指函数或者变量),那么这个跳转可以被判定为安全的,允许继续进行。包含这个地址的page页面在系统调用的执行过程中会映射上来。ROP攻击通常是通过跳转到一些不属于某个已知的kernel symbol的地址来进行的,大多数这种攻击都可以用这个新的安全机制阻止掉。如果ROP攻击碰巧跳转到了有意义的地址从而拿到了对这个page的访问权限,它也仅仅是拿到了kernel text段的一小部分而已,在当前kernel里,ROP能访问全部kernel text段内容。
不过这些patch尚未合入,原因有很多。原因之一是因为其实没有人知道如何判断某个数据访问是否安全,上面介绍的基于已知kernel symbol的检查方法,不适用于数据访问。系统调用如果使用了不合理的参数,那么还是有可能会让kernel对很多代码page进行映射,从而攻击者仍然能进行ROP攻击。并且这个方案会明显降低代码执行效率,一般很难被接受合入mainline。
之前的L1TF和MDS预测执行漏洞也带来不少挑战。这些漏洞会导致host system可以被guest OS攻击,这样对云服务提供商来说非常可怕。相应的防御手段,目前来说,只能关闭hyperthreading,不过这样就会让性能降低很多。Rapoport认为可以有个更好的方案,就是另一种形式的地址空间隔离:可以在把系统控制权交给guest OS的时候,每次都确保使用一个特殊的内核映射(kernel mapping)。这种“KVM isolation”机制是由Alexandre Chartre在5月份提出的,不过一直没能合入。
还有其他一些地址空间隔离的想法在不断涌现出来。其中一个是确保kernel里的某些特殊数据只映射给那些需要访问它的进程。这可以通过在kernel page table里面建立一段私有区域来实现。kernel代码能利用一些新增的函数例如kmalloc_proclocal()来从这段区域分配内存。为了更好的隔离,这种方式分配的内存会从kernel的"direct map"区域(就是系统所有物理内存的线性映射的地址空间)拿掉。不过把某些page从direct map里拿掉的话,可能会有一些性能问题,因为这需要把huge page(巨页)分割成更小的page。
此外,还有一些用户特有的mapping映射,这是指只有拥有者进程才可见的user-space mapping。可以用来在内存里存储一些不希望外界能访问到的秘密信息(例如秘钥)。不过,同样地,这块内存也需要从direct map里面取消映射,并且这段空间不能被swap出去。最近有组MAP_EXCLUSIVE patch set就实现了这样的功能。
最终,Rapoport也提到了namespace-based isolation(基于命名空间的隔离):让一些kernel memory跟某个指定的命名空间来绑定在一起,只能由运行于此命名空间内的进程才可以访问这块kernel memory。不过这个方案在处理network namespace(网络命名空间)的时候有点麻烦。代表数据包的sk_buff结构很显然应该用来做隔离,不过它们通常都是会跨多个命名空间的边界的。
Generalizing address-space isolation
虽然上面介绍的各种地址空间隔离机制都各不相同,其实还是有一些共同点的。他们都是希望能在现有内存中建立一块受限制的地址区间,然后只有满足某些具体执行上下文条件的时候才让这块空间可见。因此Rapoport希望能实现一套通用kernel API来供这些地址空间隔离机制使用。不过这会需要不少重要改动。
目前kernel的内存管理代码假设跟某个进程关联的mm_struct结构是跟这个进程的page table相同,不过我们需要打破这种关联。需要新建一个pg_table结构,来表示page table,还会有一个相关的API来操作这些page page。这里会有一个难题,得要创建一个机制来安全地释放kernel page table。
创建这种受限上下文(restricted context)其实会是比较简单的。例如KPTI等已经在系统启动的时候建立好了,还有其他一些会在合适的时候建立,例如:进程创建时,关联到命名空间的时候,等等。这些上下文切换代码需要能在受限地址空间之间切换,不过切换kernel的page table需要非常小心。也要有代码来释放这些受限地址空间,并且注意要避免导致问题,例如万一释放了main kernel page table会怎么处理。
当这个架构完成之后,kernel就可以支持私有内存分配了。例如alloc_page()和kmalloc()这样的函数就需要能知晓这块分配内存所对应的上下文。会有一个新增的__GFP_EXCLUSIVE标志来申请针对某个受限上下文进行分配。再次提醒,这些分配的page都需要从kernel的direct mapping里面拿掉(并且释放时需要加回去)。还要额外注意那些需要跨越上下文边界(context boundary)的对象。
最后,还需要改善slab cache。memory controller使用的caching形式已经带有了一些必须要的机制了。Slab memory释放时的上下文通常跟分配时的上下文不同,这样实现时需要注意这些陷阱。
Rapoport总结道,地址空间隔离方案值得更多调查,它能够让kernel暴露在外的受攻击面变得更小,甚至有些硬件bug导致的问题也可以用它来弥补。通过这种方式获得的安全性提升是否能和实现时带来的复杂性相比,孰轻孰重还需要在改善这组patch的过程中继续评估。很希望后续能在mailing list上看到更多相关的patch。
[Thanks to the Linux Foundation, LWN's travel sponsor, for supporting your editor's travel to the event.]
全文完
LWN文章遵循CC BY-SA 4.0许可协议。
长按下面二维码关注:Linux News搬运工,希望每周的深度文章以及开源社区的各种新近言论,能够让大家满意~