Enquanto a 118ª Semana Nobel acontecia em Estocolmo, uma revisão do código do projeto ROOT usado em pesquisas científicas para o processamento de grandes dados estava sendo preparada no escritório de desenvolvimento do analisador de código estático PVS-Studio. Obviamente, você não dará um bônus por esse código, mas os desenvolvedores receberão uma análise detalhada de defeitos interessantes do código e uma licença para uma verificação completa do projeto.
1. Introdução
ROOT - um conjunto de utilitários para trabalhar com dados de pesquisa científica. Ele fornece toda a funcionalidade necessária para processamento de big data, análise estatística, visualização e armazenamento. É escrito principalmente em C ++. O desenvolvimento começou no 
CERN (Organização Europeia de Pesquisa Nuclear) para pesquisas em física de alta energia. Todos os dias, milhares de físicos usam aplicativos ROOT para analisar seus dados ou simular.
O PVS-Studio é uma ferramenta para detectar erros e possíveis vulnerabilidades no código fonte dos programas escritos em C, C ++, C # e Java. Ele funciona em sistemas de 64 bits no Windows, Linux e macOS e pode analisar o código destinado a plataformas ARM de 32 bits, 64 bits e incorporadas.
Nova estreia de diagnóstico
V1046 Uso inseguro dos tipos bool 'e' int 'juntos na operação' & = '. 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; } 
A versão beta do analisador usada durante a verificação encontrou um erro tão terrível.
Esperando. A função 
SetFunctionList ignora a lista de iteradores. Se pelo menos um deles for inválido, o valor de retorno será 
falso , caso contrário, 
verdadeiro .
Realidade A função 
SetFunctionList pode retornar 
false, mesmo para iteradores válidos. Vamos dar uma 
olhada na situação. A 
função AddFunction retorna o número de iteradores válidos na lista 
fFunctions . I.e. ao adicionar iteradores diferentes de zero, o tamanho desta lista aumentará sequencialmente: 1, 2, 3, 4 etc. É aqui que o erro no código começa a se manifestar:
 ret &= AddFunction(*f); 
Porque Como a função retorna um resultado do tipo 
int , não 
bool , a operação '& =' com números pares fornecerá o valor 
false . Afinal, o bit menos significativo de números pares sempre será zero. Portanto, esse erro não óbvio estragará o resultado da função 
SetFunctionsList , mesmo para argumentos válidos.
Erros em expressões condicionais
V501 Existem 
subexpressões idênticas à esquerda e à direita do operador '&&': 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); .... } 
Vamos começar com o exemplo mais inofensivo. O ponteiro do módulo é verificado duas vezes. Provavelmente, uma verificação é desnecessária. Mas é melhor corrigir o código para que não haja perguntas desnecessárias.
V501 Existem 
subexpressões idênticas 'strchr (fHostAuth-> GetHost (),' * ')' à esquerda e à direita da '||' operador. TAuthenticate.cxx 300
 TAuthenticate::TAuthenticate(TSocket *sock, const char *remote, const char *proto, const char *user) { ....  
Aqui na linha 
fHostAuth-> GetHost (), o mesmo caractere é pesquisado - '*'. Talvez um deles deva ser o símbolo '?'. Geralmente eles são usados para definir máscaras diferentes.
V517 O uso do 
padrão 'if (A) {...} else if (A) {...}' foi detectado. Há uma probabilidade de presença de erro lógico. Verifique as linhas: 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) {  
A variável 
fSummaryVrs é comparada duas vezes a zero. Isso faz com que o código na ramificação else-if nunca seja executado. Muito código está escrito lá ...
V523 A 
instrução 'then' é equivalente à instrução 'else'. TKDTree.cxx 805
 template <typename Index, typename Value> void TKDTree<Index, Value>::UpdateRange(....) { .... if (point[fAxis[inode]]<=fValue[inode]){  
O mesmo código de copiar e colar é executado sob qualquer condição. Talvez haja um erro de digitação nas palavras 
esquerda e 
direita .
Há muitos códigos suspeitos semelhantes no projeto:
- V523 A instrução 'then' é equivalente à instrução 'else'. TContainerConverters.cxx 51
- V523 A instrução 'then' é equivalente à instrução 'else'. TWebFile.cxx 1310
- V523 A instrução 'then' é equivalente à instrução 'else'. MethodMLP.cxx 423
- V523 A instrução 'then' é equivalente à instrução 'else'. RooAbsCategory.cxx 394
A expressão 
V547 '! File_name_value.empty ()' é sempre falsa. 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()) {  
Muito provavelmente, não há erro. O analisador detectou um código que pode ser reduzido. Porque 
Como o valor de 
file_name_value.empty () é verificado no início do loop, então no código, essa verificação pode ser removida, reduzindo significativamente a quantidade de código desnecessário.
V590 Considere inspecionar o '! File1 || c <= 0 || c == '*' || c! = '(' 'expressão. A expressão é excessiva ou contém um erro de impressão. 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; } .... } 
Considere a parte abreviada da expressão condicional apontada pelo analisador:
 if (.... || c == '*' || c != '(') { .... } 
A condição não depende se o símbolo "asterisco" é igual ou não. Essa parte da expressão condicional sempre será verdadeira para qualquer caractere que não seja '('. É fácil ver se você cria uma tabela verdade.
Mais dois lugares com lógica estranha em expressões condicionais:
- V590 Considere inspecionar esta expressão. A expressão é excessiva ou contém uma impressão incorreta. TFile.cxx 3963
- V590 Considere inspecionar esta expressão. A expressão é excessiva ou contém uma impressão incorreta. TStreamerInfoActions.cxx 3084
V593 Considere revisar a expressão do tipo 'A = B <C'. A expressão é calculada da seguinte forma: '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); } .... } 
O erro detectado pelo analisador só se manifesta quando o programa não funciona corretamente. A variável 
ret deve armazenar o código de retorno da função 
AddWorkers e, em caso de emergência, exibir esse valor no log. Mas o código não funciona assim. A condição carece de colchetes adicionais que especificam a prioridade das operações. Não é o código de retorno armazenado na variável 
ret , mas o resultado de uma comparação lógica, ou seja, apenas 0 ou 1.
Há outro lugar com um defeito semelhante:
- V593 Considere revisar a expressão do tipo 'A = B <C'. A expressão é calculada da seguinte forma: 'A = (B <C)'. TProofServ.cxx 3897
V768 A constante de enumeração 'kCostComplexityPruning' é usada como uma variável de um tipo booleano. 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 ... Por que a negação do valor constante de 
kCostComplexityPruning ? Provavelmente, o símbolo de negação foi acidentalmente adicionado e agora leva à lógica incorreta da execução do código.
Código inválido com ponteiros
V522 A desreferenciação do ponteiro nulo 'pré' pode ocorrer. 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); } 
Eu tentei descobrir esse código estranho. Parece que a ideia não é definir um novo valor para o campo 
fpre . Então eles cometeram um erro confundindo o ponteiro, que deveria ser verificado na condição. Na implementação atual, se você passar o valor 
nullptr para a função 
SetPre , o ponteiro nulo será desreferenciado.
Provavelmente, isso está correto:
 void TSynapse::SetPre(TNeuron * pre) { if (fpre) { Error("SetPre","this synapse is already assigned to a pre-neuron."); return; } fpre = pre; pre->AddPost(this); } 
Verdade, isso ainda não salvará a função de passar um ponteiro nulo. Mas pelo menos esse código parece mais lógico que a versão original.
Aqui está outro lugar que foi copiado daqui com pequenas modificações:
- V522 A desreferenciação do ponteiro nulo 'postagem' pode ocorrer. TSynapse.cxx 74
V595 O ponteiro 'N' foi utilizado antes de ser verificado no nullptr. Verifique as linhas: 484, 488. Scanner.cxx 484
 bool RScanner::shouldVisitDecl(clang::NamedDecl *D) { if (auto M = D->getOwningModule()) {  
O analisador detectou um código muito perigoso! No primeiro caso, o ponteiro 
N é desreferenciado sem verificar um valor zero. E você nem consegue ver a chamada para o ponteiro não verificado. Isso acontece dentro da função 
shouldVisitDecl .
Tradicionalmente, essa regra de diagnóstico gera muitos avisos úteis. Aqui estão alguns deles:
- V595 O ponteiro 'arquivo' foi utilizado antes de ser verificado no nullptr. Verifique as linhas: 141, 153. TFileCacheRead.cxx 141
- V595 O ponteiro 'fFree' foi utilizado antes de ser verificado no nullptr. Verifique as linhas: 2029, 2038. TFile.cxx 2029
- V595 O ponteiro 'tbuf' foi utilizado antes de ser verificado no nullptr. Verifique as linhas: 586, 591. TGText.cxx 586
- V595 O ponteiro 'fPlayer' foi utilizado antes de ser verificado no nullptr. Verifique as linhas: 3425, 3430. TProof.cxx 3425
- V595 O ponteiro 'gProofServ' foi utilizado antes de ser verificado no nullptr. Verifique as linhas: 1192, 1194. TProofPlayer.cxx 1192
- V595 O ponteiro 'projDataTmp' foi utilizado antes de ser verificado no nullptr. Verifique as linhas: 791, 804. RooSimultaneous.cxx 791
O exemplo a seguir não é um erro, mas demonstra mais uma vez que as macros 
incentivam a gravação de código incorreto ou redundante.
V571 Verificação recorrente. A condição 'if (fCanvasImp)' já foi verificada na linha 799. TCanvas.cxx 800
 #define SafeDelete(p) { if (p) { delete p; p = 0; } } void TCanvas::Close(Option_t *option) { .... if (fCanvasImp) SafeDelete(fCanvasImp); .... } 
O ponteiro 
fCanvasImp é verificado duas vezes. Uma das verificações já está implementada na macro 
SafeDelete . Um dos problemas com as macros é que elas geralmente são difíceis de navegar a partir do código; muitas delas não estudam seu conteúdo antes do uso.
Erros ao trabalhar com matrizes
V519 A 
variável 'Line [Cursor]' recebe valores duas vezes sucessivamente. Talvez isso seja um erro. Verifique as linhas: 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; .... } 
O novo valor do elemento 
Line [Cursor] é imediatamente substituído. Algo está errado aqui ...
A saturação da matriz V557 é possível. O índice 'ivar' está apontando além do limite da matriz. BasicMinimizer.cxx 130
 bool BasicMinimizer::SetVariableValue(unsigned int ivar, double val) { if (ivar > fValues.size() ) return false; fValues[ivar] = val; return true; } 
Portanto, cometer erros na verificação do índice de uma matriz é apenas um problema enorme ultimamente. Quase todo terceiro projeto é encontrado. Se tudo é simples ao indexar uma matriz em um loop - o operador '<' é quase sempre usado para comparar o índice com o tamanho da matriz, e com essa verificação, como aqui, você precisa usar o operador '> =' e não '>'. Caso contrário, é possível ir além do limite da matriz por um elemento.
Este erro foi propagado sobre o arquivo várias vezes:
- A saturação da matriz V557 é possível. O índice 'ivar' está apontando além do limite da matriz. BasicMinimizer.cxx 186
- A saturação da matriz V557 é possível. O índice 'ivar' está apontando além do limite da matriz. BasicMinimizer.cxx 194
- A saturação da matriz V557 é possível. O índice 'ivar' está apontando além do limite da matriz. BasicMinimizer.cxx 209
- A saturação da matriz V557 é possível. O índice 'ivar' está apontando além do limite da matriz. BasicMinimizer.cxx 215
- A saturação da matriz V557 é possível. O índice 'ivar' está apontando além do limite da matriz. BasicMinimizer.cxx 230
V621 Considere inspecionar o operador 'for'. É possível que o loop seja executado incorretamente ou não seja executado. 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);  
Provavelmente, no loop for, eles queriam comparar a variável 
dim com 
dm-> fArrayDim , e não 
fArrayDim . O valor da variável utilizada é negativo, devido à condição no início da função. Esse ciclo nunca é executado.
V767 Acesso suspeito ao elemento da matriz 'atual' por um índice constante dentro de um loop. TClingUtils.cxx 3082
 llvm::StringRef ROOT::TMetaUtils::DataMemberInfo__ValidArrayIndex(....) { .... while (current!=0) {  
Esse fragmento de código analisa uma determinada linha e verifica sua correção. Depois que o caractere nulo da corrente 
atual for definido como um número, todos os outros caracteres serão percorridos para garantir que todos os caracteres sejam números até o final da sequência. Bem, como é feito ... o contador de loop 
i não 
é usado no loop. Era necessário escrever 
atual [i] e não 
atual [0] na condição.
Vazamento de memória
V773 A função foi encerrada sem liberar o ponteiro 'optionlist'. É possível um vazamento de memória. TDataMember.cxx 355
 void TDataMember::Init(bool afterReading) { .... TList *optionlist = new TList();  
Ao sair de uma função, a memória não é fornecida pelo ponteiro 
optionList . Se isso é necessário neste caso específico, é difícil para mim dizer. Mas geralmente esses erros são corrigidos em projetos nos relatórios do PVS-Studio. Tudo depende se o programa deve tentar continuar trabalhando em uma emergência ou não. Existem muitos avisos no projeto; é melhor para os desenvolvedores verificarem o projeto por conta própria e verem o relatório completo do analisador.
Novamente sobre a função memset
V597 O compilador pode excluir a chamada de função 'memset', usada para liberar o buffer 'x'. A função memset_s () deve ser usada para apagar os dados privados. TMD5.cxx 366
 void TMD5::Transform(UInt_t buf[4], const UChar_t in[64]) { UInt_t a, b, c, d, x[16]; ....  
Muitos pensam que quando o código é compilado, esse comentário não entra no arquivo binário e eles estão certos: D. Mas a função 
memset também será excluída pelo compilador, nem todo mundo imagina. E isso vai acontecer. Se o buffer liberado não for mais usado no código, o compilador otimizará o código e excluirá a chamada de função. Tecnicamente, ele está certo, mas se houver dados secretos no buffer, eles permanecerão lá. Esta é uma falha de segurança clássica do 
CWE-14 .
Avisos diversos
V591 A função não nula deve retornar um valor. 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; } 
A instrução sobrecarregada não tem um valor de retorno. Também é um problema muito comum ultimamente.
V596 O objeto foi criado, mas não está sendo usado. A palavra-chave 'throw' pode estar ausente: 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."); } .... } 
O erro é que a palavra-chave 
throw é acidentalmente esquecida. Como resultado, esse código não gera uma exceção em caso de erro.
No total, havia dois desses lugares. O segundo:
- V596 O objeto foi criado, mas não está sendo usado. A palavra-chave 'throw' pode estar ausente: throw runtime_error (FOO); Forest.hxx 137
V609 Divida por zero. Intervalo do denominador [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);  
Um caso semelhante aos descritos anteriormente sobre matrizes. Aqui, a variável 
n é limitada a um intervalo de valores de 0 a 100. Nesse caso, existe um ramo de código no qual ocorre a divisão pela variável n com o valor 0. Muito provavelmente, a restrição do valor de n deve ser reescrita da seguinte maneira:
 if (n <= 0 || n > 100) return z; 
V646 Considere inspecionar a lógica do aplicativo. É possível que a palavra-chave 'else' esteja ausente. 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'); } } .... } 
O analisador detectou uma formatação estranha. Em um lugar, a palavra-chave 
else está ausente. A partir do código, podemos assumir que realmente vale a pena corrigi-lo.
E alguns lugares para consertar ao mesmo tempo:
- V646 Considere inspecionar a lógica do aplicativo. É possível que a palavra-chave 'else' esteja ausente. TFormula_v5.cxx 3702
- V646 Considere inspecionar a lógica do aplicativo. É possível que a palavra-chave 'else' esteja ausente. RooAbsCategory.cxx 604
V663 Loop infinito é possível. A condição 'cin.eof ()' é insuficiente para interromper o loop. Considere adicionar a chamada de função 'cin.fail ()' à expressão condicional. MétodoKNN.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; } .... } .... } 
Ao trabalhar com a classe 
std :: istream , chamar a função 
eof () não é suficiente para concluir o loop. No caso de uma falha durante a leitura dos dados, chamar a função 
eof () sempre retornará 
false e não há outros pontos de saída do loop nesse código. Para concluir o loop nesse caso, é necessária uma verificação adicional do valor retornado pela função 
fail () :
 while (!is.eof() && !is.fail()) { .... } 
Ou apenas escreva:
 while (is) { .... } 
V678 Um objeto é usado como argumento para seu próprio método. Considere verificar o primeiro argumento real da função 'Copiar'. TFormLeafInfo.cxx 2414
 TFormLeafInfoMultiVarDim::TFormLeafInfoMultiVarDim( const TFormLeafInfoMultiVarDim& orig) : TFormLeafInfo(orig) { fNsize = orig.fNsize; fSizes.Copy(fSizes);  
Finalmente, aqui está um erro. Em vez de 
fSizes, era necessário passar 
orig.fSizes para a função 
Copiar .
Conclusão
Há cerca de um ano, foi 
fornecida uma visão geral do código do projeto 
NCBI Genome Workbench . Este projeto também é usado em pesquisas científicas, mas o genoma. Para o que eu lidero, o software nessa área é muito importante, mas sua qualidade não recebe a devida atenção.
A propósito, outro dia o macOS 10.15 Catalina foi lançado, no qual eles se recusaram a dar suporte a aplicativos de 32 bits. E no PVS-Studio há um grande conjunto de regras para identificar problemas ao transportar programas para sistemas de 64 bits. Mais sobre isso na 
postagem do blog do analisador.

Se você deseja compartilhar este artigo com um público que fala inglês, use o link para a tradução: Svyatoslav Razmyslov. 
Analisando o Código de ROOT, Estrutura de Análise de Dados Científicos .