Top 10 des bugs dans les projets C ++ pour 2019

Image 7

Une autre année est sur le point de se terminer, il est donc temps de faire du café et de relire les rapports de bogues de l'année écoulée. Bien sûr, cela prendra beaucoup de temps, donc cet article a été écrit. Je suggère de jeter un œil aux endroits sombres les plus intéressants des projets que nous avons rencontrés en 2019 dans des projets écrits en C et C ++.

Dixième place: "Quel est notre système d'exploitation?"


V1040 Typo possible dans l'orthographe d'un nom de macro prédéfini. La macro '__MINGW32_' est similaire à '__MINGW32__'. winapi.h 4112

#if !defined(__UNICODE_STRING_DEFINED) && defined(__MINGW32_) #define __UNICODE_STRING_DEFINED #endif 

Ici, une faute de frappe a été faite dans le nom de macro __MINGW32 _ (MINGW32 déclare __MINGW32__). Dans d'autres endroits du projet, la vérification est écrite correctement:

Image 3


Ce n'était d'ailleurs pas seulement la première erreur de l'article « CMake: le cas où le projet est impardonnable pour la qualité de son code », mais en général la première vraie erreur trouvée par les diagnostics V1040 dans un vrai projet ouvert (19 août 2019).

Neuvième place: "Qui est le premier?"


V502 L'opérateur '?:' Fonctionne peut-être d'une manière différente de celle attendue. L'opérateur '?:' A une priorité inférieure à l'opérateur '=='. mir_parser.cpp 884

 enum Opcode : uint8 { kOpUndef, .... OP_intrinsiccall, OP_intrinsiccallassigned, .... kOpLast, }; bool MIRParser::ParseStmtIntrinsiccall(StmtNodePtr &stmt, bool isAssigned) { Opcode o = !isAssigned ? (....) : (....); auto *intrnCallNode = mod.CurFuncCodeMemPool()->New<IntrinsiccallNode>(....); lexer.NextToken(); if (o == !isAssigned ? OP_intrinsiccall : OP_intrinsiccallassigned) { intrnCallNode->SetIntrinsic(GetIntrinsicID(lexer.GetTokenKind())); } else { intrnCallNode->SetIntrinsic(static_cast<MIRIntrinsicID>(....)); } .... } 

Nous sommes intéressés par la partie suivante de ce code:

 if (o == !isAssigned ? OP_intrinsiccall : OP_intrinsiccallassigned) { .... } 

L'opérateur '==' a une priorité plus élevée que l'opérateur ternaire (? :). Pour cette raison, l'expression conditionnelle n'est pas évaluée correctement. Le code écrit est équivalent à ce qui suit:

 if ((o == !isAssigned) ? OP_intrinsiccall : OP_intrinsiccallassigned) { .... } 

Et compte tenu du fait que les constantes OP_intrinsiccall et OP_intrinsiccallassigned ont des valeurs non nulles, cette condition renvoie toujours la vraie valeur. Le corps de la branche else est un code inaccessible.

Cette erreur est venue en tête de l'article " Vérification du code Ark Compiler récemment ouvert par Huawei ".

Huitième place: «Le danger des opérations au niveau du bit»


V1046 Utilisation non sûre des types bool 'et' int 'ensemble dans l'opération' & = '. GSLMultiRootFinder.h 175

 int AddFunction(const ROOT::Math::IMultiGenFunction & func) { ROOT::Math::IMultiGenFunction * f = func.Clone(); if (!f) return 0; fFunctions.push_back(f); return fFunctions.size(); } template<class FuncIterator> bool SetFunctionList( FuncIterator begin, FuncIterator end) { bool ret = true; for (FuncIterator itr = begin; itr != end; ++itr) { const ROOT::Math::IMultiGenFunction * f = *itr; ret &= AddFunction(*f); } return ret; } 

Selon le code, la fonction SetFunctionList devait contourner la liste des itérateurs. Et si au moins l'un d'entre eux n'est pas valide, la valeur de retour sera fausse , sinon vraie .

Cependant, en réalité, la fonction SetFunctionList peut retourner false même pour les itérateurs valides. Examinons la situation. La fonction AddFunction renvoie le nombre d'itérateurs valides dans la liste fFunctions . C'est-à-dire lors de l'ajout d'itérateurs non nuls, la taille de cette liste augmentera séquentiellement: 1, 2, 3, 4, etc. C'est là que l'erreur dans le code commence à se manifester:

 ret &= AddFunction(*f); 

Parce que Puisque la fonction retourne un résultat de type int , pas bool , l'opération '& =' avec des nombres pairs donnera la valeur false . Après tout, le bit de nombre pair le moins significatif sera toujours nul. Par conséquent, une telle erreur non évidente gâchera le résultat de la fonction SetFunctionsList même pour des arguments valides.

Si vous lisez attentivement le code de l'exemple (et que vous lisez attentivement, non?), Vous remarquerez peut-être qu'il s'agit du code du projet ROOT. Bien sûr, nous l'avons testé: " Analyse de code ROOT - un cadre pour l'analyse des données de recherche scientifique ."

Septième place: «Confusion dans les variables»


V1001 [CWE-563] La variable 'Mode' est affectée mais n'est pas utilisée à la fin de la fonction. SIModeRegister.cpp 48

 struct Status { unsigned Mask; unsigned Mode; Status() : Mask(0), Mode(0){}; Status(unsigned Mask, unsigned Mode) : Mask(Mask), Mode(Mode) { Mode &= Mask; }; .... }; 

Il est très dangereux de donner aux arguments de fonction les mêmes noms que les membres de la classe. Très facile à confondre. Un tel cas est devant nous. Cette expression n'a pas de sens:

 Mode &= Mask; 

L'argument de la fonction change. Et c'est tout. Cet argument n'est plus utilisé. Très probablement, il fallait écrire comme ceci:

 Status(unsigned Mask, unsigned Mode) : Mask(Mask), Mode(Mode) { this->Mode &= Mask; }; 

Et ceci est une erreur de LLVM . Nous avons de temps à autre une tradition d'analyser ce projet. Cette année, nous avons également un article sur la vérification.

Sixième place: «C ++ a ses propres lois»


L'erreur suivante apparaît dans le code car les règles C ++ ne coïncident pas toujours avec les règles mathématiques ou le «bon sens». Remarquez par vous-même où l'erreur se trouve dans un petit extrait de code?

V709 Comparaison suspecte trouvée: 'f0 == f1 == m_fractureBodies.size ()'. N'oubliez pas que «a == b == c» n'est pas égal à «a == b && b == c». btFractureDynamicsWorld.cpp 483

 btAlignedObjectArray<btFractureBody*> m_fractureBodies; void btFractureDynamicsWorld::fractureCallback() { for (int i = 0; i < numManifolds; i++) { .... int f0 = m_fractureBodies.findLinearSearch(....); int f1 = m_fractureBodies.findLinearSearch(....); if (f0 == f1 == m_fractureBodies.size()) continue; .... } .... } 

Il semblerait que la condition vérifie que f0 est égal à f1 et égal au nombre d'éléments dans m_fractureBodies . Il semble que cette comparaison aurait dû vérifier si f0 et f1 sont à la fin du tableau m_fractureBodies , car ils contiennent la position de l'objet trouvé par la méthode findLinearSearch () . Cependant, en réalité, cette expression se transforme en une vérification pour voir si f0 et f1 sont égales, puis en une vérification pour voir si m_fractureBodies.size () est égal au résultat de f0 == f1 . Par conséquent, le troisième opérande est ici comparé à 0 ou 1.

Belle erreur! Et, heureusement, assez rare. Jusqu'à présent, nous ne l'avons rencontrée que dans trois projets ouverts, et, fait intéressant, ils n'étaient tous que des moteurs de jeu. Au fait, ce n'est pas la seule erreur que nous ayons trouvée dans Bullet. Les plus intéressants sont entrés dans notre article « PVS-Studio a étudié le moteur Red Dead Redemption - Bullet ».

Cinquième place: "Quelle est la fin de la ligne?"


L'erreur suivante est facilement détectée si vous connaissez une subtilité.

V739 EOF ne doit pas être comparé à une valeur de type 'char'. Le «ch» doit être de type «int». json.cpp 762

 void JsonIn::skip_separator() { signed char ch; .... if (ch == ',') { if( ate_separator ) { .... } .... } else if (ch == EOF) { .... } 

C'est l'une de ces erreurs qui peut être difficile à remarquer si vous ne savez pas que EOF est défini comme -1. Par conséquent, si vous essayez de le comparer avec une variable de type signé char , la condition est presque toujours fausse . La seule exception est si le code de caractère est 0xFF (255). Lors de la comparaison, un tel symbole se transformera en -1 et la condition sera vraie.

Il y a beaucoup d'erreurs liées aux jeux dans ce sommet: des moteurs aux jeux ouverts. Comme vous l'avez peut-être deviné, cet endroit nous est également venu de cette région. Vous pouvez voir plus d'erreurs dans l'article " Cataclysm Dark Days Ahead, Static Analysis and Bagels ."

Quatrième place: «La magie du nombre Pi»


V624 Il y a probablement une faute d' impression dans la constante '3.141592538'. Pensez à utiliser la constante M_PI de <math.h>. PhysicsClientC_API.cpp 4109

 B3_SHARED_API void b3ComputeProjectionMatrixFOV(float fov, ....) { float yScale = 1.0 / tan((3.141592538 / 180.0) * fov / 2); .... } 


Une petite faute de frappe dans le nombre Pi (3.141592653 ...), manquant le nombre "6" à la 7ème position dans la partie fractionnaire.

Image 4
Peut-être que l'erreur à la dix millionième décimale n'entraînera pas de conséquences tangibles, mais vous devriez toujours utiliser les constantes de bibliothèque existantes sans fautes de frappe. Pour Pi, par exemple, il existe une constante M_PI de l'en-tête math.h.

Cette erreur provient de l'article « PVS-Studio a examiné le moteur Red Dead Redemption - Bullet », que nous connaissons déjà en sixième position. Si vous ne l'avez pas reporté pour plus tard, c'est la dernière chance.

Une légère digression


Nous sommes donc proches des trois erreurs les plus intéressantes. Comme vous l'avez peut-être remarqué, ils ne sont pas triés par les conséquences catastrophiques de leur présence, mais par la complexité de la détection. Après tout, au final, l'avantage le plus important de l'analyse statique sur la révision de code est que la machine ne se fatigue pas et n'oublie rien. :)

Et maintenant, j'attire votre attention sur les trois premiers.

Image 8


Troisième place: «l'exception insaisissable»


Les classes V702 doivent toujours être dérivées de std :: exception (et similaires) en tant que «public» (aucun mot-clé n'a été spécifié, donc le compilateur le définit par défaut sur «privé»). CalcManager CalcException.h 4

 class CalcException : std::exception { public: CalcException(HRESULT hr) { m_hr = hr; } HRESULT GetException() { return m_hr; } private: HRESULT m_hr; }; 

L'analyseur a détecté une classe héritée de la classe std :: exception via le modificateur privé (le modificateur par défaut si rien n'est spécifié). Le problème avec ce code est que lorsque vous essayez d'intercepter l'exception générale std :: exception, une exception de type CalcException sera ignorée. Ce problème se produit car l'héritage privé empêche la conversion de type implicite.

Oui, je ne voudrais pas voir le programme se bloquer en raison du modificateur public manquant . Soit dit en passant, je suis sûr que vous avez certainement utilisé l'application au moins une fois dans votre vie, dont nous venons de voir le code source. C'est la bonne vieille calculatrice Windows standard que nous avons également testée .

Deuxième place: balises HTML non fermées


V735 Peut-être un HTML incorrect. La balise de fermeture "</body>" a été rencontrée, tandis que la balise "</div>" était attendue. book.cpp 127

 static QString makeAlgebraLogBaseConversionPage() { return BEGIN INDEX_LINK TITLE(Book::tr("Logarithmic Base Conversion")) FORMULA(y = log(x) / log(a), log<sub>a</sub>x = log(x) / log(a)) END; } 

Comme cela arrive souvent avec le code C / C ++, rien n'est clair à partir de la source, alors passons au code prétraité pour ce fragment:

Image 6


L'analyseur a détecté une balise <div> non fermée. Il y a beaucoup de fragments de code html dans ce fichier et maintenant il devrait être vérifié en plus par les développeurs.

Surpris que nous puissions vérifier et autres? Quand j'ai vu cela pour la première fois, j'ai été impressionné. Nous analysons donc un peu le code html. Vrai, uniquement en code C ++. :)

Ce n'est pas seulement la deuxième place, mais aussi la deuxième calculatrice de notre top. Vous pouvez trouver une liste de toutes les erreurs dans l'article " Suivre la trace des calculatrices: SpeedCrunch ".

Première place: «Caractéristiques standard insaisissables»


Nous sommes donc arrivés à la première place. Un problème incroyablement étrange qui a fait l'objet d'une révision de code.

Essayez de le découvrir vous-même:

 static int EatWhitespace (FILE * InFile) /* ----------------------------------------------------------------------- ** * Scan past whitespace (see ctype(3C)) and return the first non-whitespace * character, or newline, or EOF. * * Input: InFile - Input source. * * Output: The next non-whitespace character in the input stream. * * Notes: Because the config files use a line-oriented grammar, we * explicitly exclude the newline character from the list of * whitespace characters. * - Note that both EOF (-1) and the nul character ('\0') are * considered end-of-file markers. * * ----------------------------------------------------------------------- ** */ { int c; for (c = getc (InFile); isspace (c) && ('\n' != c); c = getc (InFile)) ; return (c); } /* EatWhitespace */ 

Voyons maintenant ce que jure l'analyseur:

V560 Une partie de l'expression conditionnelle est toujours vraie: ('\ n'! = C). params.c 136.

Étrange, non? Regardons quelque chose d'intéressant dans le même projet, mais dans un fichier différent (charset.h):

 #ifdef isspace #undef isspace #endif .... #define isspace(c) ((c)==' ' || (c) == '\t') 

Donc, et c'est déjà vraiment étrange ... Il s'avère que si la variable c est égale à '\ n', alors la fonction isspace (c) absolument inoffensive à première vue retournera false et la deuxième partie de ce test ne sera pas effectuée en raison d'une évaluation de court-circuit. Si isspace (c) est exécuté, alors la variable c est soit '' soit '\ t', et ce n'est clairement pas égal à '\ n' .

Bien sûr, vous pouvez dire que cette macro est comme #define true false , et un tel code ne passera jamais en revue le code. Cependant, ce code a passé l'examen et nous attendait en toute sécurité dans le référentiel du projet.

Si vous avez besoin d'une analyse plus détaillée de l'erreur, c'est dans l'article " Pour ceux qui veulent jouer au détective: trouvez l'erreur dans la fonction de Midnight Commander ".

Conclusion


Image 9


Au cours de la dernière année, nous avons trouvé de nombreuses erreurs. Ce sont les erreurs de copier-coller habituelles, les erreurs de constantes, les balises non fermées et bien d'autres problèmes. Mais notre analyseur s'améliore et apprend à rechercher de plus en plus de bogues, donc c'est loin d'être la fin, et de nouveaux articles sur la vérification des projets seront publiés aussi souvent qu'auparavant.

Si quelqu'un lit nos articles pour la première fois, juste au cas où, je préciserai que toutes ces erreurs ont été trouvées en utilisant l'analyseur de code statique PVS-Studio, que nous suggérons de télécharger et d'essayer. L'analyseur peut détecter des erreurs dans le code des programmes écrits dans les langages: C, C ++, C # et Java.

Nous sommes donc arrivés à la fin! Si vous avez manqué les deux premiers niveaux, je vous suggère de ne pas manquer l'occasion et de les parcourir avec nous: C # et Java .



Si vous souhaitez partager cet article avec un public anglophone, veuillez utiliser le lien vers la traduction: Maxim Zvyagintsev. Top 10 des bugs trouvés dans les projets C ++ en 2019 .

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


All Articles