Sixième vérification du chrome, postface

licorne sévère

Début 2018, notre blog a été complété par une série d'articles sur la sixième vérification du code source du projet Chromium. La série comprend 8 articles sur les erreurs et des recommandations pour leur prévention. Deux articles ont suscité une discussion animée et je reçois encore occasionnellement des commentaires par courrier sur des sujets qui y sont traités. Je devrais peut-être donner des explications supplémentaires et, comme on dit, remettre les pendules à l'heure.

Un an s'est écoulé depuis la rédaction d'une série d'articles sur une vérification régulière du code source du projet Chromium:

  1. Chrome: la vérification du sixième projet et 250 bogues
  2. Nice Chrome et maladroit Memset
  3. rupture et retombée
  4. Chrome: fuites de mémoire
  5. Chrome: Typos
  6. Chrome: utilisation de données non fiables
  7. Pourquoi il est important de vérifier ce que la fonction malloc a renvoyé
  8. Chrome: autres erreurs

Les articles consacrés au memset et au malloc ont provoqué et continuent de provoquer des débats qui me paraissent étranges. Apparemment, il y avait une certaine confusion due au fait que je n'avais pas été suffisamment précis lors de la verbalisation de mes pensées. J'ai décidé de revenir sur ces articles et d'apporter quelques précisions.

memset


Commençons par un article sur memset , car ici tout est simple. Certains arguments sont apparus sur la meilleure façon d'initialiser les structures. De nombreux programmeurs ont écrit qu'il valait mieux donner la recommandation de ne pas écrire:

HDHITTESTINFO hhti = {}; 

mais d'écrire de la manière suivante:

 HDHITTESTINFO hhti = { 0 }; 

Raisons:

  1. La construction {0} est plus facile à remarquer lors de la lecture du code, que {}.
  2. La construction {0} est plus intuitivement compréhensible que {}. Ce qui signifie que 0 suggère que la structure est remplie de zéros.

En conséquence, les lecteurs me suggèrent de modifier cet exemple d'initialisation dans l'article. Je ne suis pas d'accord avec les arguments et je n'ai pas l'intention de faire de modifications dans l'article. Je vais maintenant expliquer mon opinion et donner quelques raisons.

Quant à la visibilité, je pense, c'est une question de goût et d'habitude. Je ne pense pas que la présence de 0 entre parenthèses change fondamentalement la situation.

Quant au deuxième argument, je suis totalement en désaccord avec lui. L'enregistrement de type {0} donne une raison de percevoir incorrectement le code. Par exemple, vous pouvez supposer que si vous remplacez 0 par 1, tous les champs seront initialisés avec ceux. Par conséquent, un tel style d'écriture est plus susceptible d'être nuisible qu'utile.

L'analyseur PVS-Studio a même un V1009 de diagnostic connexe , dont la description est citée ci-dessous.

V1009. Vérifiez l'initialisation de la baie. Seul le premier élément est initialisé explicitement.

L'analyseur a détecté une erreur possible liée au fait que lors de la déclaration d'un tableau, la valeur n'est spécifiée que pour un élément. Ainsi, les éléments restants seront implicitement initialisés par zéro ou par un constructeur par défaut.

Prenons l'exemple du code suspect:

 int arr[3] = {1}; 

Peut-être que le programmeur s'attendait à ce que arr soit entièrement composé de uns, mais ce n'est pas le cas. Le tableau sera composé des valeurs 1, 0, 0.

Code correct:

 int arr[3] = {1, 1, 1}; 

Une telle confusion peut se produire en raison de la similitude avec la construction arr = {0} , qui initialise l'ensemble du tableau avec des zéros.

Si de telles constructions sont activement utilisées dans votre projet, vous pouvez désactiver ce diagnostic.

Nous vous recommandons également de ne pas négliger la clarté de votre code.

Par exemple, le code de codage des valeurs d'une couleur est enregistré comme suit:

 int White[3] = { 0xff, 0xff, 0xff }; int Black[3] = { 0x00 }; int Green[3] = { 0x00, 0xff }; 

Grâce à l'initialisation implicite, toutes les couleurs sont spécifiées correctement, mais il est préférable de réécrire le code plus clairement:

 int White[3] = { 0xff, 0xff, 0xff }; int Black[3] = { 0x00, 0x00, 0x00 }; int Green[3] = { 0x00, 0xff, 0x00 }; 

malloc


Avant de poursuivre la lecture, rappelez-vous le contenu de l'article " Pourquoi il est important de vérifier ce que la fonction malloc a renvoyé ". Cet article a donné lieu à de nombreux débats et critiques. Voici quelques-unes des discussions: reddit.com/r/cpp , reddit.com/r/C_Programming , habr.com (en). Parfois, les lecteurs m'envoient encore un e-mail à propos de cet article.

L'article est critiqué par les lecteurs pour les points suivants:

1. Si malloc a retourné NULL , il est préférable d'arrêter immédiatement le programme plutôt que d'écrire un tas de if -s et d'essayer de gérer la mémoire, ce qui rend l'exécution du programme souvent impossible de toute façon.

Je n'ai pas poussé au combat jusqu'à la fin avec les conséquences d'une fuite de mémoire, en passant l'erreur de plus en plus haut. S'il est permis à votre application de terminer son travail sans avertissement, laissez-le. À cet effet, même une seule vérification juste après malloc ou en utilisant xmalloc est suffisante (voir le point suivant).

Je m'y suis opposé et j'ai mis en garde contre le manque de contrôles à cause duquel le programme continue de fonctionner comme si de rien n'était. C'est un cas complètement différent. Il est dangereux, car il conduit à un comportement indéfini, à une corruption des données, etc.

2. Il n'y a pas de description d'une solution qui consiste à écrire des fonctions d'encapsuleur pour allouer de la mémoire avec un contrôle à la suite ou à l'aide de fonctions déjà existantes, telles que xmalloc .

D'accord, j'ai raté ce point. En écrivant l'article, je ne pensais tout simplement pas à la façon de remédier à la situation. Il était plus important pour moi de transmettre au lecteur le danger de l'absence de chèque. Comment corriger une erreur est une question de goût et de détails d'implémentation.

La fonction xmalloc ne fait pas partie de la bibliothèque C standard (voir " Quelle est la différence entre xmalloc et malloc? "). Cependant, cette fonction peut être déclarée dans d'autres bibliothèques, par exemple, dans la bibliothèque GNU utils ( GNU libiberty ).

Le point principal de la fonction est que le programme se bloque lorsqu'il n'alloue pas de mémoire. La mise en œuvre de cette fonction pourrait ressembler à ceci:

 void* xmalloc(size_t s) { void* p = malloc(s); if (!p) { fprintf (stderr, "fatal: out of memory (xmalloc(%zu)).\n", s); exit(EXIT_FAILURE); } return p; } 

Par conséquent, en appelant une fonction xmalloc au lieu de malloc à chaque fois, vous pouvez être sûr qu'aucun comportement non défini ne se produira dans le programme en raison de l'utilisation d'un pointeur nul.

Malheureusement, xmalloc n'est pas un remède non plus. Il ne faut pas oublier que l'utilisation de xmalloc est inacceptable lorsqu'il s'agit d'écrire du code de bibliothèques. J'en parlerai plus tard.

3. La plupart des commentaires étaient les suivants: "dans la pratique, malloc ne renvoie jamais NULL ."

Heureusement, je ne suis pas le seul à comprendre que ce n'est pas la bonne approche. J'ai vraiment aimé ce commentaire dans mon soutien:

D'après mon expérience de discussion sur ce sujet, j'ai le sentiment qu'il existe deux sectes sur Internet. Les adhérents du premier croient fermement que malloc ne retourne jamais NULL sous Linux. Les partisans du second affirment sans réserve que si la mémoire ne peut pas être allouée dans votre programme, rien ne peut être fait, vous ne pouvez que planter. Il n'y a aucun moyen de les persuader trop. Surtout quand ces deux sectes se croisent. Vous ne pouvez le considérer que comme acquis. Et il n'est même pas important de savoir sur quelle ressource spécialisée une discussion a lieu.

J'ai réfléchi un moment et j'ai décidé de suivre les conseils, donc je n'essaierai de convaincre personne :). Espérons que ces groupes de développeurs n'écrivent que des programmes non fatals. Si, par exemple, certaines données du jeu sont corrompues, il n'y a rien de crucial.

La seule chose qui compte, c'est que les développeurs de bibliothèques, de bases de données ne doivent pas faire comme ça.

Appel aux développeurs de code et de bibliothèques hautement dépendants


Si vous développez une bibliothèque ou un autre code hautement dépendant, vérifiez toujours la valeur du pointeur renvoyé par la fonction malloc / realloc et renvoyez vers l'extérieur un code d'erreur si la mémoire n'a pas pu être allouée.

Dans les bibliothèques, vous ne pouvez pas appeler la fonction de sortie , si l'allocation de mémoire a échoué. Pour la même raison, vous ne pouvez pas utiliser xmalloc . Pour de nombreuses applications, il est inacceptable de simplement les abandonner. Pour cette raison, par exemple, une base de données peut être corrompue. On peut perdre des données qui ont été évaluées pendant plusieurs heures. De ce fait, le programme peut être atteint pour les vulnérabilités de "déni de service", lorsque, au lieu de gérer correctement la charge de travail croissante, une application multithread s'arrête simplement.

On ne peut pas supposer de quelles manières et dans quels projets la bibliothèque sera utilisée. Par conséquent, il faut supposer que l'application peut résoudre des tâches très critiques. C'est pourquoi le tuer en appelant exit n'est pas bon. Très probablement, un tel programme est écrit en tenant compte de la possibilité de manque de mémoire et il peut faire quelque chose dans ce cas. Par exemple, un système de CAO ne peut pas allouer un tampon de mémoire approprié qui sera suffisant pour un fonctionnement régulier en raison de la forte fragmentation de la mémoire. Dans ce cas, ce n'est pas la raison pour laquelle il écrase en mode d'urgence avec perte de données. Le programme peut offrir la possibilité d'enregistrer le projet et de redémarrer normalement.

En aucun cas, il est impossible de compter sur malloc pour qu'il puisse toujours allouer de la mémoire. On ne sait pas sur quelle plateforme et comment la bibliothèque sera utilisée. Si la situation de faible mémoire sur une plate-forme est exotique, il peut s'agir d'une situation assez courante sur l'autre.

Nous ne pouvons pas nous attendre à ce que si malloc renvoie NULL , le programme se bloque. Tout peut arriver. Comme je l'ai décrit dans l' article , le programme peut écrire des données non par l'adresse nulle. Par conséquent, certaines données peuvent être corrompues, ce qui entraîne des conséquences imprévisibles. Même le memset est dangereux. Si le remplissage avec des données va dans l'ordre inverse, certaines données sont d'abord corrompues, puis le programme se bloque. Mais le crash peut survenir trop tard. Si des données corrompues sont utilisées dans des threads parallèles pendant que la fonction memset fonctionne, les conséquences peuvent être fatales. Vous pouvez obtenir une transaction corrompue dans une base de données ou envoyer des commandes pour supprimer les fichiers "inutiles". Tout a une chance de se produire. Je suggère à un lecteur d'imaginer vous-même ce qui pourrait arriver en raison de l'utilisation des ordures en mémoire.

Ainsi, la bibliothèque n'a qu'une seule façon correcte de travailler avec les fonctions malloc . Vous devez IMMÉDIATEMENT vérifier que la fonction a renvoyé, et si elle est NULL, puis renvoyer un état d'erreur.

Liens supplémentaires


  1. Gestion des MOO
  2. Amusez-vous avec les pointeurs NULL: partie 1 , partie 2
  3. Ce que tout programmeur C devrait savoir sur le comportement non défini: partie 1 , partie 2 , partie 3

Source: https://habr.com/ru/post/fr438756/


All Articles