flawopen.com/内存安全/Rust unsafe
Rust 的 unsafe 块仍能保证内存安全吗?
unsafe 不会关闭 Rust 的检查。它允许执行编译器无法证明安全条件成立的特定操作,而程序员必须履行这些安全义务。因此,不健全的 unsafe 抽象可能让普通安全调用者触发未定义行为(UB),即使调用者从未编写 unsafe。
关键术语
- Unsafe 操作:
- 具有编译器无法证明之安全条件的操作,例如解引用裸指针。
- 健全抽象:
- 一种安全 API,其安全调用者无法触发未定义行为。
- size_hint():
- 用于优化的迭代器大小估计;不是精确计数,也不是内存安全证明。
- 未定义行为(UB):
- Rust 未定义其结果的程序行为;unsafe 代码必须阻止安全调用者触发它。
不健全的安全抽象如何失效
安全 API 接收迭代器
调用者将普通迭代器传给没有 unsafe 前置条件的函数。
实现依据提示预留容量
函数只预留报告的下限,该值可能小于迭代器实际产出的元素数。
未经检查的写入超过剩余容量
实现通过裸指针写入每个元素,却不扩展分配,也不逐项检查容量。
安全调用触发未定义行为
若元素数超过可容纳数量,安全调用可能越界写入。应使用 Vec::extend 或其他能安全扩容的实现。
// UNSOUND: reserve uses only the lower hint, // then unchecked writes consume every item. fn append_unsound(dst: &mut Vec<u8>, iter: impl Iterator<Item = u8>) { let (lower, _) = iter.size_hint(); dst.reserve(lower); let start = dst.len(); let mut written = 0; unsafe { let out = dst.as_mut_ptr().add(start); for item in iter { // UB once writes exceed spare capacity. out.add(written).write(item); written += 1; } dst.set_len(start + written); } }
// SOUND: Vec::extend handles capacity // growth and length updates safely. fn append(dst: &mut Vec<u8>, iter: impl Iterator<Item = u8>) { dst.extend(iter); }
Rust 的保证具体是什么
准确的保证是:只要安全 Rust 所依赖的 unsafe 代码是健全的,安全 Rust 就不会导致未定义行为。Rust Reference 从边界定义健全性:unsafe 代码必须确保安全客户端无法触发未定义行为。编译器不会验证 unsafe 操作中的每项证明义务;程序员和审查者必须完成这些论证。
与 C 或 C++ 相比,这仍是 Rust 的重要实践优势:Rust 会更清楚地标出涉及内存的操作及其安全义务,让审查者聚焦于 unsafe 边界及其上层安全抽象。边界包括 unsafe 块、unsafe 函数契约、unsafe trait 实现、FFI 声明和 unsafe 属性,并不总局限于一个小代码块。真实案例是 smallvec 的 CVE-2021-25900:SmallVec::insert_many 根据迭代器 size hint 的下限预留空间,却写入了超出分配容量的元素;CVSS 评分为 9.8。
Soundness 是标准,不是小心程度
如果安全代码能利用某个 API 触发未定义行为,该 API 就是不健全的。Iterator::size_hint 提供的是优化边界,而非精确元素数:有效迭代器可以产出多于下限的元素,有缺陷的迭代器也可能违背估算。标准文档明确指出,unsafe 代码不得据此省略边界检查。安全 trait 行为错误也可能导致逻辑错误或 panic;这不会自动构成 UB,但即使调用者代码行为意外,unsafe 代码仍须维护内存不变量。
因此,unsafe 代码的影响可能跨越多个位置:某处建立的内存不变量可能被上层安全 API 依赖。如果该不变量不成立,缺陷就在暴露安全 API 的抽象中,而不在按契约使用它的安全调用者。
实践规则
- ✓不要把安全 trait 方法当作内存安全证明。尤其是,
size_hint()只能用于优化;应保留检查或按需扩容。 - ✓确保 panic 时仍维持不变量。调用者代码(如
Iterator::next、析构函数或闭包)可能触发栈展开。应确保部分初始化状态仍然有效并能正确析构。 - ✓编写
// SAFETY:注释,说明必须满足的条件及其成立原因。没有解释的 unsafe 无法审查。 - ✓缩小 unsafe 块。为少写关键字而把整个循环包进 unsafe,会扩大审查者必须推理的范围。
- ✓在实际执行相关路径的测试中运行 Miri。它能发现已执行路径上的部分 UB,包括越界访问、释放后使用和无效别名;但不能证明所有 unsafe 代码都健全。
- ✓审计依赖项。
cargo audit检查已知 RustSec 公告;cargo geiger可协助盘点 unsafe 用法。两者都不能替代对交付代码中 unsafe 契约的审查。
常见问题
#![forbid(unsafe_code)] 可以禁止启用该属性的 crate 中的 unsafe 语法,适用于应用 crate。它不会约束依赖项,而且某些实现、FFI 边界和库仍需要 unsafe 操作。
有。unsafe trait 记录实现者必须遵守的义务,unsafe 代码可以依赖这些义务。TrustedLen 是标准库中的不稳定 trait,供承诺精确 size hint 的迭代器使用。普通 Iterator::size_hint 不提供此保证。
没有。Rust 的安全模型缩小了内存安全义务出现的范围,并在 unsafe 边界明确展示这些义务。它并未免除对 unsafe 代码、依赖项、FFI 契约,以及基于 unsafe 实现构建的安全 API 的审查。
参考资料
- The Rust Reference — unsafe code and soundness
- Rust Reference — the unsafe keyword and proof obligations
- Rust standard library — Iterator::size_hint contract
- Rustonomicon — working with unsafe
- RustSec Advisory RUSTSEC-2021-0003 — smallvec buffer overflow
- GitHub Security Advisory GHSA-43w2-9j62-hq99 — CVE-2021-25900
- The Rustonomicon — the guide to unsafe Rust
- RustSec Advisory Database
- Miri — an interpreter that detects undefined behaviour
- CWE-787 — Out-of-bounds Write
- Case study: the smallvec overflow (CVE-2021-25900)