Detecting undefined behaviour in Rust code using Miri

Published on: 2020-11-08

Home | Archive

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