C语言示例
让我们查看以下C语言的 “最小错误示例” 程序:
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
int main(int argc, char* argv[]) {
char *buf, *filename;
FILE *fp;
size_t bytes, len;
struct stat st;
switch (argc) {
case 1:
printf("Too few arguments!\n");
return 1;
case 2:
filename = argv[argc];
stat(filename, &st);
len = st.st_size;
buf = (char*)malloc(len);
if (!buf)
printf("malloc failed!\n", len);
return 1;
fp = fopen(filename, "rb");
bytes = fread(buf, 1, len, fp);
if (bytes = st.st_size)
printf("%s", buf);
else
printf("fread failed!\n");
case 3:
printf("Too many arguments!\n");
return 1;
}
return 0;
}
你发现了多少bug?
尽管该C语言示例仅有29行代码,但它却包含了至少11个严重bug:
- 使用赋值
=
而非判断相等==
(第28行) printf
有多余参数(第23行)- 文件描述符泄露(第26行之后)
- 多行
if
语句缺少花括号(第22行) switch
语句忘记添加break
(第32行)buf
字符串忘记NUL终止符,从而导致缓冲区溢出(第29行)- 未释放由
malloc
分配的缓冲区,从而导致内存泄漏(第21行) - 越界访问(第17行)
switch
语句存在未检查的情况(第11行)stat
和fopen
存在未检查的返回值(第18行及第26行)
即使对于C语言编译器,这些bug难道不应该是显而易见的吗?
惊人的是,即便使用最新版本的GCC(截至撰文时为13.2),在默认警告等级下编译代码时也不出现任何警告。
这是非常极端的示例吗?
当然不是。这些类型的bug在过去曾引发一系列的安全漏洞,比如以下案例:
- 使用赋值
=
而非判断相等==
:2003年Linux后门尝试 - 多行
if
语句缺少花括号:Apple·goto失败漏洞 switch
语句忘记添加break
:破坏sudo的break
Rust在这些方面表现得怎么样?
安全Rust使这些bug的出现变得不可能:
- 不支持
if
语句内赋值。 - 编译时检查格式化字符串。
- 在作用域末尾,Rust通过
Drop
trait来释放资源。 - 所有
if
语句必须有花括号。 match
语句(在Rust中相当于switch
)并不会落空,因此你不会意外忘记一个break
。- 缓冲区切片自带它们的大小,且不依赖NUL终止符。
- 当相关
Box
离开作用域时,Rust通过Drop
trait释放堆分配内存。 - 越界访问会导致程序严重错误而终止,也可以用
get
方法来检查一个序列是否越界。 match
语句规定要处理所有情况。- 可出错的Rust函数返回的
Result
值需要拆箱并检查是否成功。此外,如果你忽略检查标注为#[must_use]
的函数的返回值,编译器会发出警告。