在计算机世界里,提到安全,通常人们会首先想到病毒,蠕虫,以及各种网络攻击,在这个领域里,Unix 系统被证明更能抵御这种攻击。但另一方面,Unix 系统却也很容易遭受来自内部用户的攻击,fork 炸弹 (fork bomb) 就是其中的一种 (比较自虐的一种)。
fork 炸弹的原理很简单,某个进程通过恶意的 (或者因为编程失误) 疯狂调用 fork() 在短时间内生成了大量的进程,从而将操作系统的进程表填满,在这种情况下,系统将无法生成任何新的进程,除非某个进程退出。当然即使有进程退出,空出来的名额也很可能被 fork 炸弹的子进程所占据。此外,系统的 CPU 与内存等资源也被 fork 炸弹的各个进程所耗尽,事实上整个系统将因此变得巨慢无比而无法响应。
一个经典的 fork 炸弹示例是如下这样一行简洁但晦涩的 Shell 代码:
: () { : | : & }; :理解这行代码的关键是其中的冒号,它在这里并没有什么特殊含义,只是一个函数名称标识符,将冒号换个名字会更容易理解,比如:
forkbomb () { forkbomb | forkbomb & }; forkbomb可以看出这行代码一旦运行起来,将按指数级速度不停生成新进程,在我的 Debian 测试机器上,运行这行代码瞬间就能让系统毫无响应。
fork 炸弹的问题在于,当它运行起来以后,你几乎无法铲除它。杀死 fork 炸弹进程似乎是唯一的选择,但通过手工 kill 来杀死一个个 fork 炸弹进程是不现实的,即使为此专门写一个程序,这个程序也很难抓住最初的父进程,或者跟上后续生成的进程,因为该程序运行的时候,这些父进程已经退出,而子进程将被 init 接管。此外,运行这个专门的程序意味着启动一个新进程,在 fork 炸弹已启动的情况下,这几乎无法实现。因此不难想像,拆除 fork 炸弹的最有效办法就是拔下电源线。
也因此通常针对 fork 炸弹的办法都是提前阻止它,简单说就是通过调用 ulimit -u 或者配置 /etc/security/limits.conf 来限制用户所能生成的最大进程数,当进程数达到这个最大值后,任何新的生成进程的尝试都将失败。就上面所列举的 Shell 脚本例子来说,因为该脚本只是简单的 fork 进程,并且 fork 完后就退出,在遇到最大进程数后,这些 fork 炸弹进程将无法完成 fork 而退出,最终系统会回复正常。比如我的 Debian 测试机器的非 root 用户的 ulimit -u 值默认是 2047,运行 fork 炸弹后,系统有很长一段时间无法响应,但随着 fork 炸弹进程填满最大值后,系统就逐渐回复了。这个方法面临的一个问题是如何设置一个恰到好处的最大值,使得即不影响用户的正常使用,同时又能在几个恶意用户同时引爆 fork 炸弹时,还能给管理员留下一定的响应时间来完成一些处理工作。
LWN 的编辑 Jonathan Corbet 先生最近写了一篇文章,介绍了一种新的可能的拆除 fork 炸弹的方法。这个方法是由 Kernel 开发人员 Hiroyuki Kamezawa 提出来的。Kamezawa 的 patch 在 Kernel 里添加了一个新的进程跟踪结构,该结构是一个简单的树结构,记录了系统中的进程谱系。该结构与已存在的进程数据结构主要的区别在于即使某个进程退出了,该进程的相关信息仍然存在于该树中,因此 Kernel 可以据此判断系统是不是正在遭受 fork 炸弹的袭击。
一个直观的感受是该方法将使 Kernel 增加额外的存储开销,因此何时将旧的无用的进程信息清掉是关键的一点,该 patch 默认每30秒 Kernel 将进行一次检测,以判断当前是否正在遭受 fork 炸弹攻击。如果没有检测到攻击,那么已经存在了30秒以上的历史记录将被清除。
那么 Kernel 将如何判断系统正在遭受 fork 炸弹攻击呢,通常 fork 炸弹会耗尽系统的内存,因此该代码检测内存的使用情况,如果自上一次检测后,有内存分配失败发生或 kswapd 有运行,那么这将是一个标志。此外该代码还检测系统中的进程数是否自上次检测后增加了。如果所有这些检测都显示不出有什么异常,Kernel 将清除旧的历史数据。如果显示系统有内存分配困难,或者进程数持续增加,历史数据将保存下来。
当系统遭受 fork 炸弹攻击而导致内存不足时,Kernel 首先会调用 OOM killer 来尝试找出有问题的进程,但在这种情况下,OOM killer 很难意识到这不是一个进程,而是一整棵 fork 炸弹进程树。于是 OOM killer 将调用一个新的 fork bomb killer,fork bomb killer 将以深度优先的方式遍历该进程历史树,并尝试算出树中每个节点的后代子进程数,以及所有后代子进程们所使用的内存。然后计算出得分最高的进程,如果该进程叉下有10个以上的子进程,那么该进程将被认为是 fork 炸弹进程,该进程以及所有的子进程及其后代都将被杀死,于是 fork 炸弹拆除了!
Kamezawa 的 patch 还通过 /sys/kernel/mm/oom 提供了相应的用户接口。比如只有当 mm_tracking_enabled 设置为 “enabled” (默认值) 时,该功能才启用,mm_tracking_reset_interval_msecs 则用来控制多长时间清一次进程历史树。不过一个很重要的值却没有提供接口,即判断 fork 炸弹的进程数目,该值在代码中写死为10,这个数令人担心有点小。
该 patch 看起来似乎不错,并且没有多大副作用,不过在 linux-mm 的邮件列表里并不太受欢迎,评论者除了担心可能增加的 Kernel 运行时开销外,还认为 fork 炸弹问题通过用户空间的工具来解决会更好。Kamezawa 似乎也就此放弃了进一步去推动该 patch 进入 Kernel 的努力,他表示,”To go to other buildings to press reset-button is good for my health”。
引用与致谢
2011/5/7 Kin Leung <ballkid...@gmail.com>:
> LWN - Fighting fork bomb (作者 Jonathan Corbet)
> Wikipeida - Fork bomb (作者不详)
>
> 转自:开源小厨
> http://fosschef.com/2011/04/translation-of-fighting-fork-bomb/
>
> --
> 您收到此邮件是因为您订阅了 Google 网上论坛的"广州 GNU/Linux 用户组"论坛。
> 要向此网上论坛发帖,请发送电子邮件至 gz...@googlegroups.com。
> 要取消订阅此网上论坛,请发送电子邮件至 gzlug+un...@googlegroups.com。
> 若有更多问题,请通过 http://groups.google.com/group/gzlug?hl=zh-CN 访问此网上论坛。
>
--