宋宝华:武汉肺炎启示录—墨菲定律与防御性编程

墨菲定律

开始的故事

1949年,美国空军一位名叫爱德华·墨菲的空军上尉工程师,对他的某位运气不太好的同事随口开了句玩笑:“如果一件事有可能被做坏,让他去做就一定会更坏。”由此衍生出墨菲定律(Murphy's Law),指“凡是可能出错的事就一定会出错”。

“墨菲定律”告诉我们:如果坏事情有可能发生,不管这种可能性有多小,它总会发生,并引起最大可能的损失。“墨菲定律”是软件工程领域防御性编程的最基础理论,而防御性编程是每个软件工程师必须遵守的规范。


当你认为没有错误的时候,错就一定会来找你。

——《中国机长》刘传健


武汉疾控中心副主任李刚2019年1月19日通报:“此次新型冠状病毒的传染力不强”。


219年(汉献帝建安二十四年),刘备经已稳定益州、汉中,荆州守将关羽见时机成熟,遂北伐曹操。关羽攻樊城,孙权则从背后偷袭荆州。大本营南郡失,关羽唯有退守麦城,突围时被擒杀。


以上的三个片段告诉我们,凡事疏忽大意,必然酿成极其严重的后果。反观川航3U8633事故中,机长刘传健在紧急情况下,对飞机的操作,出现任何一点差错,都将直接导致机毁人亡。但是中国机长刘传健史诗级的操控却成就了成为世界航空史上的一个奇迹。这与他时时谨记“墨菲定律”是分不开的。


任何时候都要做防御性编程,将bug在早期控制住,因为bug修复成本的对数log(cost),与修复它的时间阶段成比例

所以,bug越晚暴露,修复的成本越指数级上升。如果李文亮医生因为“造谣”被训诫的时候,我们就开始修复这个bug,其成本毫无疑问会比现在的成本指数级下降。


墨菲定律

bug的启示


早期的Android(2.2及以前的版本)有个著名的root提权漏洞,叫 “Rage Against The Cage(RAtC)”。

ADB在Android里面最初以root权限运行,之后它通过setuid(AID_SHELL)降权到shell用户,但是Android adb.c的代码忽略了对函数返回值的检查:


理论上,root用户要降权到shell普通用户,是没人能阻挡的。类似马云在阿里巴巴强行要干码农,刘强东在京东强行要做快递员。但是有一种情况下,马云做不了码农,比如阿里巴巴公司有章程,规定最多只能1万个码农。公司HR听说马云明天要宣布自己做码农,前一天晚上就招满了1万个码农,这样第二天马云在宣布自己是码农后,其实还是原来的身份。由于马云调用了setuid(码农)也没检查返回值,它这个时候觉得自己已经是个码农了。


在Linux系统中,一个用户的进程数量受到RLIMIT_NPROC的限制,不可能无限制的创建。所以rage againest the cage攻击的主要原理是不断fork出shell用户的进程,把pid的坑占完,导致setuid(AID_SHELL)失败:

相关代码:rageagainstthecage.c


该提权漏洞被用于各类root刷机,漏洞发现人Sebastian Krahmer公布的利用工具RageAgainstTheCage(rageagainstthecage-arm5.bin)被用于z4root等提权工具、Trojan.Android.Rootcager等恶意代码之中。


后续版本的Android对此bug进行了修正,非常简单直接:

这个事情对我们的启示是,过度"自信"的编码,一定是错误代码


2019年,我也“亲自”犯了一个这样的错误,即使在我已经非常熟悉“墨菲定律”的情况下。

Linux的Graphics compositor与client端进行通信的一种常见协议wayland,通过UNIX DOMAIN socket在client和compositor进行通信,通信的每条消息通常很短:

主要是一些window对应的surfaces(以及对应的buffers)的创建、撤销、自动等,消息通常非常短。经过我长达半年的观察,我没看到大于200个字节的消息。

后来我为了截获wayland的message并进行最终的协议dump分析,弄了char buf[256]这样的数据结构去缓存每一条消息,当时极度自信,因为半年的观察告诉我,wayland上面的每条消息不可能超过200个字节,因此这里256个字节应该是极度安全了。后来的事实是,软件很快就崩了。因为网上下载的一个软件,有个奇葩的标题(title),它的 title十分长。在wayland的shell协议里面,window的标题是会透过socket发给compositor的:

如果有个奇葩软件,其标题非常长,set_title后面的title字符串就可以撑破256个字节。


这件事情让我再次领略到“墨菲定律”的神奇正确性,以及编码不能太“自信”。否则,即使是白马斩颜良,襄樊擒于禁、杀庞德,千里走单骑的关羽大神,也可能兵败荆州。


墨菲定律

编程的指南


首先你一定要小心内存的越界,比如下面的代码:

把环境变量拷贝给str,风险就是tmp完全可能大于str的长度。比较安全的做法可能是:

上述代码,明确地告知了str的size,以及采用了strncpy保证了不越界。


我们从C语言各个版本memcpy函数的变迁也能看出防御性编程对C语言本身发展的影响:

在C11之后,memcpy()演化出了memcpy_s(),多了一个destsz参数:

destsz-max number of bytes to modify in the destination (typically the size of the destination object)

如果大家知道memcpy()这个函数引起的内存越界在历史上作过多少孽的话,就会明白memcpy_s()的这个改进有多么重大的意义。


防御性编程要求我们一定要进行代码的静态扫描(在代码运行前先进行体检),这方面最出名的工具就是coverity,内核里面有大量的补丁是修复coverity扫描出来的问题,比如这个:

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=594619497f3d6d4b8d8440e6d380e8da9dcc9eeb


防御性编程要求我们必须提供缺省的行为,比如一定要考虑我们没有考虑的分支,将代码本身形成闭环。譬如,下面的代码最后一定要有个else(无论你多么的自信,这个else里面的情况不可能出现):

防御性编程要求我们一定要检查函数的返回值,只要这个函数不是返回void,它就可能出错,这个我们在rage againest the cage部分已经阐述。比如,哪怕是fork()、pthread_create()、read()、write()这些常规函数,都是极容易出错的,你不能做最乐观的估计。


防御性编程要求我们一定要小心运算的越界,如Linux内核著名的Y2K38问题,时间变量会在2038年越界,但是如果2个时间变量平均呢?

avg_time = (time1 + time2)/2;

那么就会提前一般的时间越界,我在2009年曾经花费了2个星期找到并修复这个bug,但是总共只改了1行代码:



我国官方曾针对武汉八义士的事情发文:"但是,事实证明,尽管新型肺炎并不是SARS,但是信息发布者发布的内容,并非完全捏造。如果社会公众当时听信了这个'谣言',并且基于对SARS的恐慌而采取了佩戴口罩、严格消毒、避免再去野生动物市场等措施,这对我们今天更好地防控新型肺炎,可能是一件幸事。"这其实是最防御性编程效果的最佳注解。


当然,防御性编程不等于过度防御。比如下面的代码,不叫防御性编程,叫有病:

拖着箱子从30层楼的家出门去机场,出到家门口,确认下门关好没,反复拉门把手;走到电梯口,拖着箱子走回家门口,反复拉门把手确认门关好没;坐电梯到1楼,又坐电梯回到30楼,反复拉门把手确认门关好了没~~。这不是防御性编程,这是强迫症编程。



墨菲定律

正向的希望


墨菲定律强调各种可能性最终都会实现,实际它也包含了正向的可能性。科幻电影巅峰之作之一的《星级穿越》,女主角的名字就叫“墨菲”。《星级穿越》实际重新诠释了“墨菲定律”,让“墨菲定律”并不总是指代坏事,而是说只要是有可能的事,就一定会发生,这自然包括拯救地球。主角的名字暗示了她和她的父亲将最终联手拯救地球:


同样,李文亮医生要拯救的地球一定会得救,我们一定会从疫情中走出来。胜利一定属于英雄的中国人民。




Linux阅码场原创精华文章汇总

更多精彩,尽在"Linux阅码场",扫描下方二维码关注

点一点右下角”在看”,为阅码场打Call~