Un collègue m'a montré un jour un binaire d'à peine 40 lignes de C. Il lisait une entrée réseau, copiait la chaîne dans un tampon fixe, la traitait. Propre en apparence. Sauf qu'un paquet de 300 octets faisait déborder le tampon, écrasait l'adresse de retour, et le programme exécutait ce qu'on lui envoyait. Il m'a fallu dix secondes pour comprendre le bug et trois heures pour savoir quoi en faire. C'est ce jour-là que j'ai commencé à regarder Rust sérieusement.
Le langage revient sans arrêt dans les discussions sur la sécurité système, et pour une bonne raison : il déplace une partie des bugs mémoire du runtime vers le compilateur. Autrement dit, une classe entière de vulnérabilités ne se contente plus d'être détectée en test — elle empêche carrément la compilation.
Points clés à retenir
- L'ownership fait vérifier à la compilation qui possède quoi, ce qui supprime les accès après libération et les doubles libérations.
- Le borrow checker refuse deux références mutables simultanées : les data races deviennent impossibles dans le code safe.
unsafene désactive rien magiquement ; il déplace votre responsabilité du compilateur vers vous, avec des invariants à documenter.- En sécurité système, on préfère
Resultàpanic!, on borne les entrées non fiables, et on interditunsafepar défaut quand c'est possible. - Le guide de l'ANSSI donne un cadre concret aux applications sensibles, mais il ne remplace pas une compréhension des fondements.
Pourquoi Rust change la donne en sécurité système
La majorité des failles critiques qu'on trouve dans les logiciels bas niveau partagent un point commun : un programme touche à de la mémoire qu'il ne devrait pas toucher. Dépassement de tampon, use-after-free, lecture hors bornes. Ces bugs n'existent pas parce que les développeurs sont négligents. Ils existent parce que le C et le C++ vous laissent faire ce que vous voulez, et comptent sur votre vigilance.
Rust renverse la logique. Il vous oblige à prouver au compilateur que votre code est cohérent, sinon rien ne se construit.
Ce que le compilateur refuse de laisser passer
Trois choses, principalement, et chacune correspond à une famille de vulnérabilités.
- Une valeur a un seul propriétaire à un instant donné. Quand ce propriétaire disparaît, la mémoire est libérée. Pas de double libération possible.
- Deux références mutables simultanées sont interdites. C'est la règle qui élimine les data races côté threads sans verrou.
- Le type de retour d'une opération est explicite. Une fonction qui peut échouer retourne
Result, et vous ne pouvez pas ignorer ce résultat sans le dire explicitement.
Franchement, la première semaine, ces règles sont pénibles. J'ai passé deux heures sur un morceau de code qui aurait pris dix minutes en C, juste pour comprendre pourquoi le borrow checker me bloquait sur une référence que je croyais inoffensive. Le déclic est venu quand j'ai réalisé que le compilateur me montrait un bug réel que j'aurais mis des jours à reproduire en production.
Pas de ramasse-miettes, pas de pause imprévisible
Un point qu'on oublie souvent : Rust n'embarque pas de garbage collector. La gestion mémoire est décidée à la compilation. Pour un composant système qui doit répondre en quelques microsecondes, c'est déterminant — pas de pause imprévisible au mauvais moment. C'est d'ailleurs pour ça qu'on le retrouve dans des noyaux, des hyperviseurs et des firmwares.
Les fondamentaux à maîtriser avant de toucher à la sécurité
Apprendre Rust par le biais de la sécurité, c'est tenter de courir avant de marcher. Il y a une base à consolider.
Variables, types et mutabilité explicite
En Rust, une variable est immuable par défaut. Pour la modifier, on écrit mut. Ce détail paraît anodin ; il force une intention explicite à chaque étape.
let x = 5; // immuable
let mut y = 5; // modifiable
y += 1;
Les types scalaires (i32, u64, bool, char) et composés (tuple, array) couvrent 90 % des besoins bas niveau. Le point à retenir : la taille d'un entier est explicite. Pas de int ambigu qui varie selon la plateforme.
Le pattern matching, arme de clarté
Rust vous laisse décrire exhaustivement ce que vous attendez d'une valeur. L'intérêt en sécurité, c'est que le compilateur vous force à traiter chaque cas.
match result {
Ok(value) => traiter(value),
Err(e) => log_error(e),
}
Si vous ajoutez une variante à un enum, le compilateur vous signale chaque endroit qui n'en tient pas compte. Ça paraît anodin. En pratique, ça élimine les cas oubliés qui finissent en comportement indéfini.
La gestion d'erreurs sans exception
Rust n'a pas d'exceptions. Tout passe par Result<T, E> ou Option<T>. En code système, c'est une différence majeure : un panic! qui traverse une frontière FFI peut laisser le système dans un état corrompu. Une erreur remontée proprement, à l'inverse, laisse la main à l'appelant.
Règle simple : panic uniquement pour les bugs de programmation, jamais pour les erreurs d'entrée.
Comprendre unsafe avant d'en écrire une ligne
Le bloc unsafe est probablement le point le plus mal compris de Rust. Il ne désactive pas le borrow checker, il ne rend pas le code magiquement dangereux. Il vous autorise à faire cinq choses précises que le compilateur ne peut pas vérifier, comme déréférencer un pointeur brut ou appeler une fonction C.
À partir de là, vous devenez le garant des invariants.
Quand c'est légitime
- Appeler une bibliothèque C existante via FFI.
- Implémenter une abstraction bas niveau dont l'API publique reste safe.
- Optimiser une portion critique après avoir mesuré que le safe est trop lent (rare).
Comment encadrer un bloc unsafe
Trois habitudes que j'applique systématiquement, et qui ne m'ont jamais coûté de temps perdu :
- Isoler la portion unsafe dans une fonction dédiée, aussi petite que possible.
- Documenter l'invariant en commentaire juste au-dessus : pourquoi ce bloc ne peut pas déclencher de comportement indéfini.
- Exposer une API safe qui ne laisse pas l'appelant deviner les contraintes.
Pour les applications sensibles, il existe une directive simple : #![forbid(unsafe_code)] en tête de crate. Le compilateur refuse alors toute introduction de code unsafe, même future. C'est brutal. C'est efficace. Et quand vous devez vraiment déroger, vous le faites dans un crate séparé, avec une justification écrite.
L'outillage, souvent sous-estimé
On parle peu de Cargo dans les articles sur la sécurité, et c'est une erreur.
Cargo est le gestionnaire de paquets et de compilation. Il verrouille les versions de vos dépendances via un fichier Cargo.lock, ce qui évite les surprises de type "la version a changé, comportement différent". Il intègre aussi rustfmt (formatage) et clippy (analyse statique). Clippy, en particulier, remonte des motifs suspects : boucles qui pourraient être des itérateurs, comparaisons redondantes, conversions implicites risquées.
| Outil | Rôle | Quand y penser |
|---|---|---|
| Cargo | Compilation, dépendances, versions verrouillées | Dès le premier projet |
| Clippy | Analyse statique, motifs à risque | À chaque commit |
| rustfmt | Formatage automatique | Configuré une fois pour toutes |
| cargo-audit | Vulnérabilités connues dans les dépendances | En intégration continue |
Bref, l'écosystème vous donne les moyens de rendre la sécurité répétable, pas seulement dépendante de la vigilance d'un développeur un vendredi soir.
Ce que le guide de l'ANSSI apporte concrètement
Pour des applications sensibles en France, l'ANSSI a publié un guide dédié aux règles de programmation sécurisée en Rust. Il ne remplace pas la compréhension du langage, mais il fixe un cadre commun aux équipes.
Plusieurs recommandations reviennent, et elles recoupent ce qu'on vient de voir :
- Interdire
unsafesauf justification écrite, avec revue systématique. - Traiter les entrées non fiables comme telles, sans présumer de leur format.
- Préférer les retours d'erreur explicites aux arrêts brutaux du programme.
- Borner les tailles de tampon, ne jamais allouer à partir d'une taille lue dans les données.
- Épingler les dépendances et surveiller les vulnérabilités connues.
Ce qui m'a marqué en le lisant, c'est que Rust seul ne suffit pas. Le langage élimine les bugs mémoire, oui. Mais un programme qui fait panic! sur une entrée réseau reste un programme qu'on peut faire tomber. La sécurité vient de l'alliance entre le langage et la discipline de codage.
Trois erreurs que j'ai commises en apprenant
Les débutants reproduisent souvent les mêmes pièges. J'en ai traversé plusieurs.
Cloner pour faire taire le compilateur
Quand le borrow checker râle, le réflexe est d'appeler .clone() partout. Ça compile. Ça masque aussi la vraie question : qui devrait posséder cette donnée ? J'ai eu un service qui clonait des buffers à chaque requête réseau, résultat : latence multipliée par quatre sur les flux volumineux. Le problème n'était pas Rust, c'était mon refus de repenser l'architecture de données.
Abuser de unsafe au premier obstacle
Quand une structure de données liée se bat avec le borrow checker, il est tentant de basculer en unsafe pour "régler le problème". Sauf qu'on troque un bug de compilation contre un bug d'exécution. Neuf fois sur dix, il existe une disposition safe correcte : indices, intervalles, ou RefCell quand la mutation partagée est vraiment nécessaire.
Ignorer un Result avec .unwrap()
.unwrap() panique si l'opération échoue. Tolérable dans un prototype. Inacceptable dans une boucle de traitement réseau. J'ai vu une fois un serveur tomber en boucle parce qu'un .unwrap() rencontrait un en-tête mal formé envoyé par un scanner qui parcourait Internet. Le correctif était trivial (match + log), mais il est venu après trois redémarrages.
Mettre en pratique sans se noyer
Si vous voulez vraiment apprendre Rust pour la sécurité système, voici un ordre qui fonctionne, testé sur moi-même et sur deux collègues :
- Deux semaines sur les bases seules — ownership, borrow, types, pattern matching. Ne touchez pas à unsafe.
- Un petit parseur binaire qui lit un format simple (en-têtes, longueur, corps). Vous verrez vite pourquoi les bornes comptent.
- Un client réseau rudimentaire avec gestion d'erreurs sérieuse. Objectif : zéro
.unwrap()dans le chemin critique. - Un module avec un bloc unsafe isolé, documenté, et une API publique safe par-dessus.
- Clippy et cargo-audit branchés dès le début, pas à la fin.
Comptez un à deux mois pour être à l'aise. Pas trois jours. Et c'est normal — vous n'apprenez pas juste une syntaxe, vous changez votre rapport à la mémoire.
Ce qui reste après avoir compris tout ça
Rust ne rend pas un système sûr à lui tout seul. Il rend certaines classes de bugs impossibles. C'est déjà énorme, parce que ces bugs sont précisément ceux qui coûtent le plus cher : silencieux, difficiles à reproduire, exploitables à distance.
La vraie question n'est pas "Rust ou pas Rust". C'est : combien de bugs mémoire êtes-vous prêt à laisser dans votre code, sachant qu'un compilateur peut les éliminer à votre place ? Pour ma part, la réponse est devenue : le moins possible. Et le jour où j'ai recompilé mon vieux parseur réseau en Rust, sans changer une ligne de logique métier, j'ai vu disparaître trois avertissements de sécurité que j'avais classés "à surveiller un jour". Ce jour-là est arrivé tout seul.