Alors que la 118e semaine Nobel se déroulait à Stockholm, un examen du code du projet ROOT utilisé dans la recherche scientifique pour le traitement des mégadonnées était en cours de préparation au bureau de développement de l'analyseur de code statique PVS-Studio. Bien sûr, vous ne donnerez pas de bonus pour un tel code, mais les développeurs recevront un examen détaillé des défauts de code intéressants et une licence pour une vérification complète du projet.
Présentation
ROOT - un ensemble d'utilitaires pour travailler avec des données de recherche scientifique. Il fournit toutes les fonctionnalités nécessaires au traitement des mégadonnées, à l'analyse statistique, à la visualisation et au stockage. Il est principalement écrit en C ++. Le développement a commencé au
CERN (Organisation européenne pour la recherche nucléaire) pour la recherche en physique des hautes énergies. Chaque jour, des milliers de physiciens utilisent des applications ROOT pour analyser leurs données ou simuler.
PVS-Studio est un outil pour détecter les erreurs et les vulnérabilités potentielles dans le code source des programmes écrits en C, C ++, C # et Java. Il fonctionne sur les systèmes 64 bits sous Windows, Linux et macOS et peut analyser le code conçu pour les plates-formes ARM 32 bits, 64 bits et intégrées.
Début des nouveaux diagnostics
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; }
La version bêta de l'analyseur qui a été utilisée lors de la vérification a trouvé une erreur si terrible.
J'attends. La fonction
SetFunctionList contourne la liste des itérateurs. Si au moins l'un d'entre eux n'est pas valide, la valeur de retour sera
fausse , sinon
vraie .
La réalité La fonction
SetFunctionList peut retourner
false même pour les itérateurs valides. Nous allons
examiner la situation:
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.
Erreurs dans les expressions conditionnelles
V501 Il existe des sous-expressions identiques à gauche et à droite de l'opérateur '&&': module && module rootcling_impl.cxx 3650
virtual void HandleDiagnostic(....) override { .... bool isROOTSystemModuleDiag = module && ....; bool isSystemModuleDiag = module && module && module->IsSystem; if (!isROOTSystemModuleDiag && !isSystemModuleDiag) fChild->HandleDiagnostic(DiagLevel, Info); .... }
Commençons par l'exemple le plus inoffensif. Le pointeur du module est vérifié deux fois. Très probablement, une vérification n'est pas nécessaire. Mais il vaut mieux corriger le code afin qu’il n’y ait pas de questions inutiles.
V501 Il existe des sous-expressions identiques 'strchr (fHostAuth-> GetHost (),' * ')' à gauche et à droite de '||' opérateur. TAuthenticate.cxx 300
TAuthenticate::TAuthenticate(TSocket *sock, const char *remote, const char *proto, const char *user) { ....
Ici, dans la ligne
fHostAuth-> GetHost (), le même caractère est recherché - '*'. Peut-être que l'un d'eux devrait être le symbole «?». Ils sont généralement utilisés pour définir différents masques.
V517 L'utilisation du
modèle 'if (A) {...} else if (A) {...}' a été détectée. Il y a une probabilité de présence d'erreur logique. Vérifiez les lignes: 163, 165. TProofMonSenderML.cxx 163
Int_t TProofMonSenderML::SendSummary(TList *recs, const char *id) { .... if (fSummaryVrs == 0) { if ((dsn = recs->FindObject("dataset"))) recs->Remove(dsn); } else if (fSummaryVrs == 0) {
La variable
fSummaryVrs est comparée deux fois à zéro. Cela provoque l'exécution du code dans la branche else-if. Beaucoup de code y est écrit ...
V523 L'
instruction 'then' est équivalente à l'instruction 'else'. TKDTree.cxx 805
template <typename Index, typename Value> void TKDTree<Index, Value>::UpdateRange(....) { .... if (point[fAxis[inode]]<=fValue[inode]){
Le même code copier-coller est exécuté dans n'importe quelle condition. Il y a peut-être une faute de frappe dans les mots
gauche et
droite .
Il y a beaucoup de code suspect similaire dans le projet:
- V523 L'instruction 'then' est équivalente à l'instruction 'else'. TContainerConverters.cxx 51
- V523 L'instruction 'then' est équivalente à l'instruction 'else'. TWebFile.cxx 1310
- V523 L'instruction 'then' est équivalente à l'instruction 'else'. MethodMLP.cxx 423
- V523 L'instruction 'then' est équivalente à l'instruction 'else'. RooAbsCategory.cxx 394
V547 L' expression '! File_name_value.empty ()' est toujours fausse. SelectionRules.cxx 1423
bool SelectionRules::AreAllSelectionRulesUsed() const { for(auto&& rule : fClassSelectionRules){ .... std::string file_name_value; if (!rule.GetAttributeValue("file_name", file_name_value)) file_name_value.clear(); if (!file_name_value.empty()) {
Très probablement, il n'y a pas d'erreur. L'analyseur a détecté un code pouvant être raccourci. Parce que
Étant donné que la valeur de
file_name_value.empty () est vérifiée au début de la boucle, puis plus bas dans le code, cette vérification peut être supprimée, réduisant considérablement la quantité de code inutile.
V590 Envisagez d'inspecter le '! File1 || c <= 0 || c == '*' || c! = '(' 'expression. L'expression est excessive ou contient une erreur d'impression. TTabCom.cxx 840
TString TTabCom::DetermineClass(const char varName[]) { .... c = file1.get(); if (!file1 || c <= 0 || c == '*' || c != '(') { Error("TTabCom::DetermineClass", "variable \"%s\" not defined?", varName); goto cleanup; } .... }
Considérez la partie abrégée de l'expression conditionnelle pointée par l'analyseur:
if (.... || c == '*' || c != '(') { .... }
La condition ne dépend pas de l’égalité ou non du symbole «astérisque». Cette partie de l'expression conditionnelle sera toujours vraie pour tout caractère autre que '('. Ceci est facile à voir si vous construisez une table de vérité.
Deux autres endroits avec une logique étrange dans les expressions conditionnelles:
- V590 Envisagez d'inspecter cette expression. L'expression est excessive ou contient une erreur d'impression. TFile.cxx 3963
- V590 Envisagez d'inspecter cette expression. L'expression est excessive ou contient une erreur d'impression. TStreamerInfoActions.cxx 3084
V593 Envisagez de revoir l'expression du type «A = B <C». L'expression est calculée comme suit: «A = (B <C)». TProofServ.cxx 1903
Int_t TProofServ::HandleSocketInput(TMessage *mess, Bool_t all) { .... if (Int_t ret = fProof->AddWorkers(workerList) < 0) { Error("HandleSocketInput:kPROOF_GETSLAVEINFO", "adding a list of worker nodes returned: %d", ret); } .... }
L'erreur détectée par l'analyseur ne se manifeste que lorsque le programme ne fonctionne pas correctement. La variable
ret doit stocker le code retour de la fonction
AddWorkers et, en cas d'urgence, afficher cette valeur dans le journal. Mais le code ne fonctionne pas comme ça. La condition manque de crochets supplémentaires qui spécifient la priorité des opérations. Pas le code retour, mais le résultat d'une comparaison logique est stocké dans la variable
ret , c'est-à-dire seulement 0 ou 1.
Il y a un autre endroit avec un défaut similaire:
- V593 Envisagez de revoir l'expression du type «A = B <C». L'expression est calculée comme suit: «A = (B <C)». TProofServ.cxx 3897
V768 La constante d'énumération 'kCostComplexityPruning' est utilisée comme variable de type booléen. MethodDT.cxx 283
enum EPruneMethod {kExpectedErrorPruning=0, kCostComplexityPruning, kNoPruning}; void TMVA::MethodDT::ProcessOptions() { .... if (fPruneStrength < 0) fAutomatic = kTRUE; else fAutomatic = kFALSE; if (fAutomatic && fPruneMethod==!DecisionTree::kCostComplexityPruning){ Log() << kFATAL << "Sorry automatic pruning strength determination is ...." << Endl; } .... }
Hmm ... Pourquoi la négation de la valeur constante de
kCostComplexityPruning ? Très probablement, le symbole de négation a été accidentellement ajouté et conduit maintenant à une logique incorrecte d'exécution de code.
Code non valide avec des pointeurs
V522 Le déréférencement du pointeur nul «pre» peut avoir lieu. TSynapse.cxx 61
void TSynapse::SetPre(TNeuron * pre) { if (pre) { Error("SetPre","this synapse is already assigned to a pre-neuron."); return; } fpre = pre; pre->AddPost(this); }
J'ai essayé de comprendre ce code bizarre. Il semble que l'idée ne consiste pas à définir une nouvelle valeur pour le champ
fpre . Ensuite, ils ont fait une erreur en confondant le pointeur, qui devrait être vérifié dans l'état. Dans l'implémentation actuelle, si vous transmettez la valeur
nullptr à la fonction
SetPre , le pointeur null est déréférencé.
Très probablement, c'est correct:
void TSynapse::SetPre(TNeuron * pre) { if (fpre) { Error("SetPre","this synapse is already assigned to a pre-neuron."); return; } fpre = pre; pre->AddPost(this); }
Certes, cela ne sauvera toujours pas la fonction de passer un pointeur nul. Mais au moins, ce code semble plus logique que la version originale.
Voici un autre endroit qui a été copié à partir d'ici avec de légères modifications:
- V522 Le déréférencement du pointeur nul «post» peut avoir lieu. TSynapse.cxx 74
V595 Le pointeur 'N' a été utilisé avant d'être vérifié par rapport à nullptr. Vérifiez les lignes: 484, 488. Scanner.cxx 484
bool RScanner::shouldVisitDecl(clang::NamedDecl *D) { if (auto M = D->getOwningModule()) {
L'analyseur a détecté un code très dangereux! Dans le premier cas, le pointeur
N est déréférencé sans vérifier une valeur nulle. Et vous ne pouvez même pas voir l'appel au pointeur non vérifié. Cela se produit à l'intérieur de la fonction
shouldVisitDecl .
Traditionnellement, cette règle de diagnostic génère de nombreux avertissements utiles. En voici quelques uns:
- V595 Le pointeur «fichier» a été utilisé avant d'être vérifié par rapport à nullptr. Vérifiez les lignes: 141, 153. TFileCacheRead.cxx 141
- V595 Le pointeur 'fFree' a été utilisé avant d'être vérifié par rapport à nullptr. Vérifiez les lignes: 2029, 2038. TFile.cxx 2029
- V595 Le pointeur 'tbuf' a été utilisé avant d'être vérifié par rapport à nullptr. Vérifier les lignes: 586, 591. TGText.cxx 586
- V595 Le pointeur 'fPlayer' a été utilisé avant d'être vérifié par rapport à nullptr. Vérifiez les lignes: 3425, 3430. TProof.cxx 3425
- V595 Le pointeur 'gProofServ' a été utilisé avant d'être vérifié par rapport à nullptr. Vérifiez les lignes: 1192, 1194. TProofPlayer.cxx 1192
- V595 Le pointeur 'projDataTmp' a été utilisé avant d'être vérifié par rapport à nullptr. Vérifiez les lignes: 791, 804. RooSimultaneous.cxx 791
L'exemple suivant n'est pas une erreur, mais démontre une fois de plus que les macros
encouragent l' écriture de code incorrect ou redondant.
V571 Contrôle récurrent. La condition «if (fCanvasImp)» a déjà été vérifiée à la ligne 799. TCanvas.cxx 800
#define SafeDelete(p) { if (p) { delete p; p = 0; } } void TCanvas::Close(Option_t *option) { .... if (fCanvasImp) SafeDelete(fCanvasImp); .... }
Le pointeur
fCanvasImp est vérifié deux fois. L'une des vérifications est déjà implémentée dans la macro
SafeDelete . Un des problèmes avec les macros est qu'elles sont souvent difficiles à naviguer à partir du code, donc beaucoup n'étudient pas leur contenu avant utilisation.
Erreurs lors de l'utilisation de tableaux
V519 La
variable 'Line [Cursor]' reçoit des valeurs successives deux fois. C'est peut-être une erreur. Vérifiez les lignes: 352, 353. Editor.cpp 353
size_t find_last_non_alnum(const std::string &str, std::string::size_type index = std::string::npos) { .... char tmp = Line.GetText()[Cursor]; Line[Cursor] = Line[Cursor - 1]; Line[Cursor] = tmp; .... }
La nouvelle valeur de l'élément
Line [Cursor] est immédiatement remplacée. Quelque chose ne va pas ici ...
V557 Le dépassement de matrice est possible. L'index «ivar» pointe au-delà de la limite du tableau. BasicMinimizer.cxx 130
bool BasicMinimizer::SetVariableValue(unsigned int ivar, double val) { if (ivar > fValues.size() ) return false; fValues[ivar] = val; return true; }
Faire des erreurs dans la vérification de l'index d'un tableau n'est donc qu'un énorme problème ces derniers temps. Presque tous les trois projets sont trouvés. Si tout est simple lors de l'indexation d'un tableau dans une boucle - l'opérateur '<' est presque toujours utilisé pour comparer l'index à la taille du tableau, alors avec une telle vérification, comme ici, vous devez utiliser l'opérateur '> =', et non '>'. Sinon, il est possible de dépasser la limite du tableau par un élément.
Cette erreur s'est propagée plusieurs fois sur le fichier:
- V557 Le dépassement de matrice est possible. L'index «ivar» pointe au-delà de la limite du tableau. BasicMinimizer.cxx 186
- V557 Le dépassement de matrice est possible. L'index «ivar» pointe au-delà de la limite du tableau. BasicMinimizer.cxx 194
- V557 Le dépassement de matrice est possible. L'index «ivar» pointe au-delà de la limite du tableau. BasicMinimizer.cxx 209
- V557 Le dépassement de matrice est possible. L'index «ivar» pointe au-delà de la limite du tableau. BasicMinimizer.cxx 215
- V557 Le dépassement de matrice est possible. L'index «ivar» pointe au-delà de la limite du tableau. BasicMinimizer.cxx 230
V621 Envisagez d'inspecter l'opérateur «pour». Il est possible que la boucle soit mal exécutée ou ne soit pas exécutée du tout. TDataMember.cxx 554
Int_t TDataMember::GetArrayDim() const { if (fArrayDim<0 && fInfo) { R__LOCKGUARD(gInterpreterMutex); TDataMember *dm = const_cast<TDataMember*>(this); dm->fArrayDim = gCling->DataMemberInfo_ArrayDim(fInfo);
Très probablement, dans la boucle for, ils voulaient comparer la variable
dim avec
dm-> fArrayDim , et non
fArrayDim . La valeur de la variable utilisée est négative, en raison de la condition au début de la fonction. Un tel cycle n'est jamais exécuté.
V767 Accès suspect à l'élément du tableau 'courant' par un index constant à l'intérieur d'une boucle. TClingUtils.cxx 3082
llvm::StringRef ROOT::TMetaUtils::DataMemberInfo__ValidArrayIndex(....) { .... while (current!=0) {
Ce fragment de code analyse une certaine ligne et vérifie son exactitude. Une fois que le caractère nul de la chaîne
actuelle est défini comme un nombre, tous les autres caractères sont parcourus pour vous assurer que tous les caractères sont des nombres à la fin de la chaîne. Eh bien, comment ça se passe ... le compteur de boucle
i n'est pas utilisé dans la boucle. Il était nécessaire d'écrire le
courant [i] et non le
courant [0] dans la condition.
Fuite de mémoire
V773 La fonction a été
fermée sans relâcher le pointeur «liste d'options». Une fuite de mémoire est possible. TDataMember.cxx 355
void TDataMember::Init(bool afterReading) { .... TList *optionlist = new TList();
Lorsque vous quittez une fonction, la mémoire n'est pas fournie par le pointeur
optionList . Il est difficile pour moi de dire si cela est nécessaire dans ce cas particulier. Mais généralement, ces erreurs sont corrigées dans les projets sur les rapports PVS-Studio. Tout dépend si le programme doit essayer de continuer à fonctionner en cas d'urgence ou non. Il existe de nombreux avertissements de ce type dans le projet, il est préférable que les développeurs revérifient le projet par eux-mêmes et voient le rapport complet de l'analyseur.
Encore une fois sur la fonction memset
V597 Le compilateur pourrait supprimer l'appel de fonction 'memset', qui est utilisé pour vider le tampon 'x'. La fonction memset_s () doit être utilisée pour effacer les données privées. TMD5.cxx 366
void TMD5::Transform(UInt_t buf[4], const UChar_t in[64]) { UInt_t a, b, c, d, x[16]; ....
Beaucoup penseront que lorsque le code sera compilé, ce commentaire n'entrera pas dans le fichier binaire, et ils auront raison: D. Mais la fonction
memset sera également supprimée par le compilateur, tout le monde ne le devine pas. Et cela arrivera. Si le tampon vidé ne sera plus utilisé dans le code, le compilateur optimisera le code et supprimera l'appel de fonction. Techniquement, il a raison, mais s'il y avait des données secrètes dans le tampon, elles y resteraient. Il s'agit d'une faille de sécurité classique
CWE-14 .
Avertissements divers
V591 La fonction non vide doit renvoyer une valeur. LogLikelihoodFCN.h 108
LogLikelihoodFCN & operator = (const LogLikelihoodFCN & rhs) { SetData(rhs.DataPtr() ); SetModelFunction(rhs.ModelFunctionPtr() ); fNEffPoints = rhs.fNEffPoints; fGrad = rhs.fGrad; fIsExtended = rhs.fIsExtended; fWeight = rhs.fWeight; fExecutionPolicy = rhs.fExecutionPolicy; }
L'instruction surchargée n'a pas de valeur de retour. Également un problème très courant ces derniers temps.
V596 L'objet a été créé mais il n'est pas utilisé. Le mot clé «throw» peut être manquant: throw runtime_error (FOO); RTensor.hxx 363
template <typename Value_t, typename Container_t> inline RTensor<Value_t, Container_t> RTensor<Value_t, Container_t>::Transpose() { if (fLayout == MemoryLayout::RowMajor) { fLayout = MemoryLayout::ColumnMajor; } else if (fLayout == MemoryLayout::ColumnMajor) { fLayout = MemoryLayout::RowMajor; } else { std::runtime_error("Memory layout is not known."); } .... }
L'erreur est que le mot clé
throw est accidentellement oublié. Par conséquent, ce code ne lève pas d'exception en cas d'erreur.
Au total, il y avait deux de ces endroits. La seconde:
- V596 L'objet a été créé mais il n'est pas utilisé. Le mot clé «throw» peut être manquant: throw runtime_error (FOO); Forest.hxx 137
V609 Divisez par zéro. Plage du dénominateur [0..100]. TGHtmlImage.cxx 340
const char *TGHtml::GetPctWidth(TGHtmlElement *p, char *opt, char *ret) { int n, m, val; .... if (n < 0 || n > 100) return z; if (opt[0] == 'h') { val = fCanvas->GetHeight() * 100; } else { val = fCanvas->GetWidth() * 100; } if (!fInTd) { snprintf(ret, 15, "%d", val / n);
Un cas similaire à ceux décrits précédemment sur les tableaux. Ici, la variable
n est limitée à une plage de valeurs de 0 à 100. Dans ce cas, il existe une branche de code dans laquelle la division par la variable n avec une valeur de 0 se produit. Très probablement, la restriction de la valeur de n doit être réécrite de cette façon:
if (n <= 0 || n > 100) return z;
V646 Envisagez d'inspecter la logique de l'application. Il est possible que le mot clé "else" soit manquant. TProofServ.cxx 729
TProofServ::TProofServ(Int_t *argc, char **argv, FILE *flog) : TApplication("proofserv", argc, argv, 0, -1) { .... if (!logmx.IsDigit()) { if (logmx.EndsWith("K")) { xf = 1024; logmx.Remove(TString::kTrailing, 'K'); } else if (logmx.EndsWith("M")) { xf = 1024*1024; logmx.Remove(TString::kTrailing, 'M'); } if (logmx.EndsWith("G")) { xf = 1024*1024*1024; logmx.Remove(TString::kTrailing, 'G'); } } .... }
L'analyseur a détecté un formatage étrange. À un endroit, le mot-clé
else est manquant. D'après le code, nous pouvons supposer que le code vaut vraiment la peine d'être corrigé.
Et quelques endroits à réparer en même temps:
- V646 Envisagez d'inspecter la logique de l'application. Il est possible que le mot clé "else" soit manquant. TFormula_v5.cxx 3702
- V646 Envisagez d'inspecter la logique de l'application. Il est possible que le mot clé "else" soit manquant. RooAbsCategory.cxx 604
La boucle infinie
V663 est possible. La condition «cin.eof ()» est insuffisante pour rompre la boucle. Pensez à ajouter l'appel de fonction 'cin.fail ()' à l'expression conditionnelle. MethodKNN.cxx 602
void TMVA::MethodKNN::ReadWeightsFromStream(std::istream& is) { .... while (!is.eof()) { std::string line; std::getline(is, line); if (line.empty() || line.find("#") != std::string::npos) { continue; } .... } .... }
Lorsque vous travaillez avec la classe
std :: istream , appeler la fonction
eof () ne suffit pas pour terminer la boucle. En cas d'échec lors de la lecture des données, l'appel de la fonction
eof () retournera toujours
false , et il n'y a aucun autre point de sortie de la boucle dans ce code. Pour terminer la boucle dans ce cas, une vérification supplémentaire de la valeur retournée par la fonction
fail () est requise:
while (!is.eof() && !is.fail()) { .... }
Ou écrivez simplement:
while (is) { .... }
V678 Un objet est utilisé comme argument de sa propre méthode. Pensez à vérifier le premier argument réel de la fonction «Copier». TFormLeafInfo.cxx 2414
TFormLeafInfoMultiVarDim::TFormLeafInfoMultiVarDim( const TFormLeafInfoMultiVarDim& orig) : TFormLeafInfo(orig) { fNsize = orig.fNsize; fSizes.Copy(fSizes);
Enfin, voici une telle erreur. Au lieu de
fSizes, vous avez dû passer
orig.fSizes à la fonction
Copy .
Conclusion
Il y a environ un an, un aperçu du code de projet
NCBI Genome Workbench a été
fourni . Ce projet est également utilisé dans la recherche scientifique, mais dans le génome. Pour ce que je veux dire, les logiciels dans ce domaine sont très importants, mais leur qualité n'est pas dûment prise en compte.
Soit dit en passant, l'autre jour, macOS 10.15 Catalina est sorti, dans lequel ils ont refusé de prendre en charge les applications 32 bits. Et dans PVS-Studio, il existe un large ensemble de règles pour identifier les problèmes lors du portage de programmes vers des systèmes 64 bits. Plus d'informations à ce sujet dans le blog de l'analyseur.

Si vous souhaitez partager cet article avec un public anglophone, veuillez utiliser le lien vers la traduction: Svyatoslav Razmyslov.
Analyse du code de ROOT, cadre d'analyse des données scientifiques .