"谁是世界上最好的语言?"
这是程序员聚在一起必定会讨论的问题,特别是在大家还不熟,不好意思聊妹子、聊游戏的时候。Rust作为最近的后起之秀,在出生后的短短6年时间内就登上了“2021年05月编程语言排行榜”的第24位,可以说成绩斐然。作为C/C++的有力替代者,Rust其中一个最引人注目的特点就是安全性,今天我们尝试通过一些数据分析来看看Rust能否在安全上完虐C/C++。
我们从两个维度上的数据来分析语言的安全性:
1、语言强相关的缺陷类型的数量
2、语言强相关的缺陷类型的缺陷数量
前者衡量的是由于语言特性导致的缺陷引入,后者衡量的是真实存在的缺陷。前者不受程序员技能的影响,而后者会受教育、工具等因素的影响。
以C/C++中可以使用指针为例,由于这种机制天生就存在“引用空指针导致系统复位”的缺陷,这会使前者的统计值上升;但由于编码规范、Coverity等静态检查工具的应用,很少在开源代码中发现此类缺陷,这会体现在后者的统计值中。
前者的数据从CWE(Common Weakness Enumeration) List中获取,其中每一类缺陷都明确标注了该缺陷是语言无关的,还是和某些语言相关的;后者的数据从CVE(Common Vulnerabilities and Exposures)库中获取,可以大致看出每一类CWE在软件开发中出现的比例。由于Rust才出生不到10年,因此在CWE中并没有收录Rust语言相关的缺陷,因此我们将通过梳理C/C++的这两项数据,然后再结合国外研究团队对Rust项目的缺陷分析数据来判断Rust是否比C/C++更安全。
言归正传,我们先来先看CWE的数据,截止到目前CWE List V4.4中共收录了941个缺陷,特定语言相关占比如下:
在CWE收录的941个缺陷类型中语言相关的缺陷占19.02%,这意味着一旦选择了某种开发语言,这些缺陷或多或少都会出现在你的程序中,当然你选择不同的语言,潜在的缺陷类型数量是不一样的,下图展示了不同语言潜在的缺陷类型数量比较:
可以看到C、C++、Java三种语言位列前三,并且遥遥领先。
这里面固然有作为三大主流编程语言必然受到更多的关注,多方照顾的原因,但也不能忽视同样作为历史悠久的web开发语言PHP和广泛应用的语言python,两者的漏洞类型数量并不多,特别是python在5月荣登编程语言排行的第二位。另外在这里要注意一下,由于一种缺陷类型可能在多个语言中都存在,因此如果你采用了C和C++混合编程,漏洞类型的数量并不是85+81。
当然CWE的数据只能说明C/C++潜在缺陷类型较多,无法说明真实缺陷的情况,因此需要在分析一下CVE中的漏洞数据。
当然CVE中肯定不包含所有漏洞,但至少可以看到大致的分布情况,以下数据分析来自154102个CVE漏洞的数据集,然后对漏洞成因进行分析,并关联到C/C++相关的CWE,以此来观察真实漏洞存在的情况,下图是C/C++相关CWE关联的漏洞数量分布(取前十):
上图中对应的CWE ID的内容和在全154102个漏洞集中的占比,如下表:
排序 | CWE ID | CWE概述 | CWE解释 | 全漏洞集占比 | C/C++特有漏洞集占比 |
1 | 119 | Improper Restriction of Operations within the Bounds of a Memory Buffer | 读写了超出缓冲区边界的内存 | 7.74% | 47.75% |
2 | 787 | Out-of-bounds Write | 写越界 | 2.26% | 13.93% |
3 | 125 | Out-of-bounds Read | 读越界 | 2.11% | 13.04% |
4 | 416 | Use After Free | 使用了一段已经被释放的内存 | 1.3% | 8.04% |
5 | 476 | NULL Pointer Dereference | 指针使用前未判空,导致使用了一个空指针 | 0.87% | 5.37% |
6 | 362 | Concurrent Execution using Shared Resource with Improper Synchronization ('Race Condition') | 多线程对同一资源访问时由于未进行相关的保护而导致非预期的错误,比如读到脏数据。 | 0.54% | 3.34% |
7 | 120 | Buffer Copy without Checking Size of Input ('Classic Buffer Overflow') | 拷贝时由于未校验而是目标内存小于原内存而导致溢出 | 0.50% | 3.1% |
8 | 415 | Double Free | 对同一块内存进行了多次重复释放 | 0.18% | 1.09% |
9 | 134 | Use of Externally-Controlled Format String | 程序允许外部输入格式字符串,这使攻击者可以通过操作输入格式字符串获得系统内存内的敏感信息 | 0.16% | 0.99% |
10 | 704 | Incorrect Type Conversion or Cast | 类型转换错误 | 0.12% | 0.72% |
从上表中可以看到位列前三的漏洞都和内存操作有关系,前五加起来占到漏洞集的12.11%。
由于Rust语言的历史还不够长,因此在CWE中并没有相关缺陷类型,因此我们退一步,通过一些研究团队的报告来识别缺陷情况。
结合C/C++的缺陷类型集中在内存操作部分,因此选择了今年6月份ACM SIGPLAN国际会议上的一份报告。该研究对Rust实现的数个系统和库进行了调查研究,对缺陷进行了分类,具体的结果如下:
Name | Memory Bugs | Blocking Bugs | Non-blocking Bugs |
Servo | 14 | 13 | 18 |
TiKV | 5 | 0 | 2 |
parity-ethereum | 2 | 34 | 4 |
Redox | 1 | 4 | 3 |
Tock | 20 | 2 | 3 |
Rust Libs | 7 | 6 | 10 |
由于原始研究报告不是严格按照CWE分类,因此做了下二次分类并与上述C/C++的缺陷分类进行了对应,如下表:
项 | CWE:119/787/125 | CWE:476 | CWE:908 | CWE:763 | CWE:416 | CWE:415 | 总数 |
Buffer overflow | Null pointer dereferencing | Read uninitialized memory | Invalid free | Use after free | Double free | ||
数量 | 21 | 12 | 7 | 10 | 14 | 6 | 70 |
C/C++安全缺陷统计中的排位 | 包揽前3名 | 第5名 | 非语言相关CWE,未统计 | 非语言相关CWE,未统计 | 第4名 | 第8名 |
可以看到除了CWE-908和CWE-763由于与语言无关而未统计到数据外,在内存相关缺陷中Rust项目中发现的缺陷数量的排序与C/C++统计得到的缺陷排序完全一致。
看到这里可能有人会说Rust在安全性上和C/C++没什么区别嘛。
别急,我们不能忘记Rust提供了一些安全特性以避免类似安全问题的发生。因此下面我们将引入这些缺陷的代码所在分为两类:安全区域、非安全区域,再看一下数据情况:
项 | Buffer overflow | Null pointer dereferencing | Read uninitialized memory | Invalid free | Use after free | Double free | 总数 |
安全区域 | 0 | 0 | 0 | 0 | 1 | 0 | 1 |
非安全区域 | 21 | 12 | 7 | 10 | 13 | 6 | 69 |
从上面数据可以看到几乎所有内存缺陷都位于非安全区,另外根据研究报告分析,唯一一个位于安全区的缺陷是由于Rust早期版本安全机制不健全导致的,在V0.3后版本已经可以对此类问题进行拦截了。
因此可以得出结论:Rust提供的安全机制可以有效避免内存安全问题,但是也不能忽视程序员仍然可能因为各种原因弃用安全机制而引入内存安全问题。
这些原因可能是性能要求、项目改造必须混用C/C++、功能实现需要绕过Rust检查机制等。
同时研究报告中还对线程安全问题进行了相关的分类研究,由于此类缺陷不属于C/C++特有的缺陷类型,因此此处不做过多分析,仅给出结论:并发问题的解决依然需要依靠程序员的设计,Rust提供的安全机制还不够完善。
如果只看C/C++与Rust的对比,小结如下:
1、C/C++特有的CWE缺陷类型在公开的154102个CVE漏洞的数据集中占比为16.2%,而在这个16.2%的子集中,内存相关缺陷占比为86.95%,分别为:内存读、写越界,拷贝越界,重复释放,使用已释放的内存块,如果能够在语言层面解决这些问题则能够消除大部分C/C++特有缺陷。
2、通过分析Rust编写的软件和库中的缺陷,按照C/C++特有的CWE进行分类,所有对应缺陷都可以通过Rust的safe机制拦截,即如果使用Rust替代C/C++实现并全部采用safe机制,理论上可以消除现有95%的缺陷。
总结
1、从数据分析上看Rust在解决内存问题上确实如它宣称的一样,通过safe机制可以很好的防止内存相关缺陷的发生;
2、从数据分析上看Rust并没有如它所宣称的一样能够很好的解决多线程问题,甚至safe机制会引入更多的死锁问题;
3、从数据上看C/C++语言相关缺陷中超过80%的缺陷与内存相关,并且这80%以上的缺陷在Rust中已经完全可以用safe机制防护;
综上,至少在内存安全方面Rust确实可以完虐C/C++。
对于一个企业是否要从C/C++迁移到Rust,仍然需要从几个维度进行考虑:
1、Rust的生态是否能够支撑企业所在的领域;
2、Rust程序员的培养能否支撑企业的开发规模;
3、Rust的静态检查工具能否高效发现其他类型的缺陷;
4、企业开发的产品是否适合全部采用Rust的safe机制;
5、企业开发专家能否放弃自身优势,拥抱变化。
即使以上维度考虑后都合适,大规模的产品仍然需要考虑替换策略,以保证产品在交付质量和效率大致不变的情况下顺利完成技术栈的演进,以后有机会的话再聊聊这方面的话题。