Miri & Valgrind

Links:

Miri - это интерпретатор, который запускает код внутри особой безопасной песочницы. Miri даже не компилирует твою программу в реальный код. Он берёт твой Rust код и интерпретирует, проверяя:

  • Правила заимствования (одна ссылка на запись или много на чтение)
  • Выход за границы массива
  • Использование после перемещения Miri понимает Rust: он знает, что такое &mut и почему два &mut на один объект — это плохо. Valgrind этого не поймёт. Miri замедляет работу кода в 100-1000х раз.

Valgrind запускает уже готовую скомпилированную программу и следит за ней со стороны:

  • Куда она обращается к памяти
  • Не забыла ли освободить то, что взяла
  • Не читает ли то, что уже удалила Valgrind не знает правил Rust: он видит просто машинный код. Поэтому он может найти многие ошибки, но не понимает высокоуровневые концепции вроде заимствования или времени жизни. При этом Valgrind поддерживает проекты с миксом из Rust и C++ и т.д. языками. Valgrind замедляет работу кода в 20-50х раз.

Установка и запуск

Miri работает только в Nightly-версии Rust:

rustup toolchain install nightly
rustup +nightly component add miri
cargo +nightly miri test


valgrind ./my_program  # программу уже скомпилировали

Проверка

Возьмём код с проблемами:

use std::alloc::{alloc, dealloc, Layout};

// BUG 1: Use-after-free (we'll read memory after freeing it)
fn use_after_free() -> u8 {
    let layout = Layout::new::<u8>();
    let ptr = unsafe { alloc(layout) };

    // Write something
    unsafe {
        *ptr = 42;
    }

    // Free the memory
    unsafe {
        dealloc(ptr, layout);
    }

    // BUG: Reading after free! This is undefined behavior.
    let value = unsafe { *ptr };
    value
}

// BUG 2: Memory leak (allocate but never free)
fn memory_leak() -> *mut u8 {
    let layout = Layout::new::<u8>();
    let ptr = unsafe { alloc(layout) };
    unsafe {
        *ptr = 99;
    }

    // BUG: We never call dealloc! The memory leaks.
    ptr
}

// BUG 3: Miri-specific - violating Rust's aliasing rules
fn aliasing_violation() {
    let mut x = 5;
    let raw_ptr = &mut x as *mut i32;

    // Create a second mutable pointer to the same memory
    let raw_ptr2 = &mut x as *mut i32;

    unsafe {
        // Miri catches this: two mutable references to same data in scope
        *raw_ptr = 10;
        *raw_ptr2 = 20; // Undefined behavior per Rust's Stacked Borrows rules
    }
}

fn main() {
    println!("Testing use-after-free...");
    let val = use_after_free();
    println!("Read value: {} (this might be garbage)", val);

    println!("\nLeaking memory...");
    let _leaked = memory_leak();
    println!("Memory leaked successfully!");

    println!("\nViolating aliasing rules...");
    aliasing_violation();
    println!("Aliasing violation completed (somehow)");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_use_after_free() {
        let val = use_after_free();
        // We're just calling it - the bug is inside
        assert!(val == 42 || val != 42); // Always passes, but the UB is the problem
    }

    #[test]
    fn test_aliasing_violation() {
        aliasing_violation(); // Miri should catch this
    }
}

При запуске cargo test ошибок не будет -> тесты напишут Ок!.

В случае проверки с Miri, инструмент отловит проблемы Bug 1 и Bug 3. Утечки памяти не специализация Miri, может пропустить.

Bug 2 с утечкой памяти отловит Valgrind.

Когда что использовать?

Miri — когда ты пишешь:

  • unsafe код
  • Свои коллекции (Vec, HashMap)
  • FFI обёртки (здесь без unsafe не обойтись)
  • Хочешь поймать тонкие нарушения правил Rust

Valgrind — когда нужно:

  • Найти утечки памяти (Rust их почти не допускает, но C да)
  • Проверить программу на другом языке (C, C++, Fortran)
  • Отловить ошибки в уже готовом бинарнике, когда нет исходников