TrustInSoft Paris à fond derrière le langage Rust
Les outils d’analyse statique faciliteront le développement de systèmes embarqués utilisant le langage Rust, explique Fabrice Derepas, cofondateur et évangéliste en chef de TrustInSoft à Paris (France).
Les langages C et C++ continuent de dominer le paysage des systèmes embarqués. Mais les développeurs sont également conscients de la manière dont l’utilisation de ces langages peut entraîner des problèmes au cours du développement. Leur traitement des pointeurs et des objets similaires peut entraîner de graves problèmes de sécurité de la mémoire.
Rust offre une syntaxe similaire, mais il est très prometteur en tant que langage offrant une grande partie de la flexibilité de ces anciens langages, mais avec des garanties de sécurité beaucoup plus solides. Rust utilise des éléments issus des langages fonctionnels et d’autres concepts avancés qui sont désormais enseignés aux jeunes développeurs de logiciels à l’université.
Mais un facteur important de la croissance du soutien à Rust est qu’il surmonte de nombreux problèmes liés à la mémoire rencontrés par les programmeurs C et C++, ainsi que par les utilisateurs de leur code. Ces caractéristiques aident Rust à devenir un choix stratégique pour le développement de nouveaux modules logiciels dans les systèmes à haute criticité, tels que ceux que l’on trouve dans les secteurs de l’automobile, de l’industrie et autres.
Rust bénéficie d’un large soutien: plusieurs des plus grands éditeurs de logiciels au monde sont déjà de grands utilisateurs de Rust, grâce à l’accent mis sur la fiabilité et la sécurité de la mémoire.
Fin 2022, Rust est devenu le premier langage à être soutenu par la communauté Linux, aux côtés du C, pour le développement de modules de noyau. Le langage Rust a connu un nouvel essor en début d’année avec la publication d’un rapport du bureau du directeur national de la cybernétique de la Maison-Blanche, qui préconise l’utilisation d’un langage à mémoire sécurisée, tel que Rust, pour se prémunir contre les cyber-attaques.
Problèmes de mémoire
Une différence importante entre C/C++ et Rust réside dans le traitement des pointeurs. Le pointeur est un moyen simple de manipuler des données en mémoire. Mais la facilité avec laquelle les pointeurs peuvent être créés et modifiés par un programme C ou C++ rend leur utilisation risquée.
Par exemple, une fonction d’un programme peut définir un pointeur d’accès à la mémoire dans un tampon temporaire alloué par le système d’exploitation. Les tentatives d’utilisation de ce pointeur si une autre partie du code a déjà désalloué la mémoire entraîneront probablement une corruption des données. L’échec du programme pourrait s’ensuivre rapidement. De même, un pointeur nul qui a été défini mais qui n’a pas été initialisé correctement doit générer une faute d’accès à la mémoire lorsqu’il est utilisé. Cela entraînera souvent l’échec du programme.
Les programmes peuvent également ne pas désallouer la mémoire lorsqu’elle n’est plus nécessaire. Dans un programme de longue durée, de telles fuites de mémoire conduisent à l’instabilité du système lorsque le système d’exploitation ne trouve pas de mémoire libre à allouer à d’autres objets.
Il existe d’autres risques. Si l’adresse contenue dans un pointeur dépasse les limites de la mémoire allouée à la structure de données ou à la mémoire tampon, il en résulte souvent une dangereuse corruption des données. Les pirates informatiques exploitent cette propriété avec des attaques par débordement de mémoire tampon. Les écritures illégales effectuées par débordement de mémoire tampon sont restées la vulnérabilité la plus courante dans l’édition 2022 de l’énumération des faiblesses communes (CWE) de Mitre.
Un choix plus sûr
Les utilisateurs de Rust peuvent éviter bon nombre de ces problèmes et d’autres problèmes liés à la sécurité de la mémoire en tirant parti de ses règles strictes et de son support intégré pour l’allocation de la mémoire. Les vérifications à la compilation permettent de garantir le comportement correct des références. Rust possède des types de données qui offrent des caractéristiques similaires à celles des pointeurs, mais qui sont pris en charge par des vérifications à la compilation. Ces contrôles permettent d’éviter les problèmes rencontrés avec les pointeurs de type C.
Le modèle de mémoire pris en charge par Rust garantit également que les structures de mémoire temporaires seront supprimées en toute sécurité lorsqu’elles ne seront plus nécessaires au programme. En fournissant des structures et des techniques de manipulation sans danger pour la mémoire, Rust peut accélérer le développement et le test des logiciels. En outre, il exploite les compétences que les développeurs formés à l’université acquièrent aujourd’hui. Ces deux facteurs sont importants dans des secteurs tels que l’automobile, où le contenu logiciel des véhicules augmente rapidement.
La nécessité d’un héritage
Cependant, la réutilisation des modules de code existants est tout aussi importante pour les organisations qui développent des systèmes à haute criticité. Le développement de systèmes critiques pour la sécurité doit être prudent. Les systèmes existants ne doivent être modifiés qu’en cas de nécessité. Il n’est pas pratique, voire pas souhaitable, de réécrire des modules dans un nouveau langage, même si ses mécanismes de protection offrent des avantages significatifs par rapport aux anciens langages C ou C++. Ces modules existants devront être vérifiés une fois intégrés dans une cible qui comprend de grandes parties écrites en Rust ou dans un langage à sécurité mémoire similaire.
Il existe également des situations où, même si un langage offre de solides garanties de comportement, les ingénieurs doivent procéder à des vérifications supplémentaires pour assurer la sécurité. C’est particulièrement vrai pour le contrôle embarqué. De nombreuses interactions de bas niveau, telles que l’accès à des registres matériels ou à des tampons de données en mémoire, ne peuvent pas être facilement effectuées en utilisant les références de Rust et d’autres éléments similaires sûrs pour la mémoire. Les programmeurs peuvent gérer ces interactions en Rust en utilisant des pointeurs bruts. Ces pointeurs se comportent de manière similaire à ceux fournis par C et C++. Mais sans les protections de Rust, ils nécessitent des vérifications supplémentaires.
Competition to convert legacy C code to Rust automatically with GenAI
Analyse statique avancée
L’analyse statique effectuée par le compilateur Rust est conservatrice et inévitablement en raison des limites de la profondeur d’analyse qu’il peut effectuer. Le code qui déréférence un pointeur brut en Rust déclenchera une erreur de compilation à moins que les développeurs n’encapsulent ce code dans des blocs unsafe{}. Ce marquage indique au compilateur de ne pas effectuer ses contrôles de sécurité habituels lors de la compilation de ce code. Par conséquent, il ne fournit aucune garantie de sécurité de la mémoire.
Il existe d’autres situations dans lesquelles les développeurs doivent utiliser des blocs unsafe{}. Sans eux, le compilateur refusera tout appel à des fonctions ou méthodes non sûres, ainsi que le code qui tente d’accéder aux champs des unions. Bien que les unions ne soient pas une fonctionnalité essentielle de Rust, leur prise en charge peut être importante pour assurer la compatibilité avec les langages C et C++. Le compilateur ne peut pas garantir la sécurité des opérations sur les champs car il ne peut pas déterminer si les écritures sur un champ corrompent ou non les autres champs qui partagent la même structure de mémoire.
Il existe de nombreuses situations où les marquages unsafe sont nécessaires dans le code natif de Rust. Dans la bibliothèque générale utilisée par Rust, environ 30 % des paquets de la collection de crates.io utilisent la construction unsafe{}. Un compilateur ne peut pas vérifier la sécurité des opérations dans ces paquets.
Cependant, il existe des techniques de vérification formelle et de raisonnement mathématique qui permettent d’analyser le code avant son exécution. Elles peuvent déterminer si le code souffrira de problèmes de sécurité de la mémoire tels que les débordements de mémoire tampon, les accès aux pointeurs nuls et d’autres problèmes. Les techniques d’analyse statique sont en quelque sorte exhaustives et fournissent des garanties que même des tests dynamiques approfondis ne peuvent offrir.
Les développeurs travaillent sur des outils pour Rust qui peuvent mettre en évidence les problèmes détectés, indiquant où les programmeurs doivent insérer des protections supplémentaires telles que des vérifications sur la plage d’adresse d’une référence de pointeur. Comme Rust devra souvent coexister avec des modules C et C++ issus de développements antérieurs, des outils tels que TrustInSoft Analyzer seront utilisés pour s’assurer que la base de code combinée est exempte de problèmes de mémoire.
Même dans les modules qui s’appuient pleinement sur les caractéristiques de sécurité de la mémoire de Rust et qui n’incluent pas de code à l’intérieur de blocs unsafe{}, il sera important de tester le comportement du programme de manière exhaustive avant de le déployer. Les erreurs qui sont détectées par Rust au moment de l’exécution mettent souvent fin au programme, ce qui n’est pas acceptable dans les systèmes à haute criticité. Les outils statiques peuvent examiner la probabilité que ces situations se produisent et avertir l’équipe de développement afin que tout problème soit corrigé avant la sortie du produit.
Certains comportements sont également définis mais non souhaités. Par exemple, une division par zéro ou certaines erreurs peuvent déclencher un état de « panique », ce qui peut conduire le système à se bloquer. La détection de tous ces comportements indésirables est également un élément clé de l’utilisation d’un analyseur statique avancé.
La vérification d’un comportement correct peut être renforcée par la génération automatique d’assertions permettant de vérifier si un code écrit dans un langage quelconque peut traiter en toute sécurité des entrées inattendues ou hors limites. Ce type de test émule le « fuzzing » souvent utilisé par les pirates informatiques pour identifier les vulnérabilités.
Ces outils fourniront une preuve formelle et vérifiable de l’absence de vulnérabilités en matière de sécurité de la mémoire qui pourraient entraîner un comportement imprévisible et dangereux des véhicules critiques en matière de sécurité. Pour éviter que les développeurs ne soient submergés d’erreurs potentielles, il existe des outils qui ont été conçus pour limiter le nombre de faux positifs afin de s’assurer qu’ils ne signalent que le code susceptible de souffrir de problèmes de sécurité de la mémoire.
Alors que Rust continue à faire des incursions dans le développement de systèmes hautement critiques, il sera toujours nécessaire de vérifier que les modules de code externes et les fonctions de bas niveau ne présentent pas de problèmes latents susceptibles de perturber les opérations sur le terrain. L’utilisation de tests statiques et de vérifications supplémentaires garantit que les développeurs détecteront et corrigeront les comportements non définis dès le début du cycle d’intégration, et bien avant le déploiement.