- TRACTOR de la DARPA est un programme de recherche visant à convertir automatiquement du code C legacy en Rust, afin d’éliminer toute une catégorie de vulnérabilités de sécurité liées à la sûreté mémoire dans les programmes C
- Les problèmes de sûreté mémoire en C et C++ sont traités depuis plus de 20 ans, mais l’idée s’est imposée que les outils de détection de bugs ne suffisent pas à eux seuls
- L’alternative consiste à porter le code existant vers un langage de programmation sûr, capable de rejeter à la compilation les programmes non sûrs
- Le résultat de la conversion vise une qualité et un style comparables à du code écrit par des développeurs Rust expérimentés, en combinant analyse statique et dynamique avec de l’apprentissage automatique fondé sur de grands modèles de langage
- Le MIT Lincoln Laboratory est chargé des tests et de l’évaluation, et les benchmarks, projets jalons et outils sont publiés sur une page publique
Objectif de TRACTOR
- TRACTOR est le programme « Translating All C to Rust », dont l’objectif est de convertir automatiquement du code C legacy en Rust
- Sur le plan de la sécurité, il vise à éliminer toute la catégorie des vulnérabilités de sécurité liées à la sûreté mémoire présentes dans les programmes C
- Le code converti ne doit pas seulement fonctionner : il vise une qualité et un style comparables à ceux d’un code écrit par des développeurs Rust expérimentés
Pourquoi une conversion vers Rust est nécessaire
- Les problèmes de sûreté mémoire en C et C++ sont un défi que la communauté du génie logiciel traite depuis plus de 20 ans
- Un consensus s’est formé sur le fait qu’il est difficile de résoudre suffisamment le problème en s’appuyant uniquement sur des outils de détection de bugs
- L’approche jugée préférable consiste à utiliser un langage de programmation sûr capable de rejeter à la compilation les programmes non sûrs
- TRACTOR utilise Rust comme langage cible
Technologies utilisées pour la conversion automatique
- TRACTOR combine plusieurs techniques d’analyse logicielle et d’apprentissage automatique pour porter du code C legacy vers Rust
-
Techniques d’analyse
- Il exploite à la fois l’analyse statique et l’analyse dynamique
- Des techniques d’apprentissage automatique, notamment les grands modèles de langage, font également partie de cette combinaison
- Cette combinaison constitue un axe technologique essentiel pour comprendre la structure et le comportement du code C, puis le convertir automatiquement en code Rust
Tests, évaluation et ressources publiques
- L’équipe du MIT Lincoln Laboratory réalise les tests et l’évaluation du programme de recherche
- Les benchmarks, projets jalons et outils sont publiés sur la page TRACTOR du MIT Lincoln Laboratory
- Un contenu associé, Eliminating Memory Safety Vulnerabilities Once and For All, est également lié
1 commentaires
Avis de Hacker News
Pour référence, https://sam.gov/opp/1e45d648886b4e9ca91890285af77eb7/view vaut aussi le détour
Il y est indiqué que l’objectif de cet événement est d’expliquer les objectifs techniques et les défis de TRACTOR, de répondre aux questions des proposants potentiels et de leur donner l’occasion d’examiner comment leurs recherches pourraient s’aligner sur les objectifs du programme TRACTOR
Ça a l’air vraiment difficile. En particulier, le Rust idiomatique écrit par des personnes expérimentées ne ressemble pas du tout à du C, et la plupart des codes intéressants sont écrits en C++
Au final, j’ai l’impression que cela revient à déterminer statiquement la durée de vie de toutes les allocations d’un programme C. Y compris les allocations qui passent par des allocateurs définis par l’utilisateur ou des bibliothèques propriétaires. Il y a eu beaucoup de recherches dans ce domaine depuis longtemps, mais peu de grands succès ; et les programmes C/C++ peuvent lier la durée de vie des allocations au bouton sur lequel l’utilisateur appuie, sans garde-fous comme le comptage de références. Ce n’est pas une bonne idée, mais c’est possible
Un autre problème évident est que le programme analysé est, par définition, du code bogué. Si les durées de vie avaient eu du sens, il n’y aurait pas eu de vulnérabilité de sûreté mémoire et pas besoin de le remplacer. Les recherches visant à détecter statiquement « ce que devraient être les durées de vie » supposent généralement que le code analysé est correct dès le départ. On pourrait viser un programme qui repère les endroits où il est impossible de déterminer la durée de vie et demande de l’aide au développeur
Cela dit, ce n’est pas la première fois que DARPA tente la traduction automatique de programmes, ni la traduction automatique vers Rust[2]
[1]: https://www.urbandictionary.com/define.php?term=DARPA%20hard
[2]: https://c2rust.com/
Ce projet est plutôt impressionnant : il dispose d’un arbre syntaxique abstrait unifié qui prend en charge ces langages avec, en pratique, le même ensemble de types de nœuds. J’ai eu l’occasion de travailler dessus lors d’un stage d’été à Livermore en 2014
Il est aussi open source : http://rosecompiler.org/ROSE_HTML_Reference/index.html
Si le simple fait de se battre avec le borrow checker vous semble déjà difficile, traduire automatiquement vers du code accepté par le borrow checker en tenant compte de tout l’espace des programmes C possibles et de ses fameux comportements indéfinis est encore bien plus dur. L’un des problèmes classiques de l’écriture de compilateurs est que l’espace des programmes valides est bien plus vaste que celui des programmes compilables
Une recherche rapide montre qu’il existe déjà des tentatives comme c2rust[1]. Je me demande en quoi TRACTOR est différent
[1] https://github.com/immunant/c2rust
Ce serait plus intéressant si, au lieu de fournir le programme d’origine, on fournissait son manuel. Mais les manuels décrivent rarement tous les comportements subtils d’un programme, donc il faudra probablement examiner le code source, au moins pour obtenir une référence réelle
Cette initiative proposée, je la connaissais depuis un moment, et c’est intéressant de la voir désormais rendue publique. C’est une proposition très ambitieuse, et une ambition de ce niveau me paraît correspondre à la mission de la DARPA ; j’espère qu’elle réussira
En tant que défenseur de Rust dans ce domaine, j’ai essayé de tempérer les attentes des personnes qui portent cette proposition quant à la possibilité de traduire automatiquement du C vers Rust. L’obstacle fondamental, à mes yeux, est que le code source C contient moins d’informations que le code source Rust. Pour traduire du code C en code Rust, quelqu’un ou quelque chose doit produire les informations manquantes. Pour la même raison qu’agrandir fortement une image ne permet pas de créer sans erreur des bits d’information qui n’étaient pas capturés dans l’image d’origine, on peut facilement démontrer qu’il est impossible de générer parfaitement ces informations manquantes. Au bout du compte, il faut extrapoler — autrement dit inventer — les informations absentes du code source existant. Extrapoler correctement demande du jugement, et c’est un processus qui produira inévitablement des erreurs, en particulier lorsqu’il est effectué à grande échelle par des modèles de langage non supervisés. J’ai déjà proposé une solution qui, selon moi, pourrait atténuer ce problème dans une certaine mesure, mais je n’entrerai pas dans les détails
Au final, je pense que ce projet peut connaître un certain succès, mais il doit être mené avec des attentes prudentes et mesurées. En même temps, il est aussi possible qu’aucun résultat public n’en sorte, donc je dirais qu’il ne faut pas surinterpréter les choses à ce stade. En particulier, le gouvernement n’est pas une entité monolithique ; il ne faut donc pas interpréter ce projet comme un rejet total de C ou comme une bénédiction totale de Rust. Chaque organisme décidera lui-même de l’orientation et du calendrier d’adoption des technologies de sûreté mémoire. Par exemple, le NIST recommande non seulement Rust, mais aussi Ada SPARK ainsi que plusieurs dialectes C/C++ renforcés
https://cpp-rust-assisted-migration.gitlab.io/blog/
Dans les vrais projets de rustification, les choses se passent souvent ainsi. Les fuzzers peuvent aussi aider à générer des données de test supplémentaires et à augmenter la couverture des branches
Personnellement, je n’aime pas l’état d’esprit consistant à dire « réécrivons tout le monde en Rust ». Cela dit, si l’on veut porter un projet vers un nouveau langage ou une nouvelle plateforme, la traduction mécanique est une mauvaise méthode
Il faut prendre le temps de planifier une meilleure architecture et de concevoir un meilleur système logiciel, puis trouver un moyen de remplacer les éléments morceau par morceau. Si l’on construit des châteaux dans le ciel, ils ne toucheront jamais le sol. Si vous avez décidé d’utiliser Rust pour ce système, très bien, mais il faut l’écrire de façon idiomatique en Rust. Il ne faut pas essayer de rétroporter du C vers Rust
À mon avis, un processus bien meilleur et plus mature consiste à mettre à jour le C vers du C moderne, puis à vérifier la sûreté de la mémoire, des ressources et des opérations entières avec un vérificateur de modèles comme CBMC. On peut obtenir une sûreté comparable à celle d’une réécriture progressive en Rust tout en conservant la base de code, la base de connaissances et les développeurs
Je suis d’accord pour dire que le remplacer par une version Rust conçue à la main sur le plan architectural est clairement une meilleure solution, mais elle coûte aussi plus cher. Ici, on semble viser un produit façon RLBox : « une grosse amélioration de sécurité avec presque aucun effort ». Cela ne veut pas dire qu’il ne faut pas faire une réécriture entièrement manuelle si l’on en a les ressources, mais que c’est mieux que de ne rien faire
Si l’on demande à ChatGPT de résoudre le problème initial, il a plus souvent juste, mais même là, cela ne marche généralement pas très bien. Il faut encore beaucoup de retouches et de réflexion pour que même les éléments de base produits fonctionnent
Pas l’attrait d’une idée évidemment bonne. Le « Rust médiocre » produit par traduction risque fort d’être bien pire à travailler que du C avec un bruit de fond de bugs. Grâce à la fidélité de la traduction, ces bugs seront aussi présents dans le « Rust médiocre ». En C, les gens savent plus ou moins comment gérer les problèmes, tandis que le « Rust médiocre » est littéralement médiocre parce que les développeurs Rust ne sont pas habitués à ce genre de choses
Malgré tout, la probabilité que quelqu’un développe une technique permettant de nettoyer cela de manière répétée jusqu’à un état supportable n’est pas nulle. Et le projet pourrait aussi produire, comme résultat non visé, des retours de linter du type « impossible de traduire en Rust supportable à cause de x, y, z ». Les développeurs C pourraient les examiner ; et si le code devient traduisible en bon Rust, il y aurait d’autant moins de raisons de le traduire
Si ce genre de résultat apparaît, il pourrait être plus simple pour certaines personnes de décrire leur solution en C exécutable et de laisser le « traducteur/linter » les guider vers une approche non cassée. Je trouve ces résultats positifs assez peu probables, mais faire de temps en temps des paris dark horse ressemble assez à la fiche de poste de la DARPA
L’essentiel est de commencer à réduire les CVE de sûreté mémoire dont il a été prouvé à de multiples reprises qu’elles posent réellement problème
Je suis d’accord pour dire que si l’on pouvait traduire automatiquement et fiablement du C/C++ vers Rust, cela aurait déjà été fait. Mais quelque part dans le processus consistant à planifier une meilleure architecture et de meilleurs systèmes, puis à remplacer les éléments morceau par morceau, les gens peuvent — et vont effectivement — prendre confiance et se dire : « maintenant que nous écrivons beaucoup mieux le C, nous ne créerons plus de bugs de sûreté mémoire ; ne peut-on pas s’arrêter ici ? ». Puis, six mois plus tard, une nouvelle CVE de buffer overflow mi-ridicule mi-tragique apparaît
Mettre à jour le C vers du C moderne et le vérifier avec CBMC représente aussi un investissement énorme. À ce niveau-là, autant passer à Rust. Dire que l’on obtient la même sûreté qu’avec une réécriture progressive en Rust est peut-être possible, mais cela semble assez incertain ou loin d’être une conclusion claire
Beaucoup lisent cela comme une demande ou une injonction à traduire tout le code C et C++ en Rust, mais malgré le nom de projet accrocheur, le résumé ne se lit pas ainsi. Il y a deux paragraphes liés, mais distincts
Premièrement, C et C++ ne sont pas suffisamment sûrs à grande échelle. Même en programmant avec prudence et en utilisant de bons outils, leur conception fondamentalement non sûre entraîne trop de vulnérabilités. Il faut donc traduire ou réécrire autant de code que possible dans des langages « sûrs », en particulier des langages garantissant la sûreté mémoire
Deuxièmement, il s’agit de financer et de lancer un appel à projets pour des logiciels qui traduisent du code C existant vers Rust
Ce n’est pas un consensus pour réécrire le monde en Rust. C’est un consensus pour migrer vers des langages sûrs ; Rust en est un exemple, et il existe un programme visant Rust dans le cadre de cette migration
unsafe, quelles sont les règles pour l’utiliser ? Sans un ensemble de règles défini à appliquer ici, on ne fait que revenir au point de départRust a un mode sûr. Cela ne veut pas dire que Rust est un langage sûr. Pour faire des choses intéressantes, il faut des blocs
unsafe. On n’y gagne pas grand-choseD’un autre côté, il existe beaucoup de langages avec garbage collector qui empêchent même les programmeurs de manipuler des pointeurs. Pourquoi ces langages ne sont-ils pas envisagés ? La raison, c’est la performance. Parce que les programmeurs Rust « accordent tant d’importance » à la performance, ce langage ne pourra jamais résoudre le problème de fond
Vous voulez la performance, ou vous voulez la sûreté ? Vous ne pouvez pas avoir les deux
C’est vraiment étonnant que cela puisse fonctionner avec quelque forme d’automatisation que ce soit. On ne peut pas transcrire ligne par ligne un programme C classique en Rust
Dans les programmes C, les pointeurs et les alias sont partout, et Rust empêche explicitement ce genre de concepts. À moins d’envelopper l’ensemble dans
unsafe, réécrire un programme C en Rust exige de repenser à haut niveau beaucoup de structures typiquesIl n’est pas non plus nécessaire de tout traduire d’un coup. L’une des valeurs du compilateur Rust est qu’il fournit beaucoup d’informations concrètes que l’on peut réinjecter dans un LLM pour itérer
Dans ma startup grit.io, nous avons travaillé sur des problèmes similaires, et je pense que C -> Rust sera tout à fait abordable dans un avenir proche. Ce n’est clairement pas facile, mais c’est un problème soluble
Si oui, on peut traiter cela comme un sous-problème, et faire de la détection de la manière dont les données sont partagées entre threads un autre problème. Dans ce dernier cas, beaucoup de programmes auront des règles de propriété implicites du type « le thread T1 met dans la file Q, et le thread T2 récupère ». Cela peut se traduire par « quand on met dans la file, la propriété est transférée »
Détecter ce genre de règles ne sera pas facile, mais cela ne semble pas totalement hors de portée non plus, et ce serait tout à fait pertinent comme projet de recherche
unsafe, compiler vers le même binaire, puis retirer progressivementunsafetout en vérifiant continuellement l’équivalence ?Ainsi, on ferait au moins passer autant de parties que possible en Rust, et les ingénieurs pourraient traiter séparément les concepts qui doivent être repensés
unsafeJe suppose que pour produire un programme réellement utile et valide, l’IA doit comprendre les durées de vie, etc. Si elle y parvient vraiment, ce serait impressionnant
On peut obtenir des résultats similaires en C/C++ si l’on respecte des pratiques de codage très strictes et que l’on intègre des outils tiers de vérification statique exigeant d’ajouter au code une quantité de commentaires propriétaires
Ou alors on peut simplement utiliser Rust
Qui, avec toute sa tête, défendrait un langage présentant des défauts de conception aussi énormes et évidents alors qu’il existe une vraie alternative
Je ne pense pas que cela marchera bien. C possède des abstractions qu’on ne peut pas reproduire en Rust sans changements importants
En C, il est courant que deux structures de données différentes contenant le même pointeur écrivent via ce pointeur. Ce n’est pas trivial à reproduire en Rust et cela nécessite une intervention assez intelligente
Mais on n’obtiendra pas une sortie avec moins de bugs, ni un processus générant automatiquement les structures de programme qui font la fiabilité de Rust
Est-ce que cela doit être automatique ? Si oui, un programme capable de porter automatiquement du C vers Rust ne devrait-il pas nécessairement inclure toutes les fonctionnalités permettant de rendre le code C lui-même sûr ?
Certains codes C ne sont pas simplement « impossibles à vérifier comme corrects », ils sont « effectivement faux du point de vue de la sûreté mémoire ». Ce genre de code ne sera pas traduit automatiquement sans intervention humaine. Comment serait-ce possible s’il n’existe pas de code équivalent correct ? L’outil a besoin d’une échappatoire lui permettant de dire : « Je ne sais pas ce que ce code est censé faire, et ce qu’il fait réellement viole le contrat avec le compilateur, donc je sais aussi que ce n’est pas l’intention. Humain, aide-moi »
En théorie, il est impossible de faire en sorte que cette échappatoire ne soit utilisée que lorsqu’un comportement indéfini se produit réellement. À cause du théorème de Rice. En pratique aussi, il ne faut pas traduire aveuglément du code excessivement obscur, donc la tentative même de le faire n’est probablement pas souhaitable
Au final, j’imagine que l’outil ressemblera à un outil interactif qui fait la majeure partie du travail, mais avec un guidage humain. Et le résultat de ce guidage humain sera un code qui accomplit le même objectif, pas un code exactement équivalent
Si 1) Rust n’a pas de bugs mémoire et 2) il est possible de traduire automatiquement du C en Rust, alors cela signifie qu’on pourrait corriger automatiquement tous les bugs mémoire.
Ce n’est presque certainement pas vrai. Il est très probable que cette tâche soit totalement impossible dans le cas général.
Par exemple, il suffit de remplacer toutes les allocations mémoire dynamiques par un tampon fixe déterminé au moment de la traduction, puis de rejeter toutes les entrées qui ne tiennent pas dans ce tampon.