FlawOpen

flawopen.com/内存安全/Rust unsafe

Rust 的 unsafe 块仍能保证内存安全吗?

CWE-787:越界写入参考页面
简要回答

unsafe 不会关闭 Rust 的检查。它允许执行编译器无法证明安全条件成立的特定操作,而程序员必须履行这些安全义务。因此,不健全的 unsafe 抽象可能让普通安全调用者触发未定义行为(UB),即使调用者从未编写 unsafe。

关键术语

Unsafe 操作:
具有编译器无法证明之安全条件的操作,例如解引用裸指针。
健全抽象:
一种安全 API,其安全调用者无法触发未定义行为。
size_hint():
用于优化的迭代器大小估计;不是精确计数,也不是内存安全证明。
未定义行为(UB):
Rust 未定义其结果的程序行为;unsafe 代码必须阻止安全调用者触发它。

不健全的安全抽象如何失效

第1步

安全 API 接收迭代器

调用者将普通迭代器传给没有 unsafe 前置条件的函数。

第2步

实现依据提示预留容量

函数只预留报告的下限,该值可能小于迭代器实际产出的元素数。

第3步

未经检查的写入超过剩余容量

实现通过裸指针写入每个元素,却不扩展分配,也不逐项检查容量。

第4步

安全调用触发未定义行为

若元素数超过可容纳数量,安全调用可能越界写入。应使用 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 的抽象中,而不在按契约使用它的安全调用者。

实践规则

常见问题

能否彻底禁止 unsafe?

#![forbid(unsafe_code)] 可以禁止启用该属性的 crate 中的 unsafe 语法,适用于应用 crate。它不会约束依赖项,而且某些实现、FFI 边界和库仍需要 unsafe 操作。

有没有办法让 unsafe 代码信任某个 trait?

有。unsafe trait 记录实现者必须遵守的义务,unsafe 代码可以依赖这些义务。TrustedLen 是标准库中的不稳定 trait,供承诺精确 size hint 的迭代器使用。普通 Iterator::size_hint 不提供此保证。

这是否削弱了 Rust 的价值?

没有。Rust 的安全模型缩小了内存安全义务出现的范围,并在 unsafe 边界明确展示这些义务。它并未免除对 unsafe 代码、依赖项、FFI 契约,以及基于 unsafe 实现构建的安全 API 的审查。

参考资料

另请参阅 strncpy() 比 strcpy() 更安全吗? CWE 与 CVE:有什么区别? Mass Assignment:风险与安全字段白名单 Serverless 状态泄漏:按请求隔离用户数据 PHP == 与 Magic Hash 比较