Rust et Miri : au-delà du compilateur pour la sécurité mémoire

En Rust, le compilateur est votre première ligne de défense. Ownership, borrowing et lifetimes empêchent la plupart des erreurs mémoire à la compilation. Mais certaines erreurs peuvent encore passer entre les mailles du filet, notamment avec unsafe ou les pointeurs bruts. C'est là que Miri entre en jeu.

Le compilateur Rust ne suffit pas toujours

Rust est réputé pour sa sécurité mémoire, et à juste titre. Le système de types, les règles d'ownership et le borrow checker forment un filet de sécurité remarquablement efficace. Pourtant, dès qu'on utilise le mot-clé unsafe, on entre dans un territoire où le compilateur ne peut plus tout vérifier.

Les blocs unsafe permettent des opérations que le compilateur ne peut pas prouver comme sûres : déréférencement de pointeurs bruts, appels à des fonctions C via FFI, manipulation directe de la mémoire. Ces opérations sont parfois indispensables dans du code système ou des bibliothèques de bas niveau, mais elles ouvrent la porte à des comportements indéfinis (Undefined Behavior, ou UB).

Le problème : rustc compilera votre code sans broncher, même si un comportement indéfini s'y cache.

Miri : un interpréteur pour traquer les comportements indéfinis

Miri est un interpréteur Rust qui exécute une représentation interne du compilateur appelée MIR (Mid-level Intermediate Representation). Cette représentation est celle que le compilateur utilise pour le borrow checker et les optimisations.

Au lieu d'exécuter votre programme nativement, Miri le simule instruction par instruction, en vérifiant à chaque étape que les règles de la mémoire sont respectées. Il peut ainsi détecter des UB que le compilateur ne peut pas vérifier statiquement :

Exemple concret

Considérons ce code Rust :

let s = String::from("hello");
let r: *const String = &s;
unsafe {
    drop(s);           // Rust ne voit pas que r devient dangling
    println!("{}", &*r); // UB : use-after-free
}

Ici, on crée une String, on prend un pointeur brut vers elle, puis on la détruit avec drop. Le pointeur r devient un dangling pointer. En essayant de le déréférencer, on accède à de la mémoire libérée.

Quand utiliser Miri

Miri est particulièrement utile dans les situations suivantes :

Pour l'utiliser, il suffit d'installer le composant via rustup et de lancer :

cargo miri run
# ou pour les tests
cargo miri test

Limites de Miri

Miri n'est pas une solution universelle. Il ne peut pas exécuter d'appels système réels ni de code FFI vers du C. Son exécution est aussi significativement plus lente que l'exécution native, puisqu'il simule chaque instruction. Il est donc complémentaire aux tests classiques, pas un remplacement.

Conclusion

Même dans un langage memory-safe comme Rust, l'analyse dynamique avec Miri est essentielle dès qu'on utilise unsafe. Le compilateur garantit la sécurité du code safe, mais Miri étend cette garantie aux zones où le compilateur ne peut pas aller. Pour tout projet sérieux ou toute bibliothèque qui touche à du code unsafe, Miri représente une couche de vérification indispensable qui permet d'attraper des bugs avant qu'ils n'atteignent la production.