前言
在 Rust 的世界里,虽然编译器已经帮我们解决了大部分内存安全问题,但当你开始编写 unsafe 代码时,就进入了一个编译器无法完全保护你的领域。这时候,Miri 就成为了你最重要的安全网。
正如一位开发者在 Rust 重写项目中所说:"如果你在编写 unsafe 代码时没有使用 Miri 进行检查,我认为这是愚蠢的。它迟早会给你带来麻烦。"
什么是 Miri?
Miri 是 Rust MIR(中级中间表示,Mid-level Intermediate Representation)的解释器。在 Rust 的编译过程中,代码首先会被编译为 MIR,然后再提交给 LLVM 进行处理。Miri 可以在这个层面上运行和检测代码。
Miri 能检测哪些问题?
Miri 可以帮助我们检查常见的未定义行为(UB),包括但不限于:
内存越界访问和释放后使用(use-after-free)
使用未初始化的数据
数据竞争
内存对齐问题
违反 Stacked Borrows 规则(多个可变引用问题)
双重释放(double free)
无效的内存访问
快速开始:安装和使用 Miri
安装 Miri
Miri 目前只能在 nightly Rust 上使用。安装步骤如下:
# 安装 nightly 工具链
rustup install nightly
# 添加 miri 组件
rustup +nightly component add miri提示:如果遇到问题,可以尝试指定特定的 nightly 版本:
rustup +nightly-2024-09-01 component add miri基本使用
# 运行程序
cargo +nightly miri run
# 运行测试
cargo +nightly miri test
# 运行特定测试
cargo +nightly miri test test_name第一个例子:检测多个可变引用
让我们看一个简单但危险的例子:
fn main() {
let mut x = 1;
unsafe {
let a: *mut usize = &mut x;
let b: *mut usize = &mut x;
*a = 2;
*b = 3;
}
}运行 cargo run 时,这段代码不会报错。但使用 Miri:
cargo +nightly miri runMiri 会检测到错误:
error: Undefined Behavior: attempting a write access using <2883>
at alloc1335[0x0], but that tag does not exist in the borrow stack
for this location
--> src/main.rs:7:9
|
7 | *a = 2;
| ^^^^^^
这就是 Miri 的价值:将本应该在运行时爆发的 UB 问题,在测试阶段就发现。
在 Fuchsia 项目中使用 Miri
如果你在使用 Fuchsia 或其他大型项目,需要一些额外的设置步骤:
1. 生成 Cargo.toml
# 配置构建
fx set PRODUCT.BOARD --with //path/to/your:tests --cargo-toml-gen
# 运行构建
fx build
# 生成 Cargo 配置
fx gen-cargo '//path/to/your:target(//build/toolchain:host_x64)'2. 设置 Miri
由于 Miri 需要编译新的 Rust sysroot,需要访问外部 crates。编辑 src/.cargo/config 并注释掉 vendored crates 配置:
# [source.crates-io]
# replace-with = "vendored-sources"
# [source.vendored-sources]
# directory = "../third_party/rust_crates/vendor"然后运行:
cargo +nightly miri setup3. 运行 Miri
cd src/path/to/your/crate
cargo +nightly miri testMiri 的最佳实践
1. 理解 Stacked Borrows
Miri 使用一个叫做 "Stacked Borrows" 的模型来检测违反 Rust 借用规则的行为。Rust 的核心规则是:
多个只读引用 XOR 一个可变引用
在 unsafe 代码中,很容易不小心违反这个规则:
fn problematic_code() {
let mut data = vec![1, 2, 3];
let ptr = data.as_mut_ptr();
unsafe {
// 危险:创建了多个指向同一内存的可变指针
let ptr1 = ptr;
let ptr2 = ptr;
*ptr1 = 10; // Miri 会在这里报错
*ptr2 = 20;
}
}
2. 在 CI/CD 中集成 Miri
为了确保代码质量,应该将 Miri 集成到持续集成流程中:
# .github/workflows/miri.yml
name: Miri
on: [push, pull_request]
jobs:
miri:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly
components: miri
override: true
- name: Run Miri
run: cargo miri test3. 使用特性标志隔离问题代码
当使用包含 C/汇编代码的库(如加密库)时,Miri 无法运行这些代码。解决方案是使用特性标志:
# Cargo.toml
[features]
default = ["native-crypto"]
native-crypto = []
miri-safe = []
[dependencies]
aws-lc-rs = { version = "1.0", optional = true }#[cfg(all(test, not(miri)))]
mod tests_with_crypto {
// 使用原生加密库的测试
}
#[cfg(all(test, miri))]
mod miri_safe_tests {
// 只使用纯 Rust 代码的测试
}运行测试时:
# 正常测试
cargo test
# Miri 测试(禁用原生加密)
cargo +nightly miri test --no-default-features --features miri-safe4. 编写 Miri 友好的测试
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_operations() {
let mut list = MyList::new();
list.push(1);
list.push(2);
assert_eq!(list.pop(), Some(2));
assert_eq!(list.pop(), Some(1));
assert_eq!(list.pop(), None);
}
#[test]
#[cfg_attr(miri, ignore)] // 对于非常慢的测试,在 Miri 下忽略
fn test_large_dataset() {
// 处理大量数据的测试
}
}5. 解读 Miri 错误信息
当 Miri 报错时,关键是理解错误信息:
error: Undefined Behavior: trying to reborrow for Unique at alloc84055,
but parent tag <209678> does not have an appropriate item in the borrow stack这个错误的含义:
trying to reborrow for Unique:尝试创建一个独占的可变引用
parent tag:原始的借用标记
borrow stack:Miri 维护的借用堆栈,用于跟踪引用的生命周期
通常这意味着你在 unsafe 代码中创建了冲突的引用。
Miri 的局限性
1. 动态分析的局限
Miri 是一个动态分析工具,这意味着:
✅ 优点:可以检测实际运行时的 UB
❌ 缺点:只能检测执行路径上的问题,无法覆盖所有可能的代码路径
这就是为什么需要高测试覆盖率的原因。
2. 无法运行 FFI 代码
Miri 无法运行 C/C++/汇编代码:
// 这段代码在 Miri 下无法运行
use openssl::*;
fn encrypt_data(data: &[u8]) -> Vec<u8> {
// OpenSSL 使用 C 代码
// Miri 会报错: can't call foreign function
}解决方案:
使用特性标志隔离 FFI 代码
使用纯 Rust 实现作为测试替代(如果可能)
对于 FFI 部分,使用 Valgrind 等工具补充检测
3. 性能开销
Miri 运行速度较慢,可能比正常测试慢 10-1000 倍。因此:
不要在 Miri 下运行性能测试
考虑只在 CI 的特定阶段运行 Miri
使用
#[cfg_attr(miri, ignore)]标记特别慢的测试
Miri vs 静态分析工具
研究人员开发了静态分析工具如 SafeDrop,它与 Miri 有不同的特点:
建议:两种工具结合使用,Miri 用于动态检测,静态分析工具用于覆盖更多路径。
实战案例:修复 use-after-free
让我们看一个实际的例子:
问题代码
pub struct MyQueue<T> {
head: Option<Box<Node<T>>>,
}
struct Node<T> {
elem: T,
next: Option<Box<Node<T>>>,
}
impl<T> MyQueue<T> {
pub fn pop(&mut self) -> Option<T> {
self.head.take().map(|head| {
let head = *head; // 将 Box<Node> 解包
self.head = head.next; // 移动 next
// 问题:head 被解包后,其内存在这里被释放
// 但我们还持有指向它的引用
head.elem // 返回 elem
})
}
}Miri 检测
$ cargo +nightly miri test
error: Undefined Behavior: trying to reborrow for Unique at alloc84055修复方案
impl<T> MyQueue<T> {
pub fn pop(&mut self) -> Option<T> {
self.head.take().map(|mut head| {
// 先获取 next
self.head = head.next.take();
// 然后返回 elem,Box 会自动清理
head.elem
})
}
}
高级技巧
1. 使用环境变量调整 Miri 行为
# 显示更详细的追踪信息
$ MIRIFLAGS="-Zmiri-track-raw-pointers" cargo +nightly miri test
# 禁用隔离(允许访问文件系统等)
$ MIRIFLAGS="-Zmiri-disable-isolation" cargo +nightly miri test
# 启用数据竞争检测
$ MIRIFLAGS="-Zmiri-disable-weak-memory-emulation=false" cargo +nightly miri test2. 针对特定平台测试
# 测试不同的目标平台
$ cargo +nightly miri test --target x86_64-unknown-linux-gnu
$ cargo +nightly miri test --target aarch64-unknown-linux-gnu3. 创建 Miri 配置文件
在项目根目录创建 .miri-config.toml:
[miri]
# 额外的 Miri 标志
flags = ["-Zmiri-track-raw-pointers"]
# 忽略特定的测试
ignored-tests = ["test_with_ffi", "slow_integration_test"]常见陷阱和解决方案
陷阱 1:忘记初始化内存
// 错误
let mut buffer: [u8; 1024];
unsafe {
// Miri 会报错:使用未初始化的内存
ptr::copy_nonoverlapping(src.as_ptr(), buffer.as_mut_ptr(), len);
}
// 正确
let mut buffer = [0u8; 1024];
// 或者使用 MaybeUninit
let mut buffer: [MaybeUninit<u8>; 1024] = unsafe {
MaybeUninit::uninit().assume_init()
};陷阱 2:指针别名问题
// 错误:创建了别名的可变指针
unsafe fn bad_swap(x: &mut i32, y: &mut i32) {
let temp = *x;
*x = *y;
*y = temp;
}
let mut value = 42;
unsafe {
bad_swap(&mut value, &mut value); // Miri 错误!
}
// 正确:使用 ptr::read 和 ptr::write
unsafe fn good_swap(x: &mut i32, y: &mut i32) {
if ptr::eq(x, y) { return; }
ptr::swap(x, y);
}陷阱 3:生命周期问题
// 错误
fn return_reference(data: &[i32]) -> &i32 {
let boxed = Box::new(data[0]);
&*boxed // Miri 错误:返回悬垂指针
}
// 正确
fn return_value(data: &[i32]) -> i32 {
data[0]
}参考资源
本文综合了多个 Rust 社区资源和实战经验,希望能帮助你更好地使用 Miri 编写更安全的 Rust 代码。