Rust与C/C++在内存安全CVE上的差异
摘要
分析Rust与C/C++在内存安全CVE报告方式上的不同,论证即使存在错误,Rust的设计也能降低某些类型漏洞的发生。
暂无内容
查看缓存全文
缓存时间: 2026/06/15 17:59
# Rust 和 C/C++ 中的内存安全 CVE 有何不同
来源:https://kobzol.github.io/rust/2026/06/15/how-memory-safety-cves-differ-between-rust-and-c-cpp.html
CVE 是一个用于对软件安全漏洞进行分类和报告的数据库。可报告的漏洞种类繁多,有些仅由程序逻辑错误导致(例如最近在 Cargo 中报告的 CVE (https://blog.rust-lang.org/2026/05/25/cve-2026-5222/)),但最棘手的漏洞往往由内存不安全引发,它们很容易被利用。本文中,我想聚焦于后一类 CVE,探讨它们是如何被报告(尤其是在库中),以及 Rust 与 C 或 C++ 在这方面有何不同。
我时常看到网上有人拿 Rust 和 C/C++ 软件中的 CVE 数量做对比,并声称 Rust 并非“真正”内存安全,或者即便它仍有 CVE,采用它也不值得。当我向习惯于 C 或 C++ 编程的人教授 Rust 时,有时也会观察到类似观点。当然,任何人都可以自由地进行此类比较,并据此得出自己的结论。但我认为,在 Rust 和 C/C++ 中,对潜在内存安全漏洞的处置方式存在一个重要差异,这一点可能并不显而易见,尤其是如果你不了解 Rust 的工作原理。我想在这篇文章中解释清楚。
但首先,我必须澄清:在 Rust 中绝对有可能引发内存不安全错误和未定义行为。在绝大多数情况下¹ (https://kobzol.github.io/rust/2026/06/15/how-memory-safety-cves-differ-between-rust-and-c-cpp.html#fn:compiler-bug),这需要用到 `unsafe` 关键字,但任何声称 Rust 程序完全不可能出现 UB 的说法都是错误的。在 Rust 中同样可能引发一般性漏洞 (https://rustsec.org/advisories/)(即与内存不安全无关的漏洞)。毕竟,忘记为自己的管理后台添加仅限管理员访问的检查,在任何语言中都可能发生。
然而,Rust 与 C 或 C++ 在潜在漏洞方面存在一个根本性区别,这也正是 Rust **在实际中**比 C 或 C++ **更安全** (https://blog.google/security/rust-in-android-move-fast-fix-things) 的核心原因。我将尝试以用 C 编写的网络库 `curl` (https://curl.se/) 为例进行说明。
## `curl` 中的潜在漏洞?
`(lib)curl` 是世界上使用最广泛、维护最优秀的开源库之一。它的主要开发者 Daniel Stenberg (https://daniel.haxx.se/) 是我们这个时代最多产的开源维护者之一,他与众多其他开发者一起,在过去 30 年里孜孜不倦地改进这个库。尽管最近不得不应对由 LLM 发现的大量 (https://daniel.haxx.se/blog/2026/05/26/the-pressure/) CVE,他和他的合作者们仍出色地让 `curl` 免受潜在利用和漏洞的侵害,并以此为傲。
那么,让我们来测试一下。我打开了 `libcurl` 的文档 (https://curl.se/libcurl/c/allfuncs.html),找到了我看到的第一个接受参数的函数 `curl_getenv` (https://curl.se/libcurl/c/curl_getenv.html)。这应该是一个简单的函数,用于在不同操作系统间提供可移植的环境变量值获取抽象。`curl` 应是安全且健壮的,那这个函数肯定不包含任何 UB 或内存不安全,对吧?那么,下面的 C 程序呢?
```c
#include <curl/curl.h>
int main(void) {
curl_getenv(NULL);
}
```
这个只有 5 行的 C 程序再简单不过了,它只是用 `NULL` 指针调用 `curl_getenv` 函数,并且编译时没有任何警告。然而,当你执行它时,你(可能)会得到一个段错误,从而出现内存安全 bug,进而成为一个潜在漏洞/利用点:
```
$ gcc test.c -otest -lcurl -Wall -Wextra
$ ./test
Segmentation fault (core dumped)
```
> 当然,这个程序人为地简单,但这正是关键所在。在实践中,类似情况在大型程序中很容易(也确实常常)意外发生。
哈,所以也许 `curl` 并没有那么安全?我应该去把这个问题报告为 `curl` 的漏洞吗?不,当然不。那样很愚蠢。我知道这一点,你也知道。但我们又是如何知道的呢?这才是有趣的地方。
想象一个非常相似的程序,它这样调用函数:`curl_getenv("FOO")`。如果那个程序仍然段错误,从而存在潜在漏洞呢?我相信 `curl` 的维护者会很想知道这种情况,并且会认为这是一个大问题。同时,我确信如果我报告第一个程序是 `curl` 的漏洞,他们会(理所当然地)训斥我。然而这两个程序只差了一点点。
所以,这到底是为什么呢?
在实践中,像我的原始示例中的 UB 通常被认为是“使用不当”² (https://kobzol.github.io/rust/2026/06/15/how-memory-safety-cves-differ-between-rust-and-c-cpp.html#fn:skill-issue),而不是我正在使用的库或 API 的问题,而是我的(应用程序)代码的问题。这主要是出于以下两个原因:
- 在 C 中,由于类型系统的限制,通常不可能精确地指定 API 的契约(不变量、前置条件、后置条件等)³ (https://kobzol.github.io/rust/2026/06/15/how-memory-safety-cves-differ-between-rust-and-c-cpp.html#fn:cpp),库作者往往懒得描述所有可能的错误用法,因为这不切实际。事实上,`curl_getenv` 的文档 (https://curl.se/libcurl/c/curl_getenv.html) 并未说明用 `NULL` 调用它是禁止的,并且可能导致段错误!因此作者假定你会“正确”使用该库(无论这意味着什么),如果你不这样做,那么由此引发的任何漏洞都是你的责任。
- 在 C 或 C++ 中,意外触发 UB 实在太简单 (https://blog.habets.se/2026/05/Everything-in-C-is-undefined-behavior.html) 了,这意味着如果我们报告所有可能引发漏洞的潜在情况(如我示例程序中的情况),大多数 C 或 C++ 库都会被数百万个 CVE 淹没。这样做没有意义,因为几乎每个函数调用都有五种不同的方式可能引发漏洞。因此,在 C 和 C++ 中,我们通常不会认为类似情况值得在使用的库中报告一个 CVE。换句话说,我们为库的特定**误用**创建 CVE,而不是因为存在一个**可以被误用**的库 API。
## Rust 中又有何不同?
那么,对于上述情况,C 或 C++ 与 Rust 的处理方式有何关键区别呢?
`hyper` (https://github.com/hyperium/hyper) 很可能是 Rust 中最流行的网络/HTTP 库,精神上与 C 中的 `libcurl` 类似。假设 `hyper` 也有一个类似的简单函数,接受一个参数,我像这样写一个 Rust 程序:
```rust
fn main() {
hyper::foo(None);
}
```
然后我运行 `cargo run`,程序段错误了。这会是 `hyper` 的一个 CVE 吗?是的,绝对会⁴ (https://kobzol.github.io/rust/2026/06/15/how-memory-safety-cves-differ-between-rust-and-c-cpp.html#fn:compiler-bugs)!该程序不包含任何 `unsafe` 块,因此如果发生内存错误,那一定是因为 hyper 库存在健全性 bug。
区别在于:在 Rust 中,如果以**任何可以想象的方式**使用库,只要用户代码不使用 `unsafe`,就可能导致内存错误,那么这永远是库的 bug,而不是用户代码的 bug。这就是为什么我们把这样的 API 称为**不健全的**,或者说它们存在**健全性漏洞**,因为在安全 Rust 中,存在一种方式可以错误地使用它们(就内存安全而言)。换句话说,即使我们尚未在野外找到任何实际触发该问题的程序,只要有可能通过安全库 API 导致内存错误,我们就会为其创建 CVE。
这意味着 Rust 中报告的一些 CVE 比 C 或 C++ 中的“更严格”,有些人觉得这“不公平” (https://xcancel.com/pcwalton/status/1579643729836924929)。如果我们将同样的逻辑应用于 C,那么 `curl_getenv` 就应该被标记为 `curl` 的 CVE,因为存在一种使用方式会导致内存错误。但当然,这在 C 中毫无意义,因为 C 里没有安全与 `unsafe` 的概念(或者更准确地说,所有 C 代码都隐式地是 `unsafe` 的)。这就是为什么我前面说报告这个 CVE 会很愚蠢。
对于“我是否正确使用了这个函数”(关于内存安全问题,而非逻辑错误)这个问题,在 C 或 C++ 中通常很难判断,但在 Rust 中却非常简单:
- 如果调用的函数没有标记为 `unsafe`,那么答案就是“**是的**”。不可能错误地使用它。
- 如果调用的函数是 `unsafe` 的,那么我必须用 `unsafe` 块标记该调用,这会在代码审查和代码库中立即显眼地表明该位置存在潜在危险。在这种(通常非常罕见的)情况下,我们就退回到了 C 或 C++ 的级别。
答案的第一部分正是 Rust 内存安全在实践中得以扩展的原因。如果你的代码中不使用 `unsafe`(在绝大多数情况下都不需要,除非你在编写操作系统或无锁数据结构之类的东西),并且没有遇到编译器错误,那么你**知道**任何潜在的内存不安全原因都不是你的错。如果一个库没有暴露任何 `unsafe` 接口,那么你**根本不可能**以导致内存错误的方式使用它,除非该库内部使用了 `unsafe` 并且有 bug。但如果发生了这种情况,bug 会在**该库内部**修复,然后它的所有用户又自动免于内存错误的困扰了。
这就是 Rust 与 C 或 C++ 的区别。尽管 `curl` 的开发者们努力构建了一个完全安全且健壮的 C 库,但使用它的数百万个其他 C 程序仍然可以仅仅因为“拿错姿势”而轻易引入内存不安全,而 `curl` 的开发者对此毫无办法。
## 结论
我用 `curl` 作为例子,但这同样适用于几乎任何 C 或 C++ 库,包括这两种语言的标准库(以及通常其他内存不安全的语言)。我原本想展示更多例子,但最终发现它们都一样,所以只保留了一个 `curl` 函数,因为它很好地说明了差异。
我在这篇博文中描述的内容并非什么开创性的发现,我认为大多数了解 Rust 工作原理的人都能理解。但我不记得见过相关的博文,而且我反复向一些人解释过这个观点,所以我想把自己的想法写下来,这样下次再发生类似讨论时,我只需链接到这篇文章。
我希望以上内容表明,比较 Rust 和 C 或 C++ 每行代码的 CVE 数量具有很大的误导性,我们在比较 Rust 和其他系统编程语言的内存安全性时应该考虑到这一点。
如果你对 CVE 在 Rust 或 C/C++ 中(应该)如何工作有不同看法,请在 Reddit (https://www.reddit.com/r/rust/comments/1u6km19/how_memory_safety_cves_differ_between_rust_and_cc) 上告诉我。
---
1. 编译器/标准库 bug;但这类 bug 在实际生产软件中极其罕见。 [return]
2. 或者更直白地说,“技术能力不足”。[return]
3. C++ 可以通过使用迭代器/span 等方式提供更好的抽象,但总的来说,在准确指定契约方面仍远不如 Rust。 [return]
4. 除非是由于编译器/标准库 bug 导致的内存问题;但再次强调,这极其罕见。 [return]
相似文章
内存安全是生死攸关的问题
作者认为,内存不安全的开源软件极易受到即将到来的人工智能漏洞查找代理的攻击,这使内存安全成为道德义务,并且Rust必须作为领先且零开销的内存安全语言取得成功。
安全变得简单 第1部分:单一所有权(并非)可选
本文介绍了一种基于线性类型和抽象解释的内存安全新方法,旨在比Rust更符合人机工程学原理地消除诸如释放后使用和内存泄漏等常见错误。
回归构建模块的构建模块
本文类比了C/C++中的安全漏洞与Verilog中的安全漏洞,指出硬件描述语言的设计导致了缺陷,并认为行业应投资于更安全的替代方案,类似于软件领域对内存安全编程语言的推动。
安全 Rust 的边界
TokioConf 2026 的一篇演讲/博客文章探讨了如何通过为复杂指针结构实现追踪式垃圾回收,将安全 Rust 推向极限,并分享处理循环引用与原始指针 GC 设计的技巧。
改进 C# 内存安全
微软宣布对 C# 16 中的 unsafe 关键字进行重新设计,以强制执行内存安全契约,使 unsafe 操作变得可见并由编译器强制执行,预览版将在 .NET 11 中发布,正式版在 .NET 12 中发布。