Récemment, Herb Sutter a publié un excellent article sur la sécurité du C++. Il y aborde de nombreuses idées ; voici un résumé de sa vision de ce qui peut être fait à moyen terme pour renforcer la sécurité du C++.
| En C++, appliquer par défaut… | (A) Solution pour le code nouveau/modifié (peut exiger des changements de code — aucun changement de lien/binaire) | (B) Solution pour le code existant (recompilation uniquement — aucune modification manuelle du code, aucun changement de lien/binaire) |
| La sûreté des types | Interdire tous les casts et conversions intrinsèquement non sûrs | Faire en sorte que les casts et conversions non sûrs disposant d’une alternative sûre adoptent le comportement sûr |
| La sûreté des bornes | Interdire l’arithmétique des pointeurs Interdire l’arithmétique non vérifiée des itérateurs | Vérifier les bornes de toute arithmétique d’itérateur autorisée Vérifier les bornes de toutes les opérations d’indexation |
| La sûreté de l’initialisation | Exiger que toutes les variables soient initialisées (à la déclaration ou avant la première utilisation) | — |
| La sûreté des durées de vie | Diagnostiquer statiquement de nombreux cas courants d’erreurs de durée de vie des pointeurs/itérateurs | Vérifier la non-nullité de tous les déréférencements de pointeurs |
| Moins de comportements indéfinis | Diagnostiquer statiquement les cas connus d’UB/bugs, pour signaler les vrais bugs du code existant avec une simple recompilation et zéro faux positif : Interdire les chaînes de comparaisons mathématiquement invalides (ajouter d’autres cas issus de la revue de l’annexe sur les UB) | Corriger automatiquement les cas connus d’UB/bugs, pour que les bugs actuels du code existant deviennent réellement corrects avec une simple recompilation et zéro faux positif : Définir les chaînes de comparaisons mathématiquement valides return *this; par défaut pour les opérateurs d’affectation de C qui retournent C& (ajouter d’autres cas issus de la revue de l’annexe sur les UB) |
Mais quelles sont les options actuellement disponibles pour atteindre cet objectif ?
La sûreté des types
Pour imposer la sûreté des types en C++, vous pouvez utiliser la commande de compilation suivante avec divers flags pour activer la vérification stricte des types et d’autres fonctionnalités de sécurité :
g++ -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wshadow -Werror -o output_file source_file.cpp
Voici ce que font ces flags :
-Wall: Active tous les avertissements courants.-Wextra: Active des avertissements supplémentaires.-Wpedantic: Impose la conformité stricte à l’ISO C++.-Wconversion: Avertit des conversions implicites susceptibles de modifier une valeur.-Wsign-conversion: Avertit des conversions implicites entre types signés et non signés.-Wshadow: Avertit si une déclaration de variable en masque une autre d’une portée englobante.-Werror: Traite tous les avertissements comme des erreurs, stoppant la compilation si des avertissements sont présents.
La sûreté des bornes
Pour imposer la sûreté des bornes en C++, vous pouvez utiliser l’option -fsanitize=bounds avec gcc ou clang. Ce flag active des vérifications à l’exécution des accès hors limites aux tableaux. Voici un exemple de commande de compilation :
g++ -fsanitize=bounds -o my_program my_program.cpp
Cette commande compile my_program.cpp avec les vérifications de bornes activées et produit un exécutable nommé my_program. Si un accès hors limites survient pendant l’exécution, le sanitizer le signalera.
La sûreté de l’initialisation
Pour imposer la sûreté de l’initialisation en C++, vous pouvez utiliser plusieurs flags du compilateur qui aident à détecter les variables non initialisées et autres problèmes connexes. Voici les commandes pour GCC et Clang :
GCC
g++ -Wall -Wextra -Wuninitialized -Wmaybe-uninitialized -o your_program your_program.cpp
Clang
clang++ -Wall -Wextra -Wuninitialized -o your_program your_program.cpp
Explication
-Wall: Active tous les messages d’avertissement couramment utilisés.-Wextra: Active des flags d’avertissement supplémentaires non couverts par-Wall.-Wuninitialized: Avertit des variables non initialisées.-Wmaybe-uninitialized: Avertit des variables potentiellement non initialisées.
La sûreté des durées de vie
Pour imposer la sûreté des durées de vie en C++, vous devez utiliser des flags spécifiques du compilateur et éventuellement activer certaines fonctionnalités ou certains outils conçus pour l’analyse des durées de vie et les vérifications de sécurité. Suite à des développements récents, le flag -fsanitize=address avec le compilateur Clang ou GCC peut aider à détecter les problèmes de durée de vie. Vous pouvez aussi utiliser l’analyseur statique de Clang ou les outils Microsoft de Visual Studio pour des vérifications plus complètes.
Voici quelques commandes utilisables pour GCC et Clang :
GCC
g++ -fsanitize=address -fno-omit-frame-pointer -g -o your_program your_program.cpp
Clang
clang++ -fsanitize=address -fno-omit-frame-pointer -g -o your_program your_program.cpp
Ces commandes activent AddressSanitizer, qui aide à détecter diverses erreurs mémoire, dont les problèmes d’use-after-free, use-after-return et use-after-scope, critiques pour imposer la sûreté des durées de vie.
Les comportements indéfinis
Pour imposer la sécurité face aux comportements indéfinis en C++, vous pouvez utiliser divers flags et outils du compilateur. Voici une commande de compilation utilisant g++ (de la GNU Compiler Collection) qui inclut des flags courants pour aider à détecter et prévenir les comportements indéfinis :
g++ -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined -fstack-protector-all -O2 -g your_file.cpp -o your_program
Voici le détail des flags utilisés :
-Wall: Active tous les messages d’avertissement couramment utilisés.-Wextra: Active des messages d’avertissement supplémentaires non inclus par-Wall.-Werror: Traite tous les avertissements comme des erreurs, vous forçant à les corriger.-pedantic: Impose la conformité stricte à l’ISO C++.-fsanitize=address,undefined: Active AddressSanitizer et UndefinedBehaviorSanitizer pour détecter les erreurs mémoire et les comportements indéfinis à l’exécution.-fstack-protector-all: Ajoute une protection de la pile pour détecter les débordements de tampon de pile.-O2: Active l’optimisation (niveau 2), un bon équilibre entre performance et débogage.-g: Inclut les informations de débogage dans le binaire pour l’utilisation avec un débogueur (par exemplegdb).
Les limites des sanitizers C++
Comme on peut le voir, les sanitizers pourraient répondre à certains des problèmes signalés par Herb Sutter. Cependant, ils présentent aussi plusieurs inconvénients :
1- Le surcoût de performance
- Les performances à l’exécution: Les sanitizers introduisent un surcoût significatif à l’exécution. AddressSanitizer, par exemple, peut ralentir l’exécution d’un programme d’un facteur deux à trois.
- L’usage mémoire: Les sanitizers, en particulier AddressSanitizer, augmentent considérablement l’usage mémoire. Cela peut être problématique dans les environnements à mémoire contrainte.
2- Les problèmes de compatibilité
- Le support des plateformes: Tous les sanitizers ne sont pas disponibles sur toutes les plateformes ni tous les compilateurs, ce qui peut limiter leur usage dans les projets multiplateformes.
- Les bibliothèques tierces: Utiliser des sanitizers avec des bibliothèques tierces qui n’ont pas été compilées avec la sanitisation à l’esprit peut entraîner des problèmes de compatibilité ou des erreurs parasites.
3- La complexité du build et de l’exécution
- La complexité du processus de build: Intégrer les sanitizers au processus de build peut rendre la configuration de build plus complexe, surtout avec plusieurs types de build (release contre debug, par exemple).
- Un environnement d’exécution spécial: Exécuter des tests sous sanitizers exige souvent un environnement d’exécution spécial et une configuration supplémentaire, rendant le processus plus lourd.
4- Une portée limitée
- Des types de bugs spécifiques: Les sanitizers sont conçus pour détecter des types de bugs spécifiques (erreurs mémoire, comportements indéfinis), et ils peuvent manquer les erreurs de logique, les inefficacités algorithmiques ou d’autres sortes de bugs.
5- La complexité du débogage
- Des rapports détaillés: Si les rapports détaillés des sanitizers sont utiles, ils peuvent parfois être impressionnants ou difficiles à interpréter, en particulier pour les bases de code vastes et complexes.
- La reproductibilité: Certains problèmes signalés par les sanitizers peuvent être non déterministes et difficiles à reproduire, ce qui complique le débogage.
Il est clair que nous avons besoin de mécanismes supplémentaires pour améliorer la sécurité sans compromettre les performances du C++. Cet objectif est devenu une préoccupation hautement prioritaire pour le comité C++, et nous espérons avoir bientôt une solution définitive à ce problème urgent.
