Miri 是 Rust 的未定义行为检测工具。它可以运行货物项目的二进制文件和测试套件,并检测未能满足其安全要求的不安全代码。例如:
unreachable_unchecked
,调用具有重叠范围的copy_nonoverlapping
,...)bool
,或者无效的枚举判别式)最重要的是,Miri 还会告诉您有关内存泄漏的信息:当执行结束时仍有分配的内存,并且该内存无法从全局static
访问时,Miri 将引发错误。
您可以使用 Miri 模拟其他目标上的程序,例如确保字节级数据操作在小端和大端系统上都能正确工作。请参阅下面的交叉解释。
Miri 已经发现了许多现实世界的错误。如果您发现 Miri 的错误,请告诉我们,我们将不胜感激,我们会将其添加到列表中!
默认情况下,Miri 确保完全确定性的执行并将程序与主机系统隔离。一些通常会访问主机的 API,例如收集随机数生成器、环境变量和时钟的熵,被确定性的“假”实现所取代。设置MIRIFLAGS="-Zmiri-disable-isolation"
以访问真实系统 API。 (特别是,“假”系统 RNG API 使 Miri不适合加密使用!请勿使用 Miri 生成密钥。)
尽管如此,请注意,Miri不会捕获程序中所有违反 Rust 规范的行为,尤其是因为没有这样的规范。 Miri 使用自己的近似值来描述 Rust 中什么是未定义行为以及什么不是未定义行为。据我们所知,Miri可以检测到所有可能影响程序正确性的未定义行为(模错误),但您应该查阅参考资料以获取未定义行为的官方定义。 Miri 将使用 Rust 编译器进行更新,以防止当前编译器理解的 UB,但它不对 rustc 的未来版本做出任何承诺。
Miri 用户应注意的进一步警告:
-Zrandomize-layout
来检测其中一些情况。)-Zmiri-seed
值运行 Miri 来在一定程度上缓解这种情况,但到目前为止仍然无法探索所有可能的执行。--target x86_64-unknown-linux-gnu
以获得更好的支持。SeqCst
栅栏时,弱内存模拟可能会产生弱行为,并且它无法产生在真实硬件上可能观察到的所有行为。而且,Miri根本无法保证你的代码是健全的。健全性是指从任意安全代码调用时,即使与其他健全代码结合使用,也不会导致未定义行为的属性。相反,Miri 只能告诉您与代码交互的特定方式(例如,测试套件)是否会导致特定执行中的任何未定义行为(其中可能有很多,例如当并发或其他形式的非确定性时)参与)。当 Miri 找到 UB 时,你的代码肯定是不健全的,但是当 Miri 没有找到 UB 时,那么你可能只需要测试更多的输入或更多可能的非确定性选择。
每晚通过rustup
在 Rust 上安装 Miri:
rustup +nightly component add miri
以下所有命令假设 nightly 工具链是通过rustup override set nightly
固定的。或者,对以下每个命令使用cargo +nightly
。
现在您可以在 Miri 运行您的项目:
cargo miri test
。cargo miri run
通过Miri运行它。第一次运行 Miri 时,它将执行一些额外的设置并安装一些依赖项。在安装任何东西之前它会要求您确认。
cargo miri run/test
支持与cargo run/test
完全相同的标志。例如, cargo miri test filter
仅运行名称中包含filter
的测试。
您可以通过MIRIFLAGS
将标志传递给 Miri。例如, MIRIFLAGS="-Zmiri-disable-stacked-borrows" cargo miri run
运行程序而不检查引用的别名。
通过cargo miri
编译代码时,会为将在 Miri 下解释的代码设置cfg(miri)
配置标志。您可以使用它来忽略在 Miri 下失败的测试用例,因为它们执行 Miri 不支持的操作:
# [ test ]
# [ cfg_attr ( miri , ignore ) ]
fn does_not_work_on_miri ( ) {
tokio :: run ( futures :: future :: ok :: < _ , ( ) > ( ( ) ) ) ;
}
没有办法列出所有 Miri 不能做的无限事情,但是解释器在发现不支持的东西时会明确告诉你:
error: unsupported operation: can't call foreign function: bind
...
= help: this is likely not a bug in the program; it indicates that the program
performed an operation that Miri does not support
Miri 不仅可以为您的主机目标运行二进制或测试套件,还可以对任意外部目标执行交叉解释: cargo miri run --target x86_64-unknown-linux-gnu
将像 Linux 一样运行您的程序程序,无论您的主机操作系统如何。如果您使用 Windows,这尤其有用,因为 Linux 目标比 Windows 目标得到更好的支持。
您还可以使用它来测试与主机平台具有不同属性的平台。例如, cargo miri test --target s390x-unknown-linux-gnu
将在大端目标上运行测试套件,这对于测试端敏感代码非常有用。
执行的某些部分是由 Miri 随机选择的,例如存储的确切基地址分配以及并发执行线程的交错。有时,探索多个不同的执行可能很有用,例如,确保您的代码不依赖于新分配的偶然“超级对齐”并测试不同的线程交错。这可以通过--many-seeds
标志来完成:
cargo miri test --many-seeds # tries the seeds in 0..64
cargo miri test --many-seeds=0..16
默认的 64 个不同种子相当慢,因此您可能需要指定较小的范围。
在 CI 上运行 Miri 时,使用以下代码片段通过 Miri 组件安装夜间工具链:
rustup toolchain install nightly --component miri
rustup override set nightly
cargo miri test
以下是 GitHub Actions 的示例作业:
miri :
name : " Miri "
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
- name : Install Miri
run : |
rustup toolchain install nightly --component miri
rustup override set nightly
cargo miri setup
- name : Test with Miri
run : cargo miri test
显式的cargo miri setup
有助于保持实际测试步骤的输出干净。
Miri 不支持 Rust 支持的所有目标。然而,好消息是,无论您的主机操作系统/平台如何,都可以轻松使用--target
为任何目标运行代码!
以下目标在 CI 上进行了测试,因此应该始终有效(达到下面记录的程度):
s390x-unknown-linux-gnu
作为我们的“选择的大端目标”。linux
、 macos
或windows
所有其他目标,Miri 通常应该可以工作,但我们不做出任何承诺,也不对此类目标运行测试。solaris
/ illumos
:由@devnexen 维护。支持std::{env, thread, sync}
,但不支持std::fs
。freebsd
:需要维护者。支持std::env
和部分std::{thread, fs}
,但不支持std::sync
。android
:需要维护者。支持非常不完整,但基本的“hello world”可以工作。wasi
:需要维护者。支持非常不完整,甚至标准输出都不起作用,但是一个空的main
函数可以起作用。main
函数之前就失败。然而,即使对于我们支持的目标,不同目标对访问平台 API(例如文件系统)的支持程度也有所不同:一般来说,Linux 目标具有最好的支持,macOS 目标通常处于同等水平。 Windows 的支持不太好。
尽管它实现了 Rust 线程,但 Miri 本身是一个单线程解释器。这意味着,在运行cargo miri test
时,由于固有的解释器速度减慢和并行性损失,您可能会发现运行整个测试套件所需的时间急剧增加。
您可以通过运行cargo miri nextest run -jN
来恢复测试套件的并行性(请注意,您需要安装cargo-nextest
)。这是有效的,因为cargo-nextest
收集所有测试的列表,然后为每个测试启动单独的cargo miri run
。您需要指定-j
或--test-threads
;默认情况下, cargo miri nextest run
一次运行一个测试。有关更多详细信息,请参阅cargo-nextest
Miri 文档。
注意:这种每个进程一个测试的模型意味着, cargo miri test
能够检测到两个测试在共享资源上竞争的数据竞争,但cargo miri nextest run
不会检测到此类竞争。
注意: cargo-nextest
不支持 doctests,请参阅 nextest-rs/nextest#16
使用上述说明时,您可能会遇到许多令人困惑的编译器错误。
RUST_BACKTRACE=1
环境变量运行以显示回溯”当尝试让 Miri 显示回溯时,您可能会看到这一点。默认情况下,Miri 不会向程序公开任何环境,因此运行RUST_BACKTRACE=1 cargo miri test
不会执行您期望的操作。
要获得回溯,您需要使用-Zmiri-disable-isolation
禁用隔离:
RUST_BACKTRACE=1 MIRIFLAGS= " -Zmiri-disable-isolation " cargo miri test
std
由不兼容版本的 rustc 编译”您可能正在使用与用于构建 Miri 使用的自定义 libstd 的编译器版本不同的编译器版本来运行cargo miri
,而 Miri 无法检测到这一点。尝试运行cargo miri clean
。
-Z
标志和环境变量Miri 添加了自己的一组-Z
标志,这些标志通常通过MIRIFLAGS
环境变量设置。我们首先记录最相关和最常用的标志:
-Zmiri-address-reuse-rate=
更改将释放的非堆栈分配添加到池中以进行地址重用的概率,以及从池中获取新的非堆栈分配的概率。堆栈分配永远不会添加到池中或从池中取出。默认值为0.5
。-Zmiri-address-reuse-cross-thread-rate=
更改尝试重用先前释放的内存块的分配也会考虑其他线程释放的块的概率。默认值为0.1
,这意味着默认情况下,在 90% 尝试重用地址的情况下,只会考虑来自同一线程的地址。重用另一个线程的地址会导致这些线程之间的同步,这可能会掩盖数据争用和弱内存错误。-Zmiri-compare-exchange-weak-failure-rate=
更改compare_exchange_weak
操作的失败率。默认值为0.8
(因此 5 个弱操作中有 4 个会失败)。您可以将其更改为0.0
和1.0
之间的任何值,其中1.0
表示它始终会失败, 0.0
表示它永远不会失败。请注意,将其设置为1.0
可能会导致挂起,因为这意味着使用compare_exchange_weak
的程序无法取得进展。-Zmiri-disable-isolation
禁用主机隔离。因此,程序可以访问主机资源,例如环境变量、文件系统和随机性。-Zmiri-disable-leak-backtraces
禁用内存泄漏的回溯报告。默认情况下,创建每个分配时都会捕获回溯,以防泄漏。这会产生一些内存开销来存储几乎从未使用过的数据。该标志由-Zmiri-ignore-leaks
暗示。-Zmiri-env-forward=
将var
环境变量转发到解释程序。可以多次使用来转发多个变量。如果转发变量的值保持不变,执行仍然是确定性的。如果设置了-Zmiri-disable-isolation
则无效。-Zmiri-env-set==
将var
环境变量设置为解释程序中的value
。它可用于传递环境变量,而无需更改主机环境。它可以多次使用来设置多个变量。如果设置了-Zmiri-disable-isolation
或-Zmiri-env-forward
,则使用此选项设置的值将优先于主机环境中的值。-Zmiri-ignore-leaks
禁用内存泄漏检查器,并且还允许主线程退出时一些剩余线程存在。-Zmiri-isolation-error=
配置 Miri 在启用隔离时对需要主机访问的操作的响应。 abort
、 hide
、 warn
和warn-nobacktrace
是受支持的操作。默认是abort
,这会停止机器。某些(但不是全部)操作还支持继续执行,并向程序返回“权限被拒绝”错误。 warn
每次发生时都会打印完整的回溯; warn-nobacktrace
不太详细,每次操作最多显示一次。 hide
完全隐藏警告。-Zmiri-num-cpus
表示 miri 报告的可用 CPU 数量。默认情况下,可用 CPU 数量为1
。请注意,此标志不会影响 miri 以任何方式处理线程的方式。-Zmiri-permissive-provenance
禁用整数到指针转换和ptr::with_exposed_provenance
的警告。这必然会错过一些错误,因为这些操作在清理程序中无法有效且准确地实现,但它只会错过与受这些操作影响的内存/指针有关的错误。-Zmiri-preemption-rate
配置在基本块结束时,活动线程被抢占的概率。默认值为0.01
(即 1%)。将其设置为0
将禁用抢占。-Zmiri-report-progress
使 Miri 时不时地打印当前的堆栈跟踪,这样您就可以知道当程序继续运行时它正在做什么。您可以通过-Zmiri-report-progress=
自定义打印报告的频率,每 N 个基本块打印一次报告。-Zmiri-seed=
配置 Miri 用于解决非确定性问题的 RNG 种子。该 RNG 用于选择分配的基地址,确定compare_exchange_weak
的抢占和失败,以及控制弱内存模拟的存储缓冲。当启用隔离(默认)时,这也用于模拟系统熵。默认种子为 0。您可以通过使用不同种子多次运行 Miri 来增加测试覆盖率。-Zmiri-strict-provenance
可在美里进行严格的出处检查。这意味着将整数转换为指针会产生出处“无效”的结果,即出处不能用于任何内存访问。-Zmiri-symbolic-alignment-check
使对齐检查更加严格。默认情况下,通过将指针转换为整数并确保它是对齐方式的倍数来检查对齐方式。这可能会导致程序纯粹偶然通过对齐检查,因为事情“碰巧”充分对齐——此执行中没有 UB,但其他执行中会有 UB。为了避免这种情况,符号对齐检查仅考虑相关分配的请求对齐以及该分配的偏移量。这可以避免遗漏此类错误,但当代码执行手动整数运算以确保对齐时,也会产生一些误报。 (标准库的align_to
方法在两种模式下都可以正常工作;在符号对齐下,当分配保证足够的对齐时,它仅填充中间切片。)其余标志仅供高级使用,并且更有可能更改或删除。其中一些是不健全的,这意味着它们可能导致 Miri 无法检测程序中未定义行为的情况。
-Zmiri-disable-alignment-check
禁用检查指针对齐,因此您可以专注于其他故障,但这意味着 Miri 可能会错过程序中的错误。使用这个标志是不合理的。-Zmiri-disable-data-race-detector
禁用数据竞争检查。使用这个标志是不合理的。这意味着-Zmiri-disable-weak-memory-emulation
。-Zmiri-disable-stacked-borrows
禁用检查实验别名规则以跟踪借用(堆叠借用和树借用)。这可以使 Miri 运行得更快,但这也意味着不会检测到别名冲突。使用此标志是不合理的(但受影响的健全性规则是实验性的)。后面的标志优先:可以通过-Zmiri-tree-borrows
重新激活借用跟踪。-Zmiri-disable-validation
禁用强制执行默认情况下强制执行的有效性不变量。这对于首先关注其他故障(例如越界访问)非常有用。设置此标志意味着 Miri 可以错过程序中的错误。然而,这也有助于让 Miri 跑得更快。使用这个标志是不合理的。-Zmiri-disable-weak-memory-emulation
禁用某些 C++11 弱内存效应的模拟。-Zmiri-native-lib=
是一个实验性标志,用于为通过 FFI 从解释器内部调用本机函数提供支持。该文件未提供的功能仍通过常用的 Miri 垫片执行。警告:如果指定了无效/不正确的.so
文件,这可能会导致 Miri 本身出现未定义的行为!当然,Miri 无法对本机代码所采取的操作进行任何检查。请注意,Miri 有自己的文件描述符处理方式,因此如果您想替换某些处理文件描述符的函数,则必须替换所有函数,否则两种文件描述符将会混淆。这是正在进行中的工作;目前,仅支持整数参数和返回值(不,用于解决此限制的指针/整数强制转换将不起作用;它们将严重失败)。目前它也仅适用于 Unix 主机。-Zmiri-measureme=
启用解释程序的measureme
分析。这可用于查找程序的哪些部分在 Miri 下执行缓慢。配置文件被写入名为
的目录中的文件,并且可以使用存储库 https://github.com/rust-lang/measureme 中的工具进行处理。-Zmiri-mute-stdout-stderr
默默地忽略对 stdout 和 stderr 的所有写入,但向程序报告它实际写入的情况。当您对实际程序的输出不感兴趣而只想查看 Miri 的错误和警告时,这非常有用。-Zmiri-recursive-validation
是一个高度实验性的标志,使有效性检查在引用下面递归。-Zmiri-retag-fields[=]
控制何时堆叠借用重新标记递归到字段中。 all
意味着它总是递归(默认值,相当于没有显式值的-Zmiri-retag-fields
), none
意味着它从不递归, scalar
意味着它只对我们也会在生成的 LLVM IR 中发出noalias
注释的类型进行递归(类型作为单个标量或标量对传递)。将其设置为none
是不合理的。-Zmiri-provenance-gc=
配置指针来源垃圾收集器运行的频率。默认是每10000
个基本块搜索并删除一次无法到达的来源。将其设置为0
会禁用垃圾收集器,这会导致某些程序出现爆炸性内存使用和/或超线性运行时。-Zmiri-track-alloc-accesses
不仅显示跟踪分配的分配和空闲事件,还显示读取和写入。-Zmiri-track-alloc-id=,,...
显示分配或释放给定分配时的回溯。这有助于调试内存泄漏和释放后使用错误。多次指定此参数不会覆盖以前的值,而是将其值附加到列表中。多次列出一个 id 没有效果。-Zmiri-track-pointer-tag=,,...
当创建给定的指针标签时以及当(如果有的话)从借用堆栈(这是标签变成的位置)弹出它时显示回溯无效,以后使用它将会出错)。这可以帮助您找出发生 UB 的原因以及代码中的何处是查找它的好地方。多次指定此参数不会覆盖以前的值,而是将其值附加到列表中。多次列出一个标签没有任何效果。-Zmiri-track-weak-memory-loads
当弱内存模拟从加载返回过时的值时显示回溯。这可以帮助诊断在-Zmiri-disable-weak-memory-emulation
下消失的问题。-Zmiri-tree-borrows
用树借用规则替换堆叠借用规则。树形借用比堆叠借用更具实验性。虽然 Tree Borrows 在捕获当前版本的编译器可能利用的所有别名违规方面仍然是合理的,但 Rust 的最终别名模型很可能会比 Tree Borrows 更严格。换句话说,如果您使用 Tree Borrows,即使您的代码今天被接受,将来也可能会被声明为 UB。对于堆叠借用来说,这种情况的可能性要小得多。-Zmiri-force-page-size=
覆盖架构的默认页面大小,以 1k 的倍数表示。 4
是大多数目标的默认值。该值应始终为 2 的幂且非零。-Zmiri-unique-is-unique
对core::ptr::Unique
执行额外的别名检查,以确保理论上它可以被视为noalias
。该标志是实验性的,仅在与-Zmiri-tree-borrows
一起使用时才有效。一些本机 rustc -Z
标志也与 Miri 非常相关:
-Zmir-opt-level
控制执行多少次 MIR 优化。 Miri 覆盖默认值0
;请注意,使用任何更高的级别都可能使 Miri 错过程序中的错误,因为它们已被优化掉。-Zalways-encode-mir
使 rustc 转储 MIR,甚至对于完全单态的函数也是如此。这是 Miri 可以执行此类函数所必需的,因此 Miri 默认设置此标志。-Zmir-emit-retag
控制是否发出Retag
语句。 Miri 默认启用此功能,因为堆叠借用和树借用需要它。此外,Miri 还可以识别一些环境变量:
MIRIFLAGS
定义了传递给 Miri 的额外标志。MIRI_LIB_SRC
定义 Miri 期望构建并用于解释的标准库源的目录。该目录必须指向rust-lang/rust
存储库签出的library
子目录。MIRI_SYSROOT
指示要使用的 sysroot。当使用cargo miri test
/ cargo miri run
时,这会跳过自动设置 - 仅当您不想使用自动创建的 sysroot 时才设置此项。当调用cargo miri setup
时,这指示 sysroot 将放置的位置。MIRI_NO_STD
确保目标的 sysroot 是在没有 libstd 的情况下构建的。这允许测试和运行 no_std 程序。通常不应使用此方法; Miri 有一种启发式方法,可以根据目标名称检测非标准目标。在支持 libstd 的目标上设置此选项可能会导致令人困惑的结果。 extern
函数Miri 提供了一些extern
函数,程序可以导入这些函数来访问 Miri 特定的功能。它们在 /tests/utils/miri_extern.rs 中声明。
不使用标准库的二进制文件应该声明这样的函数,以便 Miri 知道它应该从哪里开始执行:
# [ cfg ( miri ) ]
# [ no_mangle ]
fn miri_start ( argc : isize , argv : * const * const u8 ) -> isize {
// Call the actual start function that your project implements, based on your target's conventions.
}
如果你想为美里做出贡献,那就太好了!请查看我们的贡献指南。
如需运行 Miri 的帮助,您可以在 GitHub 上提出问题或使用 Rust Zulip 上的 Miri 流。
该项目始于 2015 年萨斯喀彻温大学@solson 本科生研究课程的一部分。该项目提供了幻灯片和报告。 2016 年,@oli-obk 加入准备 Miri 最终在 Rust 编译器本身中用作 const 求值器(基本上,用于const
和static
内容),取代直接在 AST 上工作的旧求值器。 2017 年,@RalfJung 在 Mozilla 实习,开始开发 Miri 作为检测未定义行为的工具,并使用 Miri 来探索 Rust 中未定义行为的各种可能定义的后果。 @oli-obk 将 Miri 引擎移入编译器的工作终于在 2018 年初完成。同时,同年晚些时候,@RalfJung 进行了第二次实习,进一步开发 Miri,支持检查基本类型不变量并验证引用是否按照他们的别名限制。
Miri 已经在 Rust 标准库及其他版本中发现了许多错误,我们在这里收集了其中一些错误。如果 Miri 帮助您在代码中发现了一个微妙的 UB bug,我们将不胜感激将其添加到列表中的 PR!
发现的明确错误:
Debug for vec_deque::Iter
访问未初始化的内存Vec::into_iter
执行未对齐的 ZST 读取From<&[T]> for Rc
创建未充分对齐的引用BTreeMap
创建一个指向太小的分配的共享引用Vec::append
创建悬空引用str
将共享引用转换为可变引用rand
执行未对齐读取posix_memalign
getrandom
以无效方式调用getrandom
系统调用Vec
和BTreeMap
在某些(恐慌)条件下泄漏内存beef
泄漏内存EbrCell
错误地使用未初始化的内存servo_arc
创建悬空共享引用encoding_rs
进行越界指针算术Vec::from_raw_parts
AtomicPtr
和Box::from_raw_in
的文档测试不正确ThinVec
中的对齐不足crossbeam-epoch
在部分初始化的MaybeUninit
上调用假设assume_init
integer-encoding
取消引用未对齐的指针rkyv
从过度对齐的分配构造Box<[u8]>
arc-swap
中的数据竞争thread::scope
中的数据竞争regex
错误地处理未对齐的Vec
缓冲区once_cell
中错误使用compare_exchange_weak
vec::IntoIter
中删除未对齐的指针Iterator::collect
新专业化中处理错误的布局portable-atomic-util
中高度对齐类型的偏移计算不正确std::mpsc
通道中偶尔发生内存泄漏(crossbeam 中的原始代码)违反 Stacked Borrows 发现可能是错误(但 Stacked Borrows 目前只是一个实验):
VecDeque::drain
创建重叠的可变引用BTreeMap
问题BTreeMap
迭代器创建与共享引用重叠的可变引用BTreeMap::iter_mut
创建重叠的可变引用BTreeMap
节点LinkedList
游标插入创建重叠的可变引用Vec::push
使向量中的现有引用无效align_to_mut
违反可变引用的唯一性sized-chunks
创建别名可变引用String::push_str
使字符串中的现有引用无效ryu
在其有效内存区域之外使用原始指针Env
迭代器在其有效内存区域之外使用原始指针VecDeque::iter_mut
创建重叠的可变引用<[T]>::copy_within
在无效贷款后使用贷款已获得以下任一许可
由您选择。
除非您另有明确说明,否则您有意提交的包含在作品中的任何贡献均应获得上述双重许可,无任何附加条款或条件。