Detecting undefined behaviour in Rust code using Miri
There was an excellent talk at RustFest 2020 on detecting undefined behaviours using Miri. After watching it, I tried out a few small experiments; these might be of interest to people who do low level stuff in Rust.
Miri is an experimental interpreter for Rust’s MIR (mid level intermediate representation); it is capable of detecting some kinds (not all) of undefined behaviour. Check out this list of real-world bug’s discovered by Miri.
Install Miri
rustup toolchain install nightly
rustup +nightly component add miri
Hello, world!
rustup default nightly
cargo new miri-hello; cd miri-hello
cargo miri run
Dangling pointers!
A normal “cargo run” will happily run the following program:
fn main() {
unsafe {
let y: *const i32;
{
let x = 10;
y = &x;
}
println!("{}", *y);
}
}
Let’s try with cargo miri run:
error: Undefined Behavior: pointer to alloc1811 was dereferenced after this allocation got freed
--> src/main.rs:8:24
|
8 | println!("{}", *y);
| ^^ pointer to alloc1811 was dereferenced after this allocation got freed
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
Producing invalid values
Let’s try to store a value that is not 0 or 1 into an object of type bool (this is UB in Rust):
#[repr(C)]
union Foo {
x: bool,
y: u8,
}
fn main() {
unsafe {
let mut f = Foo{x: false};
f.y = 0xff;
println!("{}", f.x);
}
}
Our usual cargo run will happily run this program (and may even print true). Let’s check the output of cargo miri run:
error: Undefined Behavior: type validation failed: encountered 0xff, but expected a boolean
--> /home/pramode/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/fmt/mod.rs:2017:25
|
2017 | Display::fmt(if *self { "true" } else { "false" }, f)
| ^^^^^ type validation failed: encountered 0xff, but expected a boolean
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
Accessing misaligned data
This one is surprising, try doing a cargo run:
#[repr(packed)]
struct Foo {
x: u8,
y: u32,
}
fn main() {
let f = Foo{x:1, y:1};
println!("{}", f.y);
}
Because we have a packed struct here, f.y is not aligned, and accessing it is undefined behaviour. Surprisingly, safe Rust lets you do it, with a warning that this is dangerous and it will become a hard error in the future!
Here is the output of cargo run:
warning: borrow of packed field is unsafe and requires unsafe function or block (error E0133)
--> src/main.rs:9:20
|
9 | println!("{}", f.y);
| ^^^
|
= note: `#[warn(safe_packed_borrows)]` on by default
= warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
= note: for more information, see issue #46043 <https://github.com/rust-lang/rust/issues/46043>
= note: fields of packed structs might be misaligned: dereferencing a misaligned pointer or even just creating a misaligned reference is undefined behavior
And here is the output of cargo miri run:
error: Undefined Behavior: type validation failed: encountered an unaligned reference (required 4 byte alignment but found 1)
--> src/main.rs:9:20
|
9 | println!("{}", f.y);
| ^^^ type validation failed: encountered an unaligned reference (required 4 byte alignment but found 1)
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
Buffer out-of-bound access
cargo run builds and runs the following program happily:
use std::alloc::{alloc, dealloc, Layout};
fn main() {
unsafe {
// An array of 100 bytes.
let layout = Layout::from_size_align_unchecked(100, 1);
let ptr = alloc(layout);
*((ptr as usize + 100) as *mut u8) = 1;
dealloc(ptr, layout);
}
}
But cargo miri run detects the out-of-bound access:
error: Undefined Behavior: memory access failed: pointer must be in-bounds at offset 101, but is outside bounds of alloc1814 which has size 100
--> src/main.rs:9:9
|
9 | *((ptr as usize + 100) as *mut u8) = 1;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ memory access failed: pointer must be in-bounds at offset 101, but is outside bounds of alloc1814 which has size 100
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information