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:

  1. 使用赋值 = 而非判断相等 ==(第28行)
  2. printf有多余参数(第23行)
  3. 文件描述符泄露(第26行之后)
  4. 多行if语句缺少花括号(第22行)
  5. switch语句忘记添加break(第32行)
  6. buf字符串忘记NUL终止符,从而导致缓冲区溢出(第29行)
  7. 未释放由malloc分配的缓冲区,从而导致内存泄漏(第21行)
  8. 越界访问(第17行)
  9. switch语句存在未检查的情况(第11行)
  10. statfopen存在未检查的返回值(第18行及第26行)

即使对于C语言编译器,这些bug难道不应该是显而易见的吗?
惊人的是,即便使用最新版本的GCC(截至撰文时为13.2),在默认警告等级下编译代码时也不出现任何警告。

这是非常极端的示例吗?
当然不是。这些类型的bug在过去曾引发一系列的安全漏洞,比如以下案例:

Rust在这些方面表现得怎么样?
安全Rust使这些bug的出现变得不可能:

  1. 不支持if语句内赋值。
  2. 编译时检查格式化字符串。
  3. 在作用域末尾,Rust通过Drop trait来释放资源。
  4. 所有if语句必须有花括号。
  5. match语句(在Rust中相当于switch)并不会落空,因此你不会意外忘记一个break
  6. 缓冲区切片自带它们的大小,且不依赖NUL终止符。
  7. 当相关Box离开作用域时,Rust通过Drop trait释放堆分配内存。
  8. 越界访问会导致程序严重错误而终止,也可以用get方法来检查一个序列是否越界。
  9. match语句规定要处理所有情况。
  10. 可出错的Rust函数返回的Result值需要拆箱并检查是否成功。此外,如果你忽略检查标注为#[must_use]的函数的返回值,编译器会发出警告。