Aller au contenu

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.

Publié le 8 min de lecture

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 bugC / C++À ramasse-miettes (Go, Java, C#, Swift*)Rust sûr
Débordement de tamponPossibleEmpêché par des vérifications de bornes (panic/exception à l'exécution)Empêché par des vérifications de bornes (panic)
Use-after-freePossibleEmpêché par le GCEmpêché par la propriété à la compilation
Double freePossibleEmpêché par le GCEmpêché par la propriété
Lectures non initialiséesPossibleEmpêchées (valeurs zéro / affectation définie)Empêchées à la compilation
Accès concurrents (data races)PossiblePossibles en Go et Java (pas non sûrs pour la mémoire en Java)Empêchés à la compilation (Send/Sync)
Débordement d'entierUB (signé) ou bouclageBouclage ou vérifié, selon le langagePanic en debug, bouclage en release sauf si vérifié
Bugs de logique, injection, erreurs d'authentificationPossiblePossiblePossible

*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 Vec hors plage provoque un panic au lieu de lire la mémoire adjacente ; get() renvoie une Option pour le code qui veut le gérer.

Où les garanties s'arrêtent

  • Les blocs unsafe laissent 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. Gardez unsafe petit, 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, cxx et 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 audit vérifie les avis connus, et des outils comme cargo-geiger indiquent où unsafe est 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émoirePourquoi
1Nouveaux composants et nouvelles fonctionnalitésLes vulnérabilités se concentrent dans le code nouveau et récemment modifié
2Parseurs, décodeurs, codecs, gestionnaires de protocolesIls traitent des entrées non fiables et sont historiquement denses en bugs
3Services exposés au réseau et couches externes des bacs à sableSurface d'attaque la plus élevée
4Composants avec un historique de CVE de sûreté mémoirePreuve du risque
5Code interne stable et bien fuzzéRetour sur réécriture le plus faible ; durcissez plutôt

Étapes pratiques :

  1. 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é.
  2. 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.
  3. 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.
  4. 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_ASSERTIONS pour libstdc++, ou les modes de durcissement de libc++ (-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST dans les versions récentes de LLVM). Ils font que operator[], 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::span et std::string_view transportent leur taille avec eux.
  • La propriété dans les types. std::unique_ptr et std::shared_ptr plutôt que des pointeurs bruts propriétaires ; pas de new/delete dans le code applicatif.
  • L'aide du compilateur. Le -Wunsafe-buffer-usage de 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 unsafe et 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.

Guides associés

0x1000 · Classes de vulnérabilités

Les débordements de tampon de pile expliqués aux défenseurs

Pourquoi écrire au-delà d'un tampon de pile est dangereux, les schémas C qui le causent, comment les compilateurs et sanitizers le détectent, et les corrections et mitigations qui le contiennent.