• ​C++升级内存安全——Rust 的冬天要来了吗?
  • 发布于 1个月前
  • 74 热度
    0 评论
在上个月,白宫国家网络安全向导办公室(White House Office of the National Cyber Director,ONCD))发布了《朝向安全和可衡量软件的道路(A PATH TOWARD SECURE AND MEASURABLE SOFTWARE)》,认为信息安全是国家安全及其重要的一环,而编程语言作为软件基础的构建模块,其安全性决定了以此编写的软硬件的安全质量,使用内存安全的编程语言可以消除大多数内存安全错误。

这份报告强烈建议所有编程人员应该将所有现行代码转移到内存安全的编程语言中,而对于那些不容易移植的遗留代码库,也应该执行内存安全的操作。CWE(Common Weakness Enumeration,通用缺陷枚举)。是由美国国土安全部国家计算机安全部门资助的软件安全战略性项目。CWE 的数据报告显示,内存安全漏洞是困扰网络防御者几十年的最普遍的漏洞类别之一。而 C 和 C++ 则是报告中被点名的“既缺乏与内存安全相关的特征、也在关键系统中高度普及”的编程语言。

C++ 的内存安全漏洞
在编程时,开发者每创建一个变量并为其赋值时,这些信息都会被存储在内存中,而如何分配这些内存则是编程语言的底层功能之一。C++ 你手动为这些变量分配内存,并在不需要这些内存时,手动回收内存——这提供了很大的控制自由度,但也会带来内存安全漏洞的问题。

举个例子,我们可以想象计算机的内存就像是一个巨大的仓库,而每一块内存就像是仓库里的一个储物柜。而 C++ 这种语言,在编写程序时,就像是仓库管理员,可以自由的打开和关闭这些储物柜,把东西放入或者取出。

这个过程中可能会发生的一个问题就是越界访问——仓库管理员疏忽大意,把一份物品放入了错误的柜子,或者从错误的柜子取出了东西。在现实生活中,这可能导致物品丢失或者被错误地分发出去;在计算机程序中,这种情况就可能让一些不应该获得的人取得了私密信息,比如黑客获取密码。

另外一个常见的问题是内存泄漏——仓库管理员打开了一个柜子,放入了物品  A,但是忘记了关闭它。可能其他的管理员看到这个开着的柜子就会再放入新的物品 B,结果导致原来的物品 A 被遗忘在里面。在计算机程序中,这种情况会浪费内存,长时间下来可能会使系统变得越来越慢,严重时甚至导致系统崩溃。

生活中的很多 APP 都会面临这个问题。比如在聊天 APP 中,你可以发送消息、分享照片、甚至是发送语音信息——这些数据在 APP 中被储存和处理。如果这些 APP 是使用 C++ 来开发的,那么开发者必须手动管理在程序运行时存储这些数据的内存。

设想一下,当你发送一张照片给朋友时,APP 需要在手机的内存中为这张照片分配一块空间。现在,如果开发者在编写代码时犯了个错误,比如在照片发送完毕后忘记告诉程序“这部分内存已经不再需要了,可以用来干其他事了”,这就类似于仓库管理员把物品放到了现实生活中的仓库里,然后就把这个物品的存在给忘了。

随着时间的推移,你发送越来越多照片,APP 就会不断“忘记”越来越多的储存块——这就是所谓的内存泄漏。手机内存逐渐被这些无用数据充满,最终会导致  APP 运行缓慢或者崩溃,严重时甚至可能影响到手机其他功能的正常使用——手机中的存储空间在无形中被蚕食,但你却不知道究竟发生了什么。

C++ 的努力
为了解决 C++ 的内存安全漏洞,C++ 的创造者 Bjarne Stroustrup 和 C++ 社区做了很多努力。尽管很多安全漏洞都需要开发者手动修改代码来修复,但是鉴于很多工业项目有着庞大的代码量,C++ 更希望能在不手动修改代码的情况下,也可以修复一些安全漏洞。

在 2023 年 CppCon 2023 的演讲中,Bjarne Stroustrup 首次提出了安全配置文件(Safety Profiles)的概念,就像是流水线在组装复杂的电子设备时的《XX产品组装规范指南》罗列的组装的步骤、零件、工具一样,安全配置文件为 C++ 开发人员提供一套规则和最佳实践办法,以确保当他们编写代码时可以避免常见的错误,来提高 C++ 的安全性。在启用这些安全配置文件后,无需任何源代码更改,就可以重新编译代码,修复漏洞,提升安全性。这些配置文件将强制执行,允许代码在保持安全的同时,不影响其性能、功能性和灵活性。

一个易于理解的关于 C++ 安全配置文件的例子是强制运行时的边界检查 。比如,假设我们有一个标准的 C++ std::vector<int>,并且我们想要访问它的第 5 个元素,通常我们会写 vector[4]。如果没有边界检查,当这个元素不存在时(如果向量中的元素少于5个),会导致未定义行为,可能进一步导致程序崩溃或者其他安全问题。

使用安全配置文件,当我们尝试访问 vector[4] 时,编译器(或者运行环境)会自动插入代码来检查4是否为有效索引,确保它大于等于0并且小于 vector.size()。如果索引越界了,程序可以抛出一个异常,或者采用其他一些方式来安全地处理这个错误情况,而不是让程序继续执行可能不安全的操作。

在这个例子中,开发者不需要手动添加任何边界检查代码;安全配置文件确保了在访问类似 std::vector 这类的容器元素时,默认进行边界检查,大大减少了缓冲区溢出的风险。这样,只需重新编译现有代码并启用安全配置文件,即可带来立即的安全优势。 

C++ 安全提升会让 Rust 坐冷板凳吗?
不会。尽管 C++ 做了一系列优化方案和实践,但它仍需要确保新的安全特性与广大的遗留代码库兼容——这可能导致实施新安全措施时遇到别的阻力或兼容性问题。另外,由于 C++ 是一个广泛使用的语言,它的安全实践往往需要一个漫长的周期,即使新的安全措施可能非常有效,但在广泛应用到实际项目前,还需要很长时间。甚至有些项目还需要特制的安全配置文件,比如安全嵌入式开发、安全汽车、安全医疗等,C++ 的完全优化可能需要十年。

回到文首的 ONCD 报告:这份报告提出开发者应该在开发中使用内存安全的编程语言——提及并且仅提到了 Rust。尽管 C++ 会引入一些安全措施来增强其内存安全能力,但 Rust 仍有它去除日常编程中常见的错误的竞争优势,比如:引入了一个独特的系统——借用检查器(borrow checker),它强制实施严格的规则以防止内存错误;

通过作用域所有权(ownership)模型在变量出界时自动回收资源,防止了悬空指针和类似问题,确保了不再存在的数据不会被误用,提升了程序的安全性和稳定性。

当然,这并不意味着使用安全的编程语言就可以彻底实现代码安全。开发者在编程时还应该养成良好的习惯,比如:合理使用编程语言的静态分析器(analyzers)和代码清理程序(sanitizers),在不运行或运行代码的情况下都检查代码,识别和修正潜在的安全隐患。
用户评论