3 points par GN⁺ 2024-07-31 | 1 commentaires | Partager sur WhatsApp
  • 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

1 commentaires

 
GN⁺ 2024-07-31
Avis de Hacker News
  • Pour référence, https://sam.gov/opp/1e45d648886b4e9ca91890285af77eb7/view vaut aussi le détour

    • Lien direct vers le PDF d’information pour les proposants : https://sam.gov/api/prod/opps/v3/opportunities/resources/fil...
      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

    • C’est une tâche très difficile, et DARPA aime financer les tâches difficiles[1]
      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/
    • Puisqu’on parle de tâches difficiles, le DOE finance aussi depuis plus de 20 ans un projet appelé ROSE. Il couvre l’analyse statique et la traduction automatique entre C/C++/Cuda, mais aussi des langages de plus haut niveau comme Python et des variantes de C/C++ pour le HPC
      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
    • Dans le cas général, je pense que ce sera traduit en unsafe Rust, avec parfois seulement quelques nœuds terminaux isolés traduits en Rust sûr
      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 n’est pas pour rien que les défis DARPA sont qualifiés de DARPA-hard
    • L’approche sera probablement quelque chose comme : « l’IA résume les fonctionnalités du programme dans un langage de spécification, puis synthétise du code Rust couvrant le même ensemble de fonctionnalités »
      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

    • Quel est le rapport avec l’effort CRAM de Grammatech ?
      https://cpp-rust-assisted-migration.gitlab.io/blog/
    • Si l’on ne cherche pas à préserver la sémantique formelle du code C, et que l’on considère qu’il suffit que la traduction passe la suite de tests après coup, cela laisse beaucoup plus de latitude à la traduction
      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

    • Aucune chance. CBMC est étonnant, mais avez-vous déjà fait de la vérification formelle sur un « vrai » programme ?
      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
    • Le C moderne présente exactement les mêmes failles de sécurité dans les tableaux et les chaînes que le C classique, et rien n’a changé en 50 ans
    • C’est clairement un défi DARPA très spéculatif, du genre qu’on aimerait avoir pour sortir de systèmes legacy. Mais même si l’on met une fonction ou une méthode d’un langage dans ChatGPT et qu’on lui demande de la traduire dans un autre langage, cela ne marche généralement pas très bien
      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
    • Pour du code dormant, exécuté partout mais que personne ne touche, l’approche « traduire en Rust médiocre avant d’y retoucher » a un certain attrait
      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’état d’esprit « réécrivons tout le monde en Rust » n’existe nulle part. Il existe énormément de logiciels qu’il vaut bien mieux laisser dans des langages dynamiques ou dans des langages statiquement typés avec ramasse-miettes, comme Go. Un bon ingénieur comprend qu’il faut utiliser le bon outil pour la tâche
      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

    • Si ces langages disposent d’une construction 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épart
      Rust 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-chose
      D’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 typiques

    • La traduction ligne par ligne est impossible, donc il faut utiliser l’IA pour effectuer un raisonnement sémantique plus large
      Il 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
    • En dehors des programmes multithread, les alias de longue durée sont-ils vraiment partout dans les programmes C ? Dans beaucoup de programmes, je m’attendrais à ce que la plupart des cas se produisent dans le périmètre d’une seule fonction, ou à travers des appels de fonctions à l’intérieur de celui-ci. Dans ce cas, cela ne se résout-il pas par l’emprunt ?
      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
    • Question naïve de débutant : ne pourrait-on pas procéder ligne par ligne, envelopper le tout dans unsafe, compiler vers le même binaire, puis retirer progressivement unsafe tout 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
    • Une traduction ligne par ligne n’a même pas besoin de beaucoup d’« IA ». Il me semble possible de produire une traduction Rust approximative, majoritairement unsafe
      Je 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
    • Je me demande surtout ce que cela donnerait dans des codebases qui utilisent beaucoup de macros
  • 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

    • C’est assez drôle de voir la communauté C/C++ réagir contre Rust. Des décennies d’efforts et d’expérience avec ces langages ont clairement réécrit les circuits de raisonnement
      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 voulais écrire commentaires propriétaires, pas « situation propriétaire ». C’est l’autocorrection de mon téléphone
  • 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

    • On pourra probablement produire quelque chose qui compile et qui soit à la fois « Rust » et « IA ». Cela coche deux fois les cases des buzzwords, donc c’est aussi pratique pour justifier un projet de recherche
      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 ?

    • Lu raisonnablement, cette phrase ne signifie probablement pas une automatisation complète, et dans ce cas la réponse à la question est non
      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.

    • Comme il n’a pas été précisé qu’il fallait préserver tous les comportements du code C, il existe une solution triviale à ce problème.
      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.