https://lwn.net/Articles/728675/

@石祤

提高系统安全性通常意味着对编程便捷性和性能代价妥协。令安全开发者沮丧的是他们经常发现对这些代价容忍度是很低的。而防止引用计数溢出这个特性就遇到了这个问题,这也导致了它迟迟没有被广泛接受。现在,防止引用计数溢出特性的性能问题已经找到了一种解决方案,这清除了它推广到整个内核代码的障碍。

引用计数溢出通常出现在编程错误。比如说,代码在增加引用计数后,经常会在处理错误路径中忘了去减小计数。攻击者可以利用这种错误重复去增加一个引用计数,直到该引用计数溢出,此时该对象被认为是未使用的而被释放,但实际上仍在使用。这产生的“释放后使用”(use-afer-free)行为是可以被利用,从而造成危害系统的行为。

防止计数溢出的保护代码已经在内核中存在已久。从 PaX/grsecurity 补丁集就开始了,但一开始的手段是基于使用 atomic_t 类型,现在已经变化了。下一步打算引入新的类型 refcount_t,保护代码也会添加到这个类型中去。这个类型在内核 4.11 的合并窗口中被加入,许多内核子系统已经修改相关代码去使用这个类型,但是许多网络子系统的开发者不愿意使用它,因为会引发性能损失。

网络层提交的补丁经常会遇到性能问题,但上述性能问题不仅仅出现在网络层。Andrew Morton 最近在进程间通信子系统迁移到 refcount_t 类型的过程中抱怨 —— 他认为在“简单,安全,老,严格测试的代码”中添加安全检查而造成运行缓慢,这是没有意义的。现在会出现这样的问题:即使计数溢出保护代码在内核中被使用了,它们也会被发行人员因性能问题而关闭掉

系统安全开发的一个真理就是,关闭(或者是从未实现)保护措施将根本无法抵御攻击者。另一个真理是,“安全,老,严格测试的代码”可能仅仅只是“老的代码”,正如 Ingo Molnar 指出的:

当代码在基于_现存的,合理的参数_使用时,它们是没有问题的。但是一旦有人用一个不合理、没人使用过的参数发现一个十年前的老问题时,它们就会基于这个问题开发漏洞

想要真正保护内核防止引用计数溢出,需要让检查变得普遍。也就是说,要么说服开发者接受检查带来的性能损耗,要么找到一种方法将性能损耗降低到一种可以接受的程度。显然,如果损耗能被降低,后者是一种最少阻力的实现路径。

在 Kees Cook 的“快速引用计数保护”补丁集中,他发现了一种确实有效的解决方案。补丁通过一个指令操作现存的(高度优化)atomic_t 实现,该指令会捕捉引用计数变成负数(也刚好是计数器溢出)的场景。这个指令非常容易通过处理器的分支预测逻辑来正确地猜测下一步,所以它执行性能非常好,如补丁集附带的微基准结果显示,标准的 atomic_t 实现使用了 822.49 亿个指令周期;新的 refcount_t 代码使用了 822.11 亿个指令周期,除去边缘误差两者是一致的。而作为对比,原来的 refcount_t 实现则需要使用 1448 亿个指令周期来运行测试。

目前,该补丁只适用与x86架构。当其他人开始着手做相关架构的移植时,只要将各自架构的代码进行组装即可实现。在主要的非x86架构的移植中,该工作也没有明显的技术障碍

相对原先完整的 refcount_t 实现,这个变化有一个不再检查从0增加1情况的代价。一个对象的引用数当降低到0时,这个对象一般会被释放;随后一个计数增加操作意味着这个将被释放的引用对象仍在被使用。很明显,这是一个有必要捕捉的场景,但是没有人能在不增加任何额外检查代码的情况下解决该问题。Cook 在这个补丁中声明,在新的 refcount_t 中能在大多数场景中捕捉到溢出行为,并引用了两个漏洞(CVE-2014-2851 和 CVE-2016-0728),即使有检查代码仍没有阻止漏洞执行。

仍有许多开发者对 refcount_t 类型保持着冷漠的态度;比如说来自 Eric Biederman 的抱怨 (以及 Cook 的回应)。剩下的反对意见主要基于几个论点:(1)refcount_t 并不能修复所有引用技术相关的问题 (2)使用这个类型暗示着这些代码可能存在问题,而着使一些开发者自尊心受到损害。但是,随着性能问题被解决,其他抱怨将不会阻碍引用计数强化内核的进程。这对关心安全的人来说是最好的消息。