Langages sûrs pour la mémoire et migration réaliste
Pourquoi les langages sûrs pour la mémoire éliminent des classes de bugs, ce que garantit la propriété de Rust, et où unsafe et le FFI restent risqués.
Chaque guide de ce site jusqu'ici a porté sur la gestion des conséquences d'un code non sûr pour la mémoire : trouver les bugs avec les sanitizers et le fuzzing, et les émousser avec des mitigations. Les langages sûrs pour la mémoire attaquent le problème à la source : ils rendent les débordements de tampon, le use-after-free et le double free impossibles à exprimer dans du code ordinaire. Ce guide explique ce que couvre cette garantie, où elle s'arrête, et comment les équipes s'en approchent sans réécriture du tout ou rien.
Pourquoi c'est devenu un sujet de politique
Plusieurs grands éditeurs ont publié des analyses de leurs bugs de sécurité. Microsoft et le projet Chromium ont tous deux déclaré publiquement qu'environ 70 % des bugs de sécurité graves qu'ils corrigent sont des problèmes de sûreté mémoire. L'équipe Android de Google a rapporté que les vulnérabilités de sûreté mémoire ont fortement diminué à mesure que la part du nouveau code natif écrit dans des langages sûrs pour la mémoire (Rust, plus Java et Kotlin) grandissait, même si le code C et C++ existant n'a pas été réécrit.
Les gouvernements ont suivi. La NSA a publié une fiche d'information sur la sûreté mémoire logicielle en 2022 recommandant les langages sûrs pour la mémoire là où c'est possible, et la CISA, avec des agences partenaires, a publié The Case for Memory Safe Roadmaps en 2023, demandant aux fabricants de logiciels de publier des plans pour réduire les vulnérabilités de sûreté mémoire dans leurs produits. Le message est cohérent : la sûreté mémoire est une décision au niveau du produit, pas seulement une habitude de développeur.
Ce que « sûr pour la mémoire » garantit
| Classe de bug | C / C++ | À ramasse-miettes (Go, Java, C#, Swift*) | Rust sûr |
|---|---|---|---|
| Débordement de tampon | Possible | Empêché par des vérifications de bornes (panic/exception à l'exécution) | Empêché par des vérifications de bornes (panic) |
| Use-after-free | Possible | Empêché par le GC | Empêché par la propriété à la compilation |
| Double free | Possible | Empêché par le GC | Empêché par la propriété |
| Lectures non initialisées | Possible | Empêchées (valeurs zéro / affectation définie) | Empêchées à la compilation |
| Accès concurrents (data races) | Possible | Possibles en Go et Java (pas non sûrs pour la mémoire en Java) | Empêchés à la compilation (Send/Sync) |
| Débordement d'entier | UB (signé) ou bouclage | Bouclage ou vérifié, selon le langage | Panic en debug, bouclage en release sauf si vérifié |
| Bugs de logique, injection, erreurs d'authentification | Possible | Possible | Possible |
*Swift utilise le comptage de références automatique plutôt qu'un GC à traçage, avec des collections à bornes vérifiées.
Le point clé pour les défenseurs : dans la colonne des langages sûrs pour la mémoire, un accès hors-limites devient un panic ou une exception contrôlée au lieu d'une corruption silencieuse. Un crash reste un bug et peut encore être un déni de service, mais ce n'est pas une écriture mémoire arbitraire.
Comment Rust l'impose
Le compilateur de Rust suit qui possède chaque valeur et combien de temps vivent les références vers elle :
fn longest_name(names: &[String]) -> Option<&str> {
names.iter().map(|s| s.as_str()).max_by_key(|s| s.len())
}
fn main() {
let result;
{
let names = vec![String::from("ada"), String::from("grace")];
result = longest_name(&names);
} // `names` est abandonné (dropped) ici
println!("{result:?}"); // erreur de compilation : `names` ne vit pas assez longtemps
}
Le même use-after-free qui compilerait silencieusement en C est rejeté avant même que le programme ne s'exécute. Trois règles font l'essentiel du travail :
- Propriété. Chaque valeur a exactement un propriétaire ; quand le propriétaire sort de portée, la valeur est abandonnée une fois. Le double free ne peut pas être exprimé.
- Emprunt. Vous pouvez avoir plusieurs références partagées (
&T) ou une référence mutable (&mut T), jamais les deux, et les références ne peuvent pas survivre à la valeur. L'invalidation d'itérateur et les pointeurs pendants sont rejetés. - Vérifications de bornes. Indexer un slice ou un
Vechors plage provoque un panic au lieu de lire la mémoire adjacente ;get()renvoie uneOptionpour le code qui veut le gérer.
Où les garanties s'arrêtent
- Les blocs
unsafelaissent le code déréférencer des pointeurs bruts, appeler des fonctions étrangères et implémenter des abstractions de bas niveau. La bibliothèque standard elle-même s'appuie sur eux. Gardezunsafepetit, enveloppez-le dans des API sûres, documentez les invariants avec des commentaires// SAFETY:, et exécutez ce code sous Miri et des sanitizers dans les tests. - Les frontières FFI avec C et C++ sont là où la plupart des bugs de sûreté mémoire des bases de code mixtes se concentrent désormais : propriété mal appariée, durées de vie que le côté Rust ne peut pas voir, et du code C qui n'a jamais été sûr au départ. Des outils comme
bindgen,cxxet des conventions de propriété soignées aident. - Les panics sont sûrs mais peuvent quand même mettre un service à terre. Traitez les entrées non fiables avec des API faillibles plutôt qu'avec
unwrap(). - Les dépendances. Une crate peut contenir du code
unsafe.cargo auditvérifie les avis connus, et des outils commecargo-geigerindiquent oùunsafeest utilisé.
Une stratégie de migration réaliste
Les organisations qui rapportent des progrès partagent un schéma commun : elles ont changé le langage dans lequel le nouveau code est écrit, et ont priorisé par exposition.
| Priorité | Ce qu'il faut déplacer ou écrire dans un langage sûr pour la mémoire | Pourquoi |
|---|---|---|
| 1 | Nouveaux composants et nouvelles fonctionnalités | Les vulnérabilités se concentrent dans le code nouveau et récemment modifié |
| 2 | Parseurs, décodeurs, codecs, gestionnaires de protocoles | Ils traitent des entrées non fiables et sont historiquement denses en bugs |
| 3 | Services exposés au réseau et couches externes des bacs à sable | Surface d'attaque la plus élevée |
| 4 | Composants avec un historique de CVE de sûreté mémoire | Preuve du risque |
| 5 | Code interne stable et bien fuzzé | Retour sur réécriture le plus faible ; durcissez plutôt |
Étapes pratiques :
- Publiez une feuille de route interne, comme le recommandent les directives de la CISA : quels langages sont approuvés pour le nouveau code, quels composants sont candidats à la migration, et comment le progrès est mesuré.
- Rendez l'interopérabilité ennuyeuse. Établissez une façon supportée d'appeler du code sûr pour la mémoire depuis la build C ou C++ existante (et vice versa) avant que le premier composant ne bouge.
- Remplacez, ne traduisez pas. Réécrire un parseur en Rust est une occasion de reconcevoir son API autour de la propriété, pas de transposer l'arithmétique de pointeurs.
- Continuez à fuzzer les deux côtés. Fuzzez la nouvelle implémentation et, pendant la transition, comparez sa sortie avec l'ancienne (fuzzing différentiel).
Durcir le C++ que vous gardez
La plupart des bases de code contiendront du C et du C++ pendant de nombreuses années. Le C++ moderne offre des défauts bien plus sûrs que sa réputation ne le suggère, si vous les utilisez :
- Accès à bornes vérifiées. Activez le durcissement de la bibliothèque standard :
-D_GLIBCXX_ASSERTIONSpour libstdc++, ou les modes de durcissement de libc++ (-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FASTdans les versions récentes de LLVM). Ils font queoperator[],front(), le mauvais usage d'itérateur et les erreurs similaires provoquent un trap au lieu de corrompre la mémoire, à faible coût. - Vues plutôt que pointeur-et-longueur.
std::spanetstd::string_viewtransportent leur taille avec eux. - La propriété dans les types.
std::unique_ptretstd::shared_ptrplutôt que des pointeurs bruts propriétaires ; pas denew/deletedans le code applicatif. - L'aide du compilateur. Le
-Wunsafe-buffer-usagede Clang signale l'arithmétique de pointeurs bruts et l'indexation de tableaux afin que vous puissiez migrer le code vers des conteneurs à bornes vérifiées fichier par fichier. - Directives et vérificateurs. Les C++ Core Guidelines avec les vérifications
cppcoreguidelines-*de clang-tidy attrapent automatiquement de nombreuses erreurs de propriété et de bornes. - Les couches habituelles. Flags de durcissement, intégrité du flux de contrôle, sanitizers en CI et fuzzing continu.
Points clés à retenir
- Les langages sûrs pour la mémoire transforment les débordements de tampon, le use-after-free et le double free d'une corruption exploitable en erreurs de compilation ou en échecs contrôlés.
- Les données de l'industrie pointent dans la même direction : la plupart des bugs graves dans les grandes bases de code C et C++ sont des bugs de sûreté mémoire, et faire basculer le nouveau code vers des langages sûrs pour la mémoire les réduit sans tout réécrire.
- Le code
unsafeet les frontières FFI sont là où le risque restant se concentre ; gardez-les petits, revus et testés. - Priorisez le nouveau code et le traitement des entrées non fiables, publiez une feuille de route, et durcissez le C++ que vous gardez avec des assertions de bibliothèque,
std::span, des pointeurs intelligents et des vérifications du compilateur.