Rust non sécurisé (unsafe)
Tout le code Rust que nous avons abordé jusqu’à présent a bénéficié des garanties de sécurité de la mémoire, vérifiées à la compilation. Cependant Rust possède un second langage caché en son sein qui n’applique pas ces vérifications de sécurité de la mémoire : il s’appelle le Rust non sécurisé et fonctionne comme le Rust habituel, mais fournit quelques super-pouvoirs supplémentaires.
Le Rust non sécurisé existe car, par nature, l’analyse statique est prudente. Lorsque le compilateur essaye de déterminer si le code respecte ou non les garanties, il vaut mieux rejeter quelques programmes valides plutôt que d’accepter quelques programmes invalides. Bien que le code puisse être correct, si le compilateur Rust n’a pas assez d’information pour être sûr, il va refuser ce code. Dans ce cas, vous pouvez utiliser du code non sécurisé pour dire au compilateur “fais-moi confiance, je sais ce que je fais”. Soyez toutefois averti que si vous utilisez du Rust non sécurisé, c’est à vos risques et périls : si vous écrivez du code non sécurisé de manière incorrecte, des problèmes liés à la sécurité de la mémoire peuvent se produire, tel qu’un déréférencement d’un pointeur vide.
Une autre raison pour laquelle Rust embarque son alter-ego non sécurisé est que le matériel des ordinateurs sur lequel il repose n’est pas sécurisé par essence. Si Rust ne vous laissait pas procéder à des opérations non sécurisées, vous ne pourriez pas faire certaines choses. Rust doit pouvoir vous permettre de développer du code bas-niveau, comme pouvoir interagir directement avec le système d’exploitation ou même écrire votre propre système d’exploitation. Pouvoir travailler avec des systèmes bas-niveau est un des objectifs du langage. Voyons ce que nous pouvons faire avec le Rust non sécurisé et comment le faire.
Mise en pratique des super-pouvoirs du code non sécurisé
Pour pouvoir utiliser le Rust non sécurisé, il faut utiliser le mot-clé unsafe et ensuite créer un nouveau bloc qui contient le code non sécurisé. Vous pouvez faire cinq actions en Rust non sécurisé, actions que vous ne pourriez pas faire en Rust sécurisé que nous appelons les super-pouvoirs du non sécurisé. Ces super-pouvoirs permettent de :
- déréférencer un pointeur brut ;
- faire appel à une fonction ou une méthode non sécurisée ;
- lire ou modifier une variable statique mutable ;
- implémenter un trait non sécurisé ;
- utiliser des champs d’
unions.
Il est important de comprendre que unsafe ne désactive pas le vérificateur d’emprunt et ne désactive pas les autres vérifications de sécurité de Rust : si vous utilisez une référence dans du code non sécurisé, elle sera toujours vérifiée. Le mot-clé unsafe vous donne seulement accès à ces cinq fonctionnalités qui ne sont alors pas vérifiées par le compilateur en vue de veiller à la sécurité de la mémoire. Vous conservez donc un certain niveau de sécurité à l’intérieur d’un bloc unsafe.
De plus, unsafe ne signifie pas que le code à l’intérieur du bloc est obligatoirement dangereux ou qu’il va forcément présenter des problèmes de sécurité mémoire : l’idée étant qu’en tant que développeur, vous vous assuriez que le code à l’intérieur d’un bloc unsafe va accéder correctement à la mémoire.
Personne n’est parfait et les erreurs arrivent, mais en imposant que ces cinq opérations non sécurisées se trouvent dans des blocs marqués d’un unsafe, vous saurez que toutes les éventuelles erreurs liées à la sécurité de la mémoire se trouveront dans un bloc unsafe. Vous devez donc essayer de minimiser la taille des blocs unsafe ; vous ne le regretterez pas lorsque vous rechercherez des bogues de mémoire.
Pour isoler autant que possible le code non sécurisé, il vaut mieux intégrer ce code dans une abstraction et fournir ainsi une API sécurisée, comme nous le verrons plus tard dans ce chapitre lorsque nous examinerons les fonctions et méthodes non sécurisées. Certaines parties de la bibliothèque standard sont implémentées comme étant des abstractions sécurisées et basées sur du code non sécurisé qui a été audité. Encapsuler du code non sécurisé dans une abstraction sécurisée évite que l’utilisation de unsafe ne se propage dans des endroits où vous ou vos utilisateurs souhaiteraient éviter d’utiliser les fonctionnalités du code unsafe, car au final utiliser une abstraction sécurisée doit rester sûr.
Analysons ces cinq super-pouvoirs à tour de rôle. Nous allons aussi découvrir quelques abstractions qui fournissent une interface sécurisée pour faire fonctionner du code non sécurisé.
Déréférencement d’un pointeur brut
Au chapitre 4, dans la section “Références pendouillantes”, nous avions mentionné que le compilateur s’assure que les références sont toujours valides. Le Rust non sécurisé offre deux nouveaux types qui s’appellent les pointeurs bruts et qui ressemblent aux références. Comme les références, les pointeurs bruts peuvent être immuables ou mutables et s’écrivent respectivement *const T et *mut T. L’astérisque n’est pas l’opérateur de déréférencement ; il fait partie du nom du type. Dans un contexte de pointeur brut, immuable signifie que le pointeur ne peut pas être affecté directement après avoir été déréférencé.
À la différence des références et des pointeurs intelligents, les pointeurs bruts :
- peuvent ignorer les règles d’emprunt en ayant plusieurs pointeurs tant immuables que mutables ou en ayant plusieurs pointeurs mutables qui pointent vers le même endroit ;
- ne garantissent pas de pointer sur un emplacement mémoire valide ;
- sont autorisés à être nuls ;
- n’implémentent aucun mécanisme de nettoyage automatique.
En renonçant à ce que Rust fasse respecter ces garanties, vous pouvez sacrifier la sécurité garantie pour obtenir de meilleures performances ou avoir la possibilité de vous interfacer avec un autre langage ou matériel pour lesquels les garanties de Rust ne s’appliquent pas.
L’encart 20-1 montre comment créer un pointeur brut immuable et mutable à partir de références.
fn main() {
let mut nombre = 5;
let r1 = &raw const nombre;
let r2 = &raw mut nombre;
}
Remarquez que nous n’incorporons pas le mot-clé unsafe dans ce code. Nous pouvons créer des pointeurs bruts dans du code sécurisé ; nous ne pouvons simplement pas déréférencer les pointeurs bruts à l’extérieur d’un bloc non sécurisé, comme vous allez le constater d’ici peu.
Nous avons créé des pointeurs bruts en utilisant les opérateurs d’emprunt bruts : &raw const num crée un pointeur brut immuable *const i32, et &raw mut num crée un pointeur brut mutable *mut i32. Comme nous les avons créés directement à partir d’une variable locale, nous savons que ces pointeurs bruts sont valides, mais nous ne pouvons pas faire cette supposition sur n’importe quel pointeur brut.
Pour démontrer ceci, nous allons créer un pointeur brut dont nous ne pouvons pas être vraiment certains de la validité, en utilisant le mot-clé as pour effectuer un transtypage d’une valeur au lieu d’utiliser l’opérateur d’emprunt brut. L’encart 20-2 montre comment créer un pointeur brut vers un emplacement quelconque de la mémoire. Essayer d’utiliser de la mémoire quelconque est indéfini : il peut y avoir des données à cette adresse comme il peut ne pas y en avoir, le compilateur pourrait optimiser le code de telle sorte qu’aucun accès mémoire n’aura lieu, ou bien le programme pourrait déclencher une erreur de segmentation. Habituellement, il n’y a pas de bonne raison d’écrire du code comme celui-ci, mais cela reste possible.
fn main() {
let addresse = 0x012345usize;
let r = addresse as *const i32;
}
Souvenez-vous que nous pouvons créer des pointeurs bruts dans du code sécurisé, mais que nous ne pouvons pas y déréférencer les pointeurs bruts et lire les données sur lesquelles ils pointent. Dans l’encart 20-3, nous utilisons l’opérateur de déréférencement * sur un pointeur brut qui nécessite un bloc unsafe.
fn main() {
let mut nombre = 5;
let r1 = &raw const nombre;
let r2 = &raw mut nombre;
unsafe {
println!("r1 vaut : {}", *r1);
println!("r2 vaut : {}", *r2);
}
}
unsafe blockLa création de pointeur ne pose pas de problèmes ; c’est seulement lorsque nous essayons d’accéder aux valeurs sur lesquelles ils pointent qu’on risque d’obtenir une valeur invalide.
Remarquez aussi que dans les encarts 20-1 et 20-3, nous avons créé les pointeurs bruts *const i32 et *mut i32 qui pointent tous les deux au même endroit de la mémoire, où nombre est stocké. Si nous avions plutôt tenté de créer une référence immuable et une mutable vers nombre, le code n’aurait pas compilé à cause des règles de possession de Rust qui ne permettent pas d’avoir une référence mutable en même temps qu’une ou plusieurs références immuables. Avec les pointeurs bruts, nous pouvons créer un pointeur mutable et un pointeur immuable vers le même endroit et changer la donnée via le pointeur mutable, en risquant un accès concurrent. Soyez vigilant !
Avec tous ces dangers, pourquoi vous risquer à utiliser les pointeurs bruts ? Une des utilisations principale consiste à s’interfacer avec du code C, comme vous allez le découvrir dans la section suivante. Une autre utilisation est de nous permettre de créer une abstraction sécurisée que le vérificateur d’emprunt ne comprend pas. Nous allons découvrir les fonctions non sécurisées puis voir un exemple d’une abstraction sécurisée qui utilise du code non sécurisé.
Faire appel à une fonction ou une méthode non sécurisée
Le deuxième type d’opération qui peut être effectuée dans un bloc unsafe est l’appel à des fonctions non sécurisées. Les fonctions et méthodes non sécurisées ressemblent exactement aux méthodes et fonctions habituelles, mais ont un unsafe en plus devant le reste de leur définition. Le mot-clé unsafe dans ce cas signifie que la fonction a des exigences que nous devons respecter pour pouvoir y faire appel, car Rust ne pourra pas garantir de son côté que nous les ayons remplies. En faisant appel à une fonction non sécurisée dans un bloc unsafe, nous reconnaissons que nous avons lu la documentation de cette fonction et nous prenons la responsabilité de respecter les conditions d’utilisation de la fonction.
Voici une fonction non sécurisée dangereux, qui ne fait rien dans son corps :
fn main() {
unsafe fn dangereux() {}
unsafe {
dangereux();
}
}
Nous devons faire appel à la fonction dangereux dans un bloc unsafe séparé. Si nous essayons d’appeler dangereux sans le bloc unsafe, nous obtenons une erreur :
$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0133]: call to unsafe function `dangereux` is unsafe and requires unsafe block
--> src/main.rs:4:5
|
4 | dangereux();
| ^^^^^^^^^^^ call to unsafe function
|
= note: consult the function's documentation for information on how to avoid undefined behavior
For more information about this error, try `rustc --explain E0133`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error
Avec le bloc unsafe, nous déclarons à Rust que nous avons lu la documentation de la fonction, que nous comprenons comment l’utiliser correctement et que nous avons vérifié que nous répondons bien aux exigences de la fonction.
Pour effectuer des opérations non sécurisées dans le corps d’une fonction unsafe, vous devez tout de même utiliser un bloc unsafe, tout comme dans une fonction classique, et le compilateur vous avertira si jamais vous oubliez. Ceci contribue à garder les blocs unsafe les plus petits possible, vu que les opérations non sécurisées ne sont pas forcément nécessaires tout au long du corps de la fonction.
Créer une abstraction sécurisée sur du code non sécurisé
Ce n’est pas parce qu’une fonction contient du code non sécurisé que nous devons forcément marquer l’intégralité de cette fonction comme non sécurisée. En fait, envelopper du code non sécurisé dans une fonction sécurisée est une abstraction courante. Par exemple, étudions la fonction split_at_mut de la bibliothèque standard, , qui nécessite du code non sécurisé. Nous allons voir comment nous pourrions l’implémenter. Cette méthode sécurisée est définie sur des slices mutables : elle prend une slice en paramètre et en créée deux autres en divisant la slice à l’indice donné en argument. L’encart 20-4 montre comment utiliser split_at_mut.
fn main() {
let mut v = vec![1, 2, 3, 4, 5, 6];
let r = &mut v[..];
let (a, b) = r.split_at_mut(3);
assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5, 6]);
}
split_at_mut functionNous ne pouvons pas implémenter cette fonction en utilisant uniquement du Rust sécurisé. Une tentative en ce sens ressemblerait à l’encart 20-5, qui ne se compilera pas. Par simplicité, nous allons implémenter split_at_mut comme une fonction plutôt qu’une méthode et seulement pour des slices de valeurs i32 au lieu d’un type générique T.
fn split_at_mut(valeurs: &mut [i32], milieu: usize) -> (&mut [i32], &mut [i32]) {
let longueur = valeurs.len();
assert!(milieu <= longueur);
(&mut valeurs[..milieu], &mut valeurs[milieu..])
}
fn main() {
let mut vecteur = vec![1, 2, 3, 4, 5, 6];
let (gauche, droite) = split_at_mut(&mut vecteur, 3);
}
split_at_mut using only safe RustCette fonction commence par obtenir la longueur totale de la slice. Elle vérifie ensuite que l’indice donné en paramètre est bien à l’intérieur de la slice en vérifiant s’il est inférieur ou égal à la longueur. La vérification implique que si nous envoyons un indice qui est plus grand que la longueur de la slice à découper, la fonction va paniquer avant d’essayer d’utiliser cet indice.
Ensuite, nous retournons deux slices mutables dans un tuple : une à partir du début de la slice initiale jusqu’à l’indice milieu et une autre à partir de milieu jusqu’à la fin de la slice.
Lorsque nous essayons de compiler le code de l’encart 20-5, nous allons obtenir une erreur :
$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0499]: cannot borrow `*valeurs` as mutable more than once at a time
--> src/main.rs:6:31
|
1 | fn split_at_mut(valeurs: &mut [i32], milieu: usize) -> (&mut [i32], &mut [i32]) {
| - let's call the lifetime of this reference `'1`
...
6 | (&mut valeurs[..milieu], &mut valeurs[milieu..])
| ------------------------------^^^^^^^-----------
| | | |
| | | second mutable borrow occurs here
| | first mutable borrow occurs here
| returning this value requires that `*valeurs` is borrowed for `'1`
|
= help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices
For more information about this error, try `rustc --explain E0499`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error
Le vérificateur d’emprunt de Rust ne comprend pas que nous empruntons différentes parties de la slice ; il comprend seulement que nous empruntons la même slice à deux reprises. L’emprunt de différentes parties d’une slice ne pose fondamentalement pas de problèmes car les deux slices ne se chevauchent pas, mais Rust n’est pas suffisamment intelligent pour comprendre ceci. Lorsque nous savons que ce code est correct, mais que Rust ne le sait pas, il est approprié d’utiliser du code non sécurisé.
L’encart 20-6 montre comment utiliser un bloc unsafe, un pointeur brut, et quelques appels à des fonctions non sécurisées pour construire une implémentation de split_at_mut qui fonctionne.
use std::slice;
fn split_at_mut(valeurs: &mut [i32], milieu: usize) -> (&mut [i32], &mut [i32]) {
let longueur = valeurs.len();
let ptr = valeurs.as_mut_ptr();
assert!(milieu <= longueur);
unsafe {
(
slice::from_raw_parts_mut(ptr, milieu),
slice::from_raw_parts_mut(ptr.add(milieu), longueur - milieu),
)
}
}
fn main() {
let mut vecteur = vec![1, 2, 3, 4, 5, 6];
let (gauche, droite) = split_at_mut(&mut vecteur, 3);
}
split_at_mut functionSouvenez-vous de la section “Le type slice” du chapitre 4 dans laquelle nous avions dit qu’une slice est définie par un pointeur vers une donnée ainsi qu’une longueur de la slice. Nous avons utilisé la méthode len pour obtenir la longueur d’une slice ainsi que la méthode as_mut_ptr pour accéder au pointeur brut d’une slice. Dans ce cas, comme nous avons une slice mutable de valeurs i32, as_mut_ptr retourne un pointeur brut avec le type *mut i32 que nous stockons dans la variable ptr.
Nous avons conservé la vérification que l’indice milieu soit dans la slice. Ensuite, nous utilisons le code non sécurisé : la fonction slice::from_raw_parts_mut prend en paramètre un pointeur brut et une longueur, et elle créée une slice. Nous utilisons cette fonction pour créer une slice qui débute à ptr et qui est longue de milieu éléments. Ensuite nous faisons appel à la méthode add sur ptr avec milieu en argument pour obtenir un pointeur brut qui démarre à milieu, et nous créons une slice qui utilise ce pointeur et le nombre restant d’éléments après milieu comme longueur.
La fonction slice::from_raw_parts_mut est non sécurisée car elle prend en argument un pointeur brut et doit avoir confiance en la validité de ce pointeur. La méthode add sur les pointeurs bruts est aussi non sécurisée, car elle doit croire que l’emplacement décalé est aussi un pointeur valide. Voilà pourquoi nous avons placé un bloc unsafe autour de nos appels à slice::from_raw_parts_mut et add afin que nous puissions les effectuer. En analysant le code et en ayant ajouté la vérification que milieu doit être inférieur ou égal à len, nous pouvons affirmer que tous les pointeurs bruts utilisés dans le bloc unsafe sont des pointeurs valides vers les données de la slice. C’est une utilisation acceptable et appropriée de unsafe.
Remarquez que nous n’avons pas eu besoin de marquer la fonction résultante split_at_mut comme étant unsafe, et que nous pouvons faire appel à cette fonction dans du code Rust sécurisé. Nous avons créé une abstraction sécurisée du code non sécurisé avec une implémentation de la fonction qui utilise de manière sécurisée du code non sécurisé, car elle créée uniquement des pointeurs valides à partir des données auxquelles cette fonction a accès.
En contre-partie, l’utilisation de slice::from_raw_parts_mut dans l’encart 20-7 peut planter lorsque la slice sera utilisée. Ce code prend un emplacement arbitraire dans la mémoire et crée un slice de 10 000 éléments.
fn main() {
use std::slice;
let addresse = 0x01234usize;
let r = addresse as *mut i32;
let valeurs: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
}
Nous ne possédons pas la mémoire à cet emplacement arbitraire, et il n’y a aucune garantie que la slice créée par ce code contiennent des valeurs i32 valides. Toute tentative d’utilisation de valeurs aura un comportement imprévisible bien qu’il s’agisse d’une slice valide.
Utiliser des fonctions extern pour faire appel à du code externe
Parfois, votre code Rust peut avoir besoin d’interagir avec du code écrit dans d’autres langages. Dans ce cas, Rust propose le mot-clé extern qui facilite la création et l’utilisation du Foreign Function Interface (FFI), qui est un outil permettant à un langage de programmation de définir des fonctions auxquelles d’autres langages de programmation pourront faire appel.
L’encart 20-8 montre comment configurer l’intégration de la fonction abs de la bibliothèque standard du C. Les fonctions déclarées dans des blocs extern sont généralement non sécurisées pour des appels depuis du code Rust, donc les blocs extern doivent aussi être marqués unsafe. La raison à cela est que les autres langages n’appliquent pas les règles et garanties de Rust, Rust ne peut donc pas les vérifier, si bien que la responsabilité de s’assurer de la sécurité revient au développeur.
unsafe extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("La valeur absolue de -3 selon le langage C : {}", abs(-3));
}
}
extern function defined in another languageAu sein du bloc unsafe extern "C", nous listons les noms et les signatures des fonctions externes de l’autre langage que nous souhaitons solliciter. La partie “C” définit quelle est l’application binary interface (ABI) que la fonction doit utiliser : l’ABI définit comment faire appel à la fonction au niveau assembleur. L’ABI "C" est la plus courante et respecte l’ABI du langage de programmation C. Vous trouverez des informations sur toutes les ABIs prises en charge par Rust dans la Référence de Rust.
Chaque élément déclaré au sein d’un bloc unsafe extern est implicitement non sécurisé. Toutefois, les appels à certaines fonctions FFI sont sécurisés. Par exemple, la fonction abs de la bibliothèque standard du C ne pose aucun problème de sécurité concernant la mémoire, et nous savons qu’elle peut être appelée avec n’importe quel i32. Dans de tels cas, nous pouvons utiliser le mot-clé safe pour indiquer que cette fonction précise peut être appelée en toute sécurité, quand bien même elle se trouve dans un bloc unsafe extern. Une fois cette modification effectuée, son appel ne nécessite plus de bloc unsafe comme le montre l’encart 20-9.
unsafe extern "C" {
safe fn abs(input: i32) -> i32;
}
fn main() {
println!("La valeur absolue de -3 selon le langage C : {}", abs(-3));
}
safe within an unsafe extern block and calling it safelyLe simple fait de marquer une fonction comme safe ne la rend pas automatiquement sûre ! C’est plutôt comme une promessse que vous faites à Rust qu’elle est sûre. C’est toujours à vous qu’incombe la responsabilité de vérifier que cette promessse soit tenue !
Appel de fonctions Rust depuis d’autres langages
Nous pouvons aussi utiliser extern pour créer une interface qui permet à d’autres langages de faire appel à des fonctions Rust. Au lieu de créer tout un bloc extern, nous ajoutons le mot-clé extern et nous renseignons l’ABI à utiliser juste avant le mot-clé fn pour la fonction concernée. Nous avons aussi besoin d’ajouter une annotation #[unsafe(no_mangle)] pour dire au compilateur Rust de ne pas déformer le nom de cette fonction. La déformation (mangling) s’effectue lorsqu’un compilateur change le nom que nous avons donné à une fonction pour un nom qui contient plus d’informations pour d’autres étapes du processus de compilation, mais qui est moins lisible par l’humain. Tous les compilateurs de langages de programmation déforment les noms de façon légèrement différente, donc pour que le nom d’une fonction Rust soit utilisable par d’autres langages, nous devons désactiver la déformation du nom par le compilateur de Rust.
Dans l’exemple suivant, nous rendons la fonction call_from_c accessible à du code C, une fois qu’elle est compilée en une bibliothèque partagée et liée dans le C :
#[unsafe(no_mangle)]
pub extern "C" fn appel_depuis_c() {
println!("On vient d'appeler une fonction Rust function depuis du C !");
}
Cette utilisation de extern n’a besoin de unsafe que dans l’attribut, et pas dans le bloc extern.
Lire ou modifier une variable statique mutable
Dans ce livre, nous n’avons pas encore parlé des variables globales, que Rust accepte mais qui peuvent poser des problèmes avec les règles de possession de Rust. Si deux tâches accèdent en même temps à la même variable globale, cela peut causer un accès concurrent.
En Rust, les variables globales s’appellent des variables statiques. L’encart 20-10 montre un exemple de déclaration et d’utilisation d’une variable statique avec une slice de chaîne de caractères comme valeur.
static HELLO_WORLD: &str = "Hello, world!";
fn main() {
println!("Cela vaut : {HELLO_WORLD}");
}
Les variables statiques ressemblent aux constantes, que nous avons vues dans la section “Déclaration des constantes” du chapitre 3. Les noms des variables statiques sont par convention en SCREAMING_SNAKE_CASE. Les variables statiques peuvent uniquement stocker des références ayant la durée de vie 'static, de façon à ce que le compilateur Rust puisse la déterminer tout seul et que nous n’ayons pas besoin de la renseigner explicitement. L’accès à une variable statique immuable est sécurisé.
Une différence subtile entre les constantes et les variables immuables est que les valeurs d’une variable statique ont une adresse fixe en mémoire. L’utilisation de cette valeur donnera toujours accès aux mêmes données. Les constantes, pour leur part, peuvent dupliquer leurs données à chaque utilisation. Une autre différence est que les variables statiques peuvent être mutables. Lire et modifier des variables statiques mutables est non sécurisé. L’encart 20-11 montre comment déclarer, lire et modifier la variable statique mutable COMPTEUR.
static mut COMPTEUR: u32 = 0;
/// SÉCURITÉ : appeler cette fonction à partir de plus d'un fil d'exécution
/// simultanément entraîne un comportement indéfini ; vous *devez* donc vous
/// assurer de ne l'appeler qu'à partir d'un seul fil d'exécution à la fois.
unsafe fn ajouter_au_compteur(valeur: u32) {
unsafe {
COMPTEUR += valeur;
}
}
fn main() {
unsafe {
// SÉCURITÉ : cette fonction n'est appelée qu'à partir d'un seul fil d'exécution dans `main`.
ajouter_au_compteur(3);
println!("COMPTEUR : {}", *(&raw const COMPTEUR));
}
}
Comme avec les variables classiques, nous renseignons la mutabilité en utilisant le mot-clé mut. Le code dans l’encart 20-11 se compile et affiche COMPTEUR : 3 comme nous l’espérions car il n’y a qu’un seul fil d’exécution. Si nous en avions plusieurs qui accèdent à COMPTEUR, nous pourrions avoir des accès concurrents, ce qui conduit à un comportement indéfini. De ce fait, nous devons marquer l’ensemble de la fonction comme unsafe et documenter la limite de sécurité afin que quiconque appellant la fonction sache ce qu’il peut faire et ce qu’elle ne peut pas faire en toute sécurité.
Chaque fois que nous écrivons une fonction non sécurisée, la pratique courante est d’écrire un commentaire commençant par SAFETY qui explique ce que l’appelant doit faire pour appeler la fonction de manière sécurisée. De même, à chaque fois que nous effectuons une opération non sécurisée, la pratique courante consiste à rédiger un commentaire commençant par SÉCURITÉ pour expliquer comment les règles de sécurité sont mises en œuvre.
De plus, le compilateur va bloquer, par défaut, toute tentative de créer des références à une variable statique mutable via un outil de vérification syntaxique (lint) du compilateur. Vous devez soit désactiver de manière explicite cette protection en ajoutant une annotation #[allow(static_mut_refs)], soit accéder à la variable statique mutable par l’intermédiaire d’un pointeur brut créé avec un des opérateurs d’emprunt brut. Ceci comprend les cas de figure dans lesquels la référence est créée de manière invisible, comme lorsqu’elle est utilisée dans le println! de cet extrait de code. L’obligation d’avoir recours à des références vers des variables mutables statiques contribue à rendre plus évidentes les exigences de sécurité liées à leur utilisation.
Avec des données mutables qui sont accessibles globalement, il devient difficile de s’assurer qu’il n’y a pas d’accès concurrents, c’est pourquoi Rust considère les variables statiques mutables comme étant non sécurisées. Lorsque c’est possible, il vaut mieux utiliser les techniques de concurrence et les pointeurs intelligents adaptés au multitâche que nous avons vus au chapitre 16, afin que le compilateur puisse vérifier que l’accès aux données depuis différents fils d’exécution se fasse de manière sécurisée.
Implémenter un trait non sécurisé
Nous pouvons utiliser unsafe pour implémentater un trait non sécurisé. Un trait n’est pas sécurisé lorsqu’au moins une de ses méthodes contient une invariante que le compilateur ne peut pas vérifier. Nous déclarons un trait non sécurisé en ajoutant le mot-clé unsafe devant trait et en marquant aussi l’implémentation du trait comme unsafe, comme dans l’encart 20-12.
unsafe trait Foo {
// les méthodes vont ici
}
unsafe impl Foo for i32 {
// les implémentations des méthodes vont ici
}
fn main() {}
En utilisant unsafe impl, nous promettons que nous veillons aux invariantes que le compilateur ne peut pas vérifier.
Par exemple, souvenez-vous des traits Send et Sync que nous avions découverts dans la section “Extension de la concurrence avec les traits Send et Sync” du chapitre 16 : le compilateur implémente automatiquement ces traits si nos types sont entièrement composés d’autres types qui implémentent Send et Sync. Si nous implémentons un type qui contient un type qui n’implémente pas Send ou Sync, tel que les pointeurs bruts, et que nous souhaitons marquer ce type comme étant Send ou Sync, nous devons utiliser unsafe. Rust ne peut pas vérifier que notre type respecte les garanties pour que ce type puisse être envoyé en toute sécurité entre des fils d’exécution, ou qu’il puisse être utilisé par plusieurs fils d’exécution ; conséquemment, nous devons faire ces vérifications manuellement et les signaler avec unsafe.
Utiliser des champs d’un Union
La dernière action qui fonctionne uniquement avec unsafe est d’accéder aux champs d’un union. Un union ressemble à une struct, mais un seul champ de ceux déclarés est utilisé dans une instance précise au même moment. Les unions sont principalement utilisés pour s’interfacer avec les unions du code C. L’accès aux champs des unions n’est pas sécurisé car Rust ne peut pas garantir le type de la donnée qui est actuellement stockée dans l’instance de l’union. Vous pouvez en apprendre plus sur les unions dans the Rust Reference.
Utilisation de Miri pour contrôler du code non sécurisé
Quand vous écrivez du code non sécurisé, vous pourriez vouloir contrôler que ce que vous avez écrit est réellement sûr et correct. Parmi les meilleures manières de faire se trouve le recours à Miri, un outil Rust officiel pour la détection de comportements indéfinis. Alors que le vérificateur d’emprunt est un outil statique qui fonctionne au moment de la compilation, Miri est un outil dynamique qui fonctionne au moment de l’exécution. Il contrôle votre code en exécutant votre programme ou bien sa série de tests, et détecte les cas où vous enfreignez les règles qu’il connaît concernant le comportement attendu de Rust.
Pour utiliser Miri, il faut une version “nightly” de Rust (dont nous parlerons plus en détail dans l’Annexe G : comment Rust est fabriqué, et “Nightly Rust”). Vous pouvez installer à la fois une version “nightly” de Rust et l’outil Miri en entrant rustup +nightly component add miri. Cette commande ne change pas la version de Rust utilisée par votre projet ; elle ajoute simplement l’outil au système de manière à ce que vous puissiez l’utiliser quand vous le souhaitez. Vous pouvez exécuter Miri sur un projet en entrant cargo +nightly miri run ou cargo +nightly miri test.
Pour avoir un exemple montrant combien cela peut être utile, voyez ce qui se passe quand nous l’exécutons sur le code de l’encart 20-7.
$ cargo +nightly miri run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
Running `file:///home/.rustup/toolchains/nightly/bin/cargo-miri runner target/miri/debug/unsafe-example`
warning: integer-to-pointer cast
--> src/main.rs:5:13
|
| ^^^^^^^^^^^^^^^^^^^ integer-to-pointer cast
| ^^^^^^^^^^^^^^^^^^^ integer-to-pointer cast
|
= help: this program is using integer-to-pointer casts or (equivalently) `ptr::with_exposed_provenance`, which means that Miri might miss pointer bugs in this program
= help: see https://doc.rust-lang.org/nightly/std/ptr/fn.with_exposed_provenance.html for more details on that operation
= help: to ensure that Miri does not miss bugs in your program, use Strict Provenance APIs (https://doc.rust-lang.org/nightly/std/ptr/index.html#strict-provenance, https://crates.io/crates/sptr) instead
= help: you can then set `MIRIFLAGS=-Zmiri-strict-provenance` to ensure you are not relying on `with_exposed_provenance` semantics
= help: alternatively, `MIRIFLAGS=-Zmiri-permissive-provenance` disables this warning
error: Undefined Behavior: constructing invalid value of type &mut [i32]: encountered a dangling reference (0x1234[noalloc] has no provenance)
--> src/main.rs:7:35
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
|
= 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
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
error: aborting due to 1 previous error; 1 warning emitted
Miri nous avertit à juste titre que nous convertissons un entier vers un pointeur, ce qui pourrait être un problème, mais Miri ne peut déterminer si le problème existe réellement, car il ne connaît pas l’origine du pointeur. Miri renvoie alors une erreur indiquant que le code de l’encart 20-7 présente un comportement indéfini parce que nous avons une référence pendouillante, de sorte que nous pouvons songer à la manière de sécuriser ce code . Dans certains cas, Miri peut même faire des recommandations sur la manière de corriger les erreurs.
Miri ne détecte pas tout ce que vous pourriez faire de mal en écrivant du code non sécurisé. Miri est un outil d’analyse dynamique, il ne peut donc détecter des problèmes que sur du code qui peut réellement être exécuté. Ceci implique que vous devrez l’utiliser conjointement avec de bonnes techniques de tests afin d’augmenter votre confiance dans le code non sécurisé que vous avez écrit. Miri ne prend pas non plus en compte toutes les façons possibles dont votre code peut être défaillant.
Tournons cela d’une autre manière : si Miri détecte réellement un problème, vous savez qu’il y a un bogue, mais le fait que Miri ne détecte pas de bogue ne veut pas dire qu’il n’y a pas de problème. Toutefois, Miri peut détecter beaucoup de bogues. Tentez de l’exécuter sur d’autres exemples de code non sécurisé dans ce chapitre, vous verrez ce qui en résulte !
Vous pouvez en savoir plus sur Miri en allant sur son repo GitHub .
Utilisation correcte de code non sécurisé
L’utilisation de unsafe pour mettre en oeuvre un des cinq super-pouvoirs que nous venons d’aborder n’est pas une mauvaise chose et ne doit pas être mal vu, mais il est plus difficile de sécuriser du code unsafe car le compilateur ne peut pas aider à garantir la sécurité de la mémoire. Lorsque vous avez une bonne raison d’utiliser du code non sécurisé, vous pouvez le faire, et vous aurez l’annotation explicite unsafe pour faciliter la recherche de la source des problèmes lorsqu’ils surviennent. À chaque fois que vous écrivez du code non sécurisé, vous pouvez utiliser Miri pour vous aider à avoir plus de confiance dans le fait que le code que vous avez écrit ne contrevient pas aux règles de Rust.
Pour aller bien plus loin dans la découverte de la manière de travailler efficacement avec du Rust non sécurisé, consultez le guide officiel de Rust consacré à unsafe, le Rustonomicon.