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 .