Top 10 des bugs trouvés dans les projets C ++ en 2019

Image 7

Une autre année touche à sa fin, et c'est le moment idéal pour vous préparer une tasse de café et relire les critiques de bugs collectés dans le cadre de projets open source au cours de cette année. Cela prendrait un certain temps, bien sûr, nous avons donc préparé cet article pour vous faciliter la tâche. Aujourd'hui, nous nous souviendrons des points noirs les plus intéressants que nous ayons rencontrés dans des projets C / C ++ open source en 2019.

Non. 10. Sur quel système d'exploitation fonctionnons-nous?


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 

Il existe une faute de frappe dans le nom de la macro __MINGW32 _ (MINGW32 est en fait déclaré par __MINGW32__). Ailleurs dans le projet, le chèque est écrit correctement:

Image 3


Soit dit en passant, ce bogue n'était pas seulement le premier à être décrit dans l'article « CMake: le cas où la qualité du projet est impardonnable », mais le tout premier bogue authentique trouvé par le diagnostic V1040 dans un véritable projet open source (19 août , 2019).

Non. 9. 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:

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

La priorité de l'opérateur '==' est supérieure à celle de l'opérateur ternaire (? :). Par conséquent, l'expression conditionnelle est évaluée dans le mauvais ordre et équivaut au code suivant:

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

Étant donné que les constantes OP_intrinsiccall et OP_intrinsiccallassigned ne sont pas nulles, la condition retournera true tout le temps, ce qui signifie que le corps de la branche else est du code inaccessible.

Ce bogue a été décrit dans l'article " Vérification du compilateur Ark récemment créé en open-source par Huawei ".

Non. 8. Opérations dangereuses 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; } 

Le code suggère que la fonction SetFunctionList traverse une liste d'itérateurs. Si au moins un itérateur n'est pas valide, la fonction renvoie false , ou true sinon.

Cependant, la fonction SetFunctionList peut retourner false même pour les itérateurs valides. Voyons pourquoi . La fonction AddFunction renvoie le nombre d'itérateurs valides dans la liste fFunctions . Autrement dit, l'ajout d'itérateurs non nuls entraînera une augmentation incrémentielle de la liste: 1, 2, 3, 4, etc. C'est là que le bug entre en jeu:

 ret &= AddFunction(*f); 

Étant donné que la fonction renvoie une valeur de type int plutôt que bool , l'opération '& =' renvoie false pour les valeurs paires car le bit le moins significatif d'un nombre pair est toujours défini sur zéro. C'est ainsi qu'un bug subtil peut casser la valeur de retour de SetFunctionsList même lorsque ses arguments sont valides.

Si vous lisiez attentivement l'extrait (et vous l'étiez, n'est-ce pas?), Vous auriez pu remarquer qu'il provenait du projet ROOT. Oui, nous l'avons vérifié aussi: " Analyse du code de ROOT, cadre d'analyse des données scientifiques ."

Non. 7. Variables mélangées


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 d'utiliser les mêmes noms pour les arguments de fonction que pour les membres de la classe car vous risquez de les mélanger. Et c'est exactement ce qui s'est passé ici. L'expression suivante n'a pas de sens:

 Mode &= Mask; 

L'argument de la fonction change, et c'est tout. Cet argument n'est en aucun cas utilisé après cela. Ce que le programmeur voulait vraiment écrire était probablement le suivant:

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

Ce bogue a été trouvé dans LLVM . Nous avons une tradition de vérifier ce projet de temps en temps. Cette année, nous l'avons vérifié une fois de plus.

Non. 6. C ++ a ses propres lois


Ce bogue provient du fait que les règles C ++ ne suivent pas toujours les règles mathématiques ou le "bon sens". Regardez le petit extrait ci-dessous et essayez de trouver le bogue vous-même.

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; .... } .... } 

La condition semble vérifier que f0 est égal à f1 et est égal au nombre d'éléments dans m_fractureBodies . Il était probablement destiné à vérifier si f0 et f1 sont situés à la fin du tableau m_fractureBodies car ils contiennent une position d'objet trouvée par la méthode findLinearSearch () . Mais en réalité, cette expression conditionnelle vérifie si f0 est égal à f1 et ensuite si m_fractureBodies.size () est égal au résultat de l'expression f0 == f1 . Autrement dit, le troisième opérande ici est vérifié par rapport à 0 ou 1.

C'est un joli bug! Et, heureusement, assez rare. Jusqu'à présent, nous ne l'avons vu que dans trois projets open-source, et, fait intéressant, tous les trois étaient des moteurs de jeu. Ce n'est pas le seul bug trouvé dans Bullet; les plus intéressants ont été décrits dans l'article " PVS-Studio a examiné le moteur de balle de Red Dead Redemption ".

Non. 5. Qu'est-ce qui se trouve à la fin de la ligne?


Celui-ci est facile si vous connaissez un détail délicat.

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'un de ces bogues que vous ne pouvez pas facilement repérer si vous ne savez pas que EOF est défini sur -1. Donc, si vous essayez de le comparer avec une variable de type signé char , la condition sera presque toujours fausse . La seule exception est le caractère codé en 0xFF (255). Par rapport à EOF , ce caractère se transformera en -1, rendant ainsi la condition vraie.

Beaucoup de bugs dans le Top 10 de cette année ont été trouvés dans les logiciels de jeux informatiques: moteurs ou jeux open-source. Comme vous l'avez déjà deviné, celui-ci provenait également de cette région. D'autres erreurs sont décrites dans l'article " Cataclysm Dark Days Ahead: Static Analysis and Roguelike Games ".

Non. 4. La constante magique 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); .... } 


Il y a une petite faute de frappe dans le nombre Pi (3,141592653 ...): le nombre "6" est manquant à la 7ème décimale.

Image 4
Un millionième chiffre décimal incorrect ne causerait guère de dommages notables, mais il est toujours préférable d'utiliser les constantes existantes des bibliothèques, dont l'exactitude est garantie. Le nombre Pi, par exemple, est représenté par la constante M_PI de l'en-tête math.h.

Vous avez déjà lu ce bug dans l'article " PVS-Studio a regardé le moteur de puces de Red Dead Redemption ", où il était placé sixième. Si vous ne l'avez pas encore lu, c'est votre dernière chance.

Une petite diversion


Nous approchons du Top 3 des bugs les plus intéressants. Comme vous l'avez probablement remarqué, je trie les bugs non pas par leur impact mais par l'effort qu'il faut à un critique humain pour les trouver. Après tout, l'avantage de l'analyse statique sur les revues de code est essentiellement l'incapacité des outils logiciels à se fatiguer ou à oublier des choses. :)

Voyons maintenant ce que nous avons dans notre Top 3.

Image 8


Non. 3. Une 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 dérivée de la classe std :: exception à l'aide du modificateur private (qui est utilisé par défaut s'il n'est pas spécifié autrement). Le problème avec ce code est qu'une tentative d'attraper une exception std :: générique entraînera le programme à manquer une exception de type CalcException . Ce comportement provient du fait que l'héritage privé interdit la conversion de type implicite.

Vous n'aimeriez certainement pas voir votre programme planter à cause d'un modificateur public manquant. Soit dit en passant, je parie que vous avez utilisé cette application au moins une fois dans votre vie car c'est la bonne vieille calculatrice Windows , que nous avons également vérifiée plus tôt cette année.

Non. 2. 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, le code source C / C ++ ne dit pas grand-chose en soi, alors jetons un coup d'œil au code prétraité généré à partir de l'extrait ci-dessus:

Image 6


L'analyseur a trouvé une balise <div> non fermée. Il existe de nombreux fragments de code html ici, donc les auteurs doivent le réviser.

Surpris de pouvoir diagnostiquer ce genre de bugs? J'ai aussi été impressionné lorsque j'ai vu cela pour la première fois. Donc, oui, nous savons quelque chose sur l'analyse du code html. Eh bien, seulement si c'est dans le code C ++. :)

Non seulement ce bug est placé en deuxième position, mais c'est une deuxième calculatrice dans notre liste des 10 meilleurs. Pour savoir quels autres bugs nous avons trouvé dans ce projet, voir l'article " Suivre les traces des calculatrices: SpeedCrunch ".

Non. 1. Fonctions standard insaisissables


Voici le bug placé en premier. Celui-ci est un bug incroyablement étrange, qui a réussi à passer à travers la révision du code.

Essayez de le trouver 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 l'analyseur a à dire:

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

Bizarre, non? Jetons un coup d'œil à un autre endroit curieux mais dans un fichier différent (charset.h):

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

Hm, c'est vraiment étrange ... Donc, si la variable c est égale à '\ n', alors la fonction apparemment inoffensive isspace (c) retournera fausse , empêchant ainsi la deuxième partie du contrôle de s'exécuter en raison d'une évaluation en court-circuit. Et si isspace (c) s'exécute, la variable c sera égale à '' ou '\ t', ce qui n'est évidemment pas égal à '\ n' .

Vous pourriez faire valoir que cette macro est similaire à #define true false et qu'un code comme celui-ci ne passerait jamais par un examen du code. Mais cet extrait particulier l'a fait - et était assis dans le référentiel en attente d'être découvert.

Pour des commentaires plus détaillés sur ce bogue, consultez l'article "Vous voulez jouer un détective? Trouvez le bogue dans une fonction de Midnight Commander ".

Conclusion


Image 9


Nous avons trouvé des tonnes de bugs cette année. Il s'agissait d'erreurs de copier-coller courantes, de constantes inexactes, de balises non fermées et de nombreux autres défauts. Mais notre analyseur évolue et apprend à diagnostiquer de plus en plus de types de problèmes, donc nous n'allons certainement pas ralentir et publierons de nouveaux articles sur les bugs trouvés dans les projets aussi régulièrement qu'auparavant.

Juste au cas où vous n'avez pas lu nos articles auparavant, tous ces bogues ont été trouvés en utilisant notre analyseur statique PVS-Studio, que vous pouvez télécharger et essayer sur vos propres projets. Il détecte les bogues dans les programmes écrits en C, C ++, C # et Java.

Vous avez enfin atteint la ligne d'arrivée! Si vous avez manqué les deux premiers niveaux, je vous suggère de saisir l'opportunité et de compléter ces niveaux avec nous: C # et Java .

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


All Articles