Aller au contenu

Unsafe en Rust

Toutes les garanties dont ce cours a parlé (pas de références pendantes, pas de data races, pas d’accès hors limites) sont imposées par le compilateur. unsafe est la façon de dire au compilateur : « laisse-moi faire quelque chose dont tu ne peux pas vérifier la sûreté ; c’est moi qui en prends la responsabilité ».

On croit souvent que unsafe désactive complètement les vérifications de sécurité de Rust. Ce n’est pas le cas. Dans un bloc unsafe, vous pouvez faire exactement cinq choses supplémentaires, interdites le reste du temps :

  1. Déréférencer un pointeur brut
  2. Appeler une fonction unsafe
  3. Lire ou modifier une variable static mutable
  4. Implémenter un trait unsafe
  5. Accéder aux champs d’une union

Tout le reste, les règles du borrowing (emprunt), la sémantique de déplacement, la vérification des types, s’applique toujours pleinement dans le code unsafe. C’est un scalpel pour cinq opérations précises, pas un interrupteur qui coupe tout.

Les références normales de Rust (& et &mut) pointent toujours vers des données valides et correctement alignées : cela fait partie de ce que garantit le compilateur. Un pointeur brut (raw pointer, *const T ou *mut T) abandonne cette garantie, et c’est pourquoi le lire demande unsafe :

fn main() {
let value = 5;
let pointer = &value as *const i32;
unsafe {
println!("{}", *pointer);
}
}

Sortie :

5
▶ Essayez par vous-même

Vous pouvez créer un pointeur brut dans du code sûr. Seul le fait de le déréférencer (lire la valeur vers laquelle il pointe) demande unsafe, car le compilateur ne peut plus vérifier que le pointeur est valide.

Certaines fonctions sont elles-mêmes marquées unsafe, parce qu’elles ont des conditions que le compilateur ne peut pas vérifier à votre place. Les appeler, c’est promettre que vous avez vérifié ces conditions vous-même :

unsafe fn dangerous() {
println!("cette fonction compte sur vous pour avoir vérifié ses conditions");
}
fn main() {
unsafe {
dangerous();
}
}

Sortie :

cette fonction compte sur vous pour avoir vérifié ses conditions
▶ Essayez par vous-même

La bibliothèque standard contient de vraies fonctions unsafe que vous pouvez appeler. L’une d’elles est get_unchecked, sur les slices. Elle fonctionne comme numbers[i], mais saute la vérification des limites. Si i est hors limites, elle ne panique pas : elle lit ce qui se trouve en mémoire à cet endroit, ce qu’on appelle un comportement indéfini (undefined behavior). Votre programme peut planter, afficher n’importe quoi, ou sembler fonctionner… jusqu’au jour où il ne fonctionne plus.

La règle pour l’appeler est donc « uniquement avec un indice dont vous savez qu’il est valide ». La méthode habituelle consiste à vérifier cette règle vous-même, à l’intérieur d’une fonction normale et sûre :

fn first_or_zero(numbers: &[i32]) -> i32 {
if numbers.is_empty() {
return 0;
}
// SAFETY: la slice n'est pas vide, donc l'indice 0 est dans les limites.
unsafe { *numbers.get_unchecked(0) }
}
fn main() {
println!("{}", first_or_zero(&[7, 8, 9]));
println!("{}", first_or_zero(&[]));
}

Sortie :

7
0
▶ Essayez par vous-même

Regardez ce que voit l’appelant : fn first_or_zero(numbers: &[i32]) -> i32. Il n’y a pas d’unsafe dans la signature, et aucun moyen de mal l’appeler, car la fonction vérifie la règle avant même d’atteindre le bloc unsafe. Le code unsafe reste petit et caché à l’intérieur.

Deux habitudes que suivent les développeurs Rust expérimentés :

  • Écrire un commentaire // SAFETY: au-dessus de chaque bloc unsafe, pour expliquer pourquoi les règles sont respectées. C’est pour la prochaine personne qui lira le code, et pour vous dans le futur. Le mot SAFETY: reste en anglais : c’est une convention que reconnaissent les outils comme Clippy.
  • Garder le bloc unsafe aussi petit que possible, pour avoir moins de code à revérifier.

Pour être honnête, vous n’écririez pas cette fonction telle quelle dans du vrai code : numbers.first() fait la même chose de façon sûre, et le compilateur est très doué pour supprimer les vérifications de limites dont il peut prouver l’inutilité. Mais le principe (vérifier les règles, puis appeler du code unsafe à l’intérieur d’une fonction sûre) est exactement celui qui sert à construire Vec, String et compagnie. La page FFI : appeler du C utilise la même astuce pour envelopper des fonctions C.

Si Rust tient tant à la sécurité, pourquoi autoriser ça ? Parce que certaines choses sont vraiment impossibles à prouver sûres à la compilation, alors qu’elles sont parfaitement sûres en pratique : dialoguer directement avec le matériel, appeler une bibliothèque C (voir FFI : appeler du C), ou construire certaines structures de données très performantes. unsafe permet à la bibliothèque standard (et, rarement, à votre propre code) d’implémenter ces briques de bas niveau, tandis que tout ce qui est construit par-dessus reste du Rust sûr ordinaire, entièrement vérifié.

Écrire du code unsafe ne veut pas dire que votre code est désormais bogué. Cela veut dire que le compilateur ne peut plus détecter certaines catégories d’erreurs dans ce bloc précis, et que c’est à vous de maintenir les mêmes garanties à la main. La plupart des développeurs Rust expérimentés écrivent très peu de code unsafe directement ; il se trouve généralement dans de petits recoins soigneusement relus, tout en bas d’une bibliothèque, enveloppé dans une API sûre que tout le monde peut utiliser.

1. Modifier une valeur à travers un pointeur brut

Section intitulée « 1. Modifier une valeur à travers un pointeur brut »

Créez un let mut score = 10;, créez un pointeur brut *mut i32 vers cette variable, et utilisez le pointeur pour ajouter 5. Ensuite, affichez score. Indice : créez le pointeur avec &mut score as *mut i32, et écrivez *pointer += 5 dans un bloc unsafe.

Voir la solution
fn main() {
let mut score = 10;
let pointer = &mut score as *mut i32; // le créer est sûr
unsafe {
*pointer += 5; // écrire à travers lui ne l'est pas
}
println!("score = {score}");
}

Sortie :

score = 15
▶ Essayez par vous-même

C’est sûr en pratique, car score est toujours vivante et rien d’autre ne l’utilise pendant qu’on écrit. Tenir ces promesses devient votre travail dès que vous utilisez unsafe.

Écrivez fn get_or(numbers: &[i32], index: usize, default: i32) -> i32. Elle renvoie l’élément situé à index, ou default si l’indice est hors limites. Utilisez get_unchecked pour la lecture, et assurez-vous que la fonction ne puisse jamais être appelée d’une façon qui enfreint sa règle. Indice : comparez d’abord index < numbers.len(), et n’appelez get_unchecked que si c’est vrai.

Voir la solution
fn get_or(numbers: &[i32], index: usize, default: i32) -> i32 {
if index < numbers.len() {
// SAFETY: on vient de vérifier que index est dans les limites.
unsafe { *numbers.get_unchecked(index) }
} else {
default
}
}
fn main() {
let numbers = [10, 20, 30];
println!("{}", get_or(&numbers, 1, -1));
println!("{}", get_or(&numbers, 5, -1));
}

Sortie :

20
-1
▶ Essayez par vous-même

L’indice 5 n’atteint jamais le bloc unsafe : il est donc traité de façon sûre. À titre de comparaison, la version entièrement sûre est numbers.get(index).copied().unwrap_or(default), et c’est ce que vous écririez normalement.

  • unsafe débloque exactement cinq opérations (surtout les pointeurs bruts et l’appel de fonctions unsafe) : tout le reste est toujours entièrement vérifié.
  • Un pointeur brut (*const T / *mut T) peut être créé dans du code sûr ; le déréférencer demande unsafe.
  • Enveloppez unsafe dans une petite fonction sûre qui vérifie d’abord les règles, et expliquez pourquoi c’est correct dans un commentaire // SAFETY:.
  • unsafe existe pour qu’on puisse construire du code de bas niveau (accès au matériel, interopérabilité avec le C), tandis que le code qui l’utilise reste sûr.
  • En tant que débutant, vous aurez rarement besoin d’en écrire vous-même : cherchez d’abord une abstraction sûre.

Petit quiz

1. Que débloque réellement un bloc unsafe ?

2. Peut-on créer un pointeur brut dans du code sûr ?

3. En tant que débutant, à quelle fréquence devriez-vous écrire unsafe vous-même ?

4. Écrire du code unsafe signifie-t-il que votre programme contient désormais des bugs ?

Score : 0 / 4