Enquanto Estocolmo realizava a 118ª Semana Nobel, eu estava sentado em nosso escritório, onde desenvolvemos o analisador estático PVS-Studio, trabalhando em uma revisão de análise do projeto ROOT, uma estrutura de processamento de big data usada em pesquisas científicas. Esse código não ganharia um prêmio, é claro, mas os autores podem definitivamente contar com uma revisão detalhada dos defeitos mais interessantes, além de uma licença gratuita para verificar o projeto por conta própria.
1. Introdução
ROOT é um kit de ferramentas de software científico modular. Ele fornece todas as funcionalidades necessárias para lidar com o processamento de big data, análise estatística, visualização e armazenamento. É escrito principalmente em C ++. ROOT nasceu no 
CERN , no coração da pesquisa em física de alta energia. Todos os dias, milhares de físicos usam aplicativos ROOT para analisar seus dados ou realizar simulações.
O PVS-Studio é uma ferramenta para detectar bugs de software e possíveis vulnerabilidades no código fonte de programas escritos em C, C ++, C # e Java. É executado no Windows, Linux e macOS de 64 bits e pode analisar o código-fonte escrito para plataformas ARM de 32 bits, 64 bits e incorporadas.
Estréia de um novo 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; } 
Primeiro, aqui está um bug maravilhoso encontrado pela versão beta do PVS-Studio, que eu estava usando para esta revisão.
Expectativas A função 
SetFunctionList percorre uma lista de iteradores. Se pelo menos um iterador for inválido, a função retornará 
false ou 
true, caso contrário.
Realidade A função 
SetFunctionList pode retornar 
false, mesmo para iteradores válidos. Vamos descobrir o porquê. A função 
AddFunction retorna o número de iteradores válidos na lista 
fFunctions . Ou seja, adicionar iteradores não nulos fará com que a lista cresça gradualmente em tamanho: 1, 2, 3, 4 e assim por diante. É aqui que o bug entra em jogo:
 ret &= AddFunction(*f); 
Como a função retorna um valor do tipo 
int em vez de 
bool , a operação '& =' retornará 
false para valores pares porque o bit menos significativo de um número par é sempre definido como zero. É assim que um bug sutil pode quebrar o valor de retorno de 
SetFunctionsList, mesmo quando seus argumentos são 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 bug menos prejudicial. O ponteiro do 
módulo é verificado duas vezes. Uma das verificações provavelmente é redundante, mas ainda assim seria prudente corrigi-la para evitar qualquer confusão no futuro.
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) { ....  
A 
string fHostAuth-> GetHost () é varrida pelo caractere '*' duas vezes. Uma dessas verificações provavelmente foi feita para procurar o '?' caractere, pois esses dois caracteres geralmente são os usados para especificar várias máscaras curinga.
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 com zero duas vezes, portanto, a execução nunca atinge o código na ramificação 
else-if . E há bastante código 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 bloco de código, que é um clone de copiar e colar, é executado independentemente da condição. Acho que há uma confusão entre as palavras 
esquerda e 
direita .
O projeto está cheio de pontos suspeitos como esse:
- 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()) {  
Provavelmente não é um bug; o analisador acabou de encontrar algum código que pode ser simplificado. Como o valor de retorno 
file_name_value.empty () já está verificado no início do loop, a segunda verificação duplicada pode ser removida, descartando, assim, uma boa 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; } .... } 
Aqui está a parte do problema da expressão condicional relatada pelo analisador:
 if (.... || c == '*' || c != '(') { .... } 
A verificação do caractere asterisco não afetará o resultado da condição. Esta parte sempre será verdadeira para qualquer caractere que não seja '('. Você pode verificar isso facilmente desenhando uma tabela verdade.
Mais dois avisos sobre condições com lógica estranha:
- 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); } .... } 
Esse bug se revela apenas no caso de comportamento defeituoso do programa. A variável 
ret deve armazenar o código de retorno da função 
AddWorkers e gravar esse valor no log em caso de condição de erro. Mas não funciona como pretendido. A condição não possui parênteses adicionais, forçando a ordem de avaliação desejada. O que a variável 
ret realmente armazena não é o código de retorno, mas o resultado da comparação lógica, ou seja, 0 ou 1.
Outro problema 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; } .... } 
Hum ... Por que negar o valor constante 
kCostComplexityPruning ? Suspeito que o caractere de negação seja um erro de digitação, que agora distorce a lógica de execução.
Erros de manipulação de ponteiro
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); } 
Fiz o meu melhor tentando entender esse código estranho, e parece que a idéia era evitar atribuir um novo valor ao campo 
fpre . Nesse caso, o programador está checando acidentalmente o ponteiro errado. A implementação atual leva a remover a referência de um ponteiro nulo se você passar o valor 
nullptr para a função 
SetPre .
Eu acho que esse trecho deve ser corrigido da seguinte forma:
 void TSynapse::SetPre(TNeuron * pre) { if (fpre) { Error("SetPre","this synapse is already assigned to a pre-neuron."); return; } fpre = pre; pre->AddPost(this); } 
Isso, no entanto, não impediria a passagem de um ponteiro nulo para a função, mas pelo menos essa versão é mais logicamente consistente que a original.
Um clone ligeiramente modificado desse código pode ser encontrado em outro local:
- 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()) {  
Este é um pedaço de código extremamente perigoso! O ponteiro 
N não é verificado como nulo antes de ser desferenciado pela primeira vez. Além disso, você não pode ver isso acontecer aqui porque a desreferência ocorre dentro da função 
shouldVisitDecl .
Tradicionalmente, esse diagnóstico gera vários avisos relevantes. Aqui estão apenas alguns exemplos:
- 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 próximo não é um bug, mas é outro exemplo de como as macros 
incentivam a escrever código defeituoso 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, com uma das verificações já implementadas na macro 
SafeDelete . Um dos problemas das macros é que elas são difíceis de navegar a partir do código, e é por isso que muitos programadores não examinam seu conteúdo antes de usá-lo.
Erros de manipulação de matriz
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 elemento 
Linha [Cursor] recebe um novo valor, que é substituído imediatamente. Isso não parece certo ...
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; } 
Cometer esse erro ao verificar os índices da matriz é uma tendência recente; nós vemos isso em quase todos os terceiros projetos. Embora seja fácil indexar em uma matriz dentro de um loop - você normalmente usa o operador '<' para comparar o índice com o tamanho da matriz - verificações como a mostrada acima exigem o operador '> =', não '>'. Caso contrário, você corre o risco de indexar um elemento além do limite da matriz.
Este bug foi clonado em todo o código algumas 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);  
No loop 
for , os desenvolvedores aparentemente pretendiam comparar a variável 
dim com 
dm-> fArrayDim em vez de 
fArrayDim . O valor de 
fArrayDim é negativo, o que é garantido pela condição no início da função. Conseqüentemente, esse loop nunca será 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) {  
Este código está analisando e verificando alguma sequência. Se o primeiro caractere da string 
atual (ou seja, no índice 0) for reconhecido como um número, o loop percorrerá todos os caracteres restantes para garantir que todos sejam números. Bem, pelo menos essa é a ideia. O problema é que o contador 
i não é usado no loop. A condição deve ser reescrita para verificar a 
corrente [i] em vez da 
corrente [0] .
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();  
O ponteiro 
optionList não é liberado antes de retornar da função. Não sei se essa liberação é necessária nesse caso específico, mas quando relatamos erros como esse, os desenvolvedores geralmente os corrigem. Tudo depende se você deseja ou não que seu programa continue sendo executado em caso de condição de erro. O ROOT tem vários defeitos assim, então eu aconselho os autores a verificar novamente o projeto.
memset novamente
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 acham que o comentário não chegará ao arquivo binário após a compilação e eles estão absolutamente corretos: D. O que alguns podem não saber é que o compilador também removerá a função 
memset . E isso vai acontecer com certeza. Se o buffer em questão não for mais usado no código, o compilador otimizará a chamada de função. Tecnicamente, é uma decisão razoável, mas se o buffer estiver armazenando dados privados, esses dados permanecerão lá. Esta é uma fraqueza clássica de segurança 
CWE-14 .
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; } 
O operador sobrecarregado não tem valor de retorno. Essa é outra tendência recente.
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 problema é que o programador acidentalmente deixou de fora a palavra-chave 
throw , impedindo, assim, o lançamento de uma exceção em caso de condição de erro.
Havia apenas dois avisos desse tipo. Aqui está 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);  
Este é semelhante aos exemplos de manipulação de matriz discutidos anteriormente. A variável 
n é limitada ao intervalo de 0 a 100. Mas há um ramo que executa a divisão pela variável 
n que pode ter o valor 0. Acho que os limites do intervalo de 
n devem ser fixados da seguinte forma:
 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 relata uma instrução 
if estranhamente formatada com a palavra-chave 
else else ausente. A aparência desse código sugere que ele precisa ser corrigido.
Mais alguns avisos desse tipo:
- 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 finalizar o loop. A função 
eof () sempre retornará 
false se os dados não puderem ser lidos e não houver outros pontos de terminação nesse código. Para garantir o término do loop, é necessária uma verificação adicional do valor retornado pela função 
fail () :
 while (!is.eof() && !is.fail()) { .... } 
Como alternativa, ele pode ser reescrito da seguinte maneira:
 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);  
Vamos terminar o artigo com este pequeno erro de digitação. A função 
Copiar deve ser chamada com 
orig.fSizes , não 
fSizes .
Conclusão
Há cerca de um ano, verificamos o projeto 
NCBI Genome Workbench , outro programa usado em pesquisas científicas que tratam da análise do genoma. Estou mencionando isso porque a qualidade do software científico é extremamente crucial, mas os desenvolvedores tendem a subestimá-lo.
A propósito, o macOS 10.15 Catalina foi lançado outro dia, onde deixou de oferecer suporte a aplicativos de 32 bits. Felizmente, o PVS-Studio oferece um grande conjunto de diagnósticos projetados especificamente para detectar bugs que acompanham a portabilidade de programas para sistemas de 64 bits. Saiba mais neste 
post pela equipe do PVS-Studio.