Rust unsafe
unsafe 扩大信任边界而非关闭借用检查;能做什么、必须保证什么、如何封装安全抽象。
#concept
#growing
#dev
#rust
[!info] related notes
Rust unsafe
这个概念为什么出现
安全 Rust 无法表达所有底层操作(原始指针、某些 FFI、手工布局)。
unsafe 让你进入需要额外人工证明的区域,同时仍保留大量编译检查。
[!abstract] 一句话理解
unsafe允许五类超能力(裸指针解引用、unsafe 函数、可变静态、union 字段、实现 unsafe trait);不变量由程序员保证,应用安全封装对外。
最小可运行示例
场景:区分安全切片 API 与需要调用方证明的裸指针 API
空指针与长度检查不能证明地址存活、已初始化或不存在数据竞争。安全入口直接接收切片;确需裸指针时,用 unsafe fn 暴露证明义务,不能仅写一条“调用方保证”就声称函数安全。
// 裸指针入口必须将无法运行时验证的契约交给调用方。
/// # Safety
/// 若 data 非空且 len > 0,data 必须指向一个存活、已初始化的 u8,
/// 并且读取期间没有并发写入。此函数仅读取首字节,不验证整段 len。
unsafe fn first_byte_raw(data: *const u8, len: usize) -> Option<u8> {
if data.is_null() || len == 0 {
return None;
}
// SAFETY: 调用方保证首字节有效且可读,当前分支已排除空指针。
Some(unsafe { *data })
}
fn first_byte(data: &[u8]) -> Option<u8> {
data.first().copied()
}
fn main() {
let buf = [9u8, 8, 7];
println!("{:?}", first_byte(&buf));
println!("{:?}", first_byte(&[]));
// SAFETY: buf 在调用期间存活、已初始化且未被修改。
assert_eq!(unsafe { first_byte_raw(buf.as_ptr(), buf.len()) }, Some(9));
}
建议运行:cargo run
期望:
Some(9)
None
结合场景再看三个关注点
- SAFETY 注释写清前提
- 安全入口须对所有合法安全调用成立;无法检查的指针契约不能藏在 safe 函数内
- 最小化 unsafe 行数
核心概念与准确模型
unsafe 超能力(教学列表):
- 解引用裸指针
- 调用
unsafe fn - 访问/修改可变静态
- 实现
unsafe trait - 读 union 字段
未定义行为(UB)例子:悬垂、越界、数据竞争、无效枚举判别式…
安全抽象必须在所有合法安全调用下保持不变量;仅缩小 unsafe 块不能证明封装 sound。Rust 2024 中 unsafe fn 内的具体操作仍应显式放入 unsafe 块,声明契约与履行契约是两回事。
设计动机
- 系统编程需要底层
- 把危险集中审查
- 不逼所有代码进入 unsafe
边界与误区
- “有 unsafe 就随便” → UB
- 大范围 unsafe 块
- 无测试/无 Miri
[!warning] 常见误区:用 unsafe 绕过借用检查偷懒 正确动机应是表达安全 Rust 无法表达且已证明的不变量。
工程实践
- 最小权限 unsafe
- 文档 + SAFETY 注释
- 用 Miri/测试发现已覆盖路径中的问题;通过不等于对所有输入的安全证明
- 代码评审强制
本节总结
- unsafe = 信任边界
- 封装安全 API
- 与 FFI/性能相关
自测题
- unsafe 会关闭借用检查吗?
- 为什么要写 SAFETY 注释?
参考答案
- 不会整体关闭;只允许额外操作。
- 记录证明义务,供审计与未来修改。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| The Book — Unsafe Rust | 官方书 | unsafe |
| Rustonomicon | 官方 | 不变量与 UB |
| Miri | 工具 | 检测 UB |