Mientras Estocolmo celebraba la 118a Semana Nobel, estaba sentado en nuestra oficina, donde desarrollamos el analizador estático PVS-Studio, trabajando en una revisión de análisis del proyecto ROOT, un marco de procesamiento de grandes datos utilizado en la investigación científica. Este código no ganaría un premio, por supuesto, pero los autores definitivamente pueden contar con una revisión detallada de los defectos más interesantes más una licencia gratuita para verificar a fondo el proyecto por su cuenta.
Introduccion
ROOT es un kit de herramientas de software científico modular. Proporciona todas las funcionalidades necesarias para lidiar con el procesamiento de big data, análisis estadístico, visualización y almacenamiento. Está escrito principalmente en C ++. ROOT nació en el
CERN , en el corazón de la investigación sobre física de alta energía. Todos los días, miles de físicos utilizan aplicaciones ROOT para analizar sus datos o realizar simulaciones.
PVS-Studio es una herramienta para detectar errores de software y vulnerabilidades potenciales en el código fuente de programas escritos en C, C ++, C # y Java. Se ejecuta en Windows, Linux y macOS de 64 bits y puede analizar el código fuente escrito para plataformas ARM integradas y de 32 bits.
El debut de un nuevo diagnóstico.
V1046 Uso inseguro de los tipos bool 'e' int 'juntos en la operación' & = '. 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; }
En primer lugar, aquí hay un error maravilloso encontrado por la versión beta de PVS-Studio, que estaba usando para esta revisión.
Las expectativas La función
SetFunctionList atraviesa una lista de iteradores. Si al menos un iterador no es válido, la función devuelve
falso o
verdadero en caso contrario.
Realidad La función
SetFunctionList puede devolver
false incluso para iteradores válidos. Averigüemos por qué. La función
AddFunction devuelve el número de iteradores válidos en la lista de
fFunctions . Es decir, agregar iteradores no nulos hará que la lista crezca gradualmente en tamaño: 1, 2, 3, 4, etc. Aquí es donde entra en juego el error:
ret &= AddFunction(*f);
Dado que la función devuelve un valor de tipo
int en lugar de
bool , la operación '& =' devolverá
falso para valores pares porque el bit menos significativo de un número par siempre se establece en cero. Así es como un error sutil puede romper el valor de retorno de
SetFunctionsList incluso cuando sus argumentos son válidos.
Errores en expresiones condicionales
V501 Hay
subexpresiones idénticas a la izquierda y a la derecha del operador '&&': módulo && 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); .... }
Comencemos con el error menos dañino. El puntero del
módulo se verifica dos veces. Probablemente una de las verificaciones sea redundante, sin embargo, sería prudente solucionarlo para evitar confusiones en el futuro.
V501 Hay
subexpresiones idénticas 'strchr (fHostAuth-> GetHost (),' * ')' a la izquierda y a la derecha de '||' operador TAuthenticate.cxx 300
TAuthenticate::TAuthenticate(TSocket *sock, const char *remote, const char *proto, const char *user) { ....
La
cadena fHostAuth-> GetHost () se explora dos veces para el carácter '*'. Probablemente, uno de estos controles estaba destinado a buscar el '?' carácter, ya que estos dos caracteres suelen ser los que se utilizan para especificar varias máscaras comodín.
V517 El uso del
patrón 'if (A) {...} else if (A) {...}' fue detectado. Hay una probabilidad de presencia de error lógico. Líneas de verificación: 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 se compara con cero dos veces, por lo que la ejecución nunca alcanza el código en la rama
else-if . Y hay bastante código allí ...
V523 La declaración 'then' es equivalente a la declaración 'else'. TKDTree.cxx 805
template <typename Index, typename Value> void TKDTree<Index, Value>::UpdateRange(....) { .... if (point[fAxis[inode]]<=fValue[inode]){
El mismo bloque de código, que es un clon de copiar y pegar, se ejecuta sin importar la condición. Supongo que hay una confusión entre las palabras
izquierda y
derecha .
El proyecto está lleno de puntos sospechosos como ese:
- V523 La declaración 'then' es equivalente a la declaración 'else'. TContainerConverters.cxx 51
- V523 La declaración 'then' es equivalente a la declaración 'else'. TWebFile.cxx 1310
- V523 La declaración 'then' es equivalente a la declaración 'else'. Método MLP.cxx 423
- V523 La declaración 'then' es equivalente a la declaración 'else'. RooAbsCategory.cxx 394
La expresión
V547 '! File_name_value.empty ()' siempre es 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()) {
Esto probablemente no sea un error; El analizador acaba de encontrar un código que se puede simplificar. Dado que el valor de retorno de
file_name_value.empty () ya está marcado al comienzo del ciclo, la segunda verificación duplicada se puede eliminar, lo que arroja una buena cantidad de código innecesario.
V590 Considere inspeccionar el '! File1 || c <= 0 || c == '*' || c! = '(' 'expresión. La expresión es excesiva o contiene un error de imprenta. 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; } .... }
Aquí está la parte problemática de la expresión condicional informada por el analizador:
if (.... || c == '*' || c != '(') { .... }
La verificación del carácter de asterisco no afectará el resultado de la condición. Esta parte siempre será cierta para cualquier carácter que no sea '('. Puede verificarlo usted mismo fácilmente dibujando una tabla de verdad.
Dos advertencias más sobre condiciones con lógica extraña:
- V590 Considere inspeccionar esta expresión. La expresión es excesiva o contiene un error de imprenta. TFile.cxx 3963
- V590 Considere inspeccionar esta expresión. La expresión es excesiva o contiene un error de imprenta. TStreamerInfoActions.cxx 3084
V593 Considere revisar la expresión del tipo 'A = B <C'. La expresión se calcula de la siguiente manera: '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); } .... }
Este error se revela solo en el caso del comportamiento defectuoso del programa. Se supone que la variable
ret almacena el código de retorno de la función
AddWorkers y escribe ese valor en el registro en caso de error. Pero no funciona según lo previsto. La condición carece de paréntesis adicionales que fuercen el orden de evaluación deseado. Lo que la variable
ret realmente almacena no es el código de retorno sino el resultado de la comparación lógica, es decir, 0 o 1.
Otro problema similar:
- V593 Considere revisar la expresión del tipo 'A = B <C'. La expresión se calcula de la siguiente manera: 'A = (B <C)'. TProofServ.cxx 3897
V768 La constante de enumeración 'kCostComplexityPruning' se usa como una variable de 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; } .... }
Hm ... ¿Por qué negar el valor constante
kCostComplexityPruning ? Sospecho que el personaje de negación es un error tipográfico, que ahora distorsiona la lógica de ejecución.
Errores de manejo del puntero
V522 Puede tener lugar la desreferenciación del puntero nulo 'pre'. 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); }
Hice mi mejor
esfuerzo tratando de entender este extraño código, y parece que la idea era evitar asignar un nuevo valor al campo
fpre . Si es así, el programador está comprobando accidentalmente el puntero incorrecto. La implementación actual lleva a desreferenciar un puntero nulo si pasa el valor
nullptr a la función
SetPre .
Creo que este fragmento debe repararse de la siguiente manera:
void TSynapse::SetPre(TNeuron * pre) { if (fpre) { Error("SetPre","this synapse is already assigned to a pre-neuron."); return; } fpre = pre; pre->AddPost(this); }
Sin embargo, esto no evitaría el paso de un puntero nulo a la función, pero al menos esta versión es lógicamente más consistente que la original.
Un clon ligeramente modificado de este código se puede encontrar en otro lugar:
- V522 Puede tener lugar la desreferenciación del puntero nulo 'post'. TSynapse.cxx 74
V595 El puntero 'N' se utilizó antes de que se verificara contra nullptr. Líneas de verificación: 484, 488. Scanner.cxx 484
bool RScanner::shouldVisitDecl(clang::NamedDecl *D) { if (auto M = D->getOwningModule()) {
¡Este es un código extremadamente peligroso! El puntero
N no se verifica como nulo antes de que se desreferencia la primera vez. Además, no puede ver que suceda aquí porque la desreferencia se lleva a cabo dentro de la función
shouldVisitDecl .
Este diagnóstico tradicionalmente genera un montón de advertencias relevantes. Estos son solo algunos ejemplos:
- V595 El puntero 'archivo' se utilizó antes de que se verificara contra nullptr. Líneas de verificación: 141, 153. TFileCacheRead.cxx 141
- V595 El puntero 'fFree' se utilizó antes de verificarlo con nullptr. Líneas de verificación: 2029, 2038. TFile.cxx 2029
- V595 El puntero 'tbuf' se utilizó antes de que se verificara contra nullptr. Líneas de verificación: 586, 591. TGText.cxx 586
- V595 El puntero 'fPlayer' se utilizó antes de que se verificara contra nullptr. Líneas de verificación: 3425, 3430. TProof.cxx 3425
- V595 El puntero 'gProofServ' se utilizó antes de que se verificara contra nullptr. Líneas de verificación: 1192, 1194. TProofPlayer.cxx 1192
- V595 El puntero 'projDataTmp' se utilizó antes de que se verificara contra nullptr. Líneas de verificación: 791, 804. RooSim simultánea.cxx 791
El siguiente no es un error, pero es otro ejemplo de cómo las macros
fomentan la escritura de código defectuoso o redundante.
V571 Verificación recurrente. La condición 'if (fCanvasImp)' ya se verificó en la línea 799. TCanvas.cxx 800
#define SafeDelete(p) { if (p) { delete p; p = 0; } } void TCanvas::Close(Option_t *option) { .... if (fCanvasImp) SafeDelete(fCanvasImp); .... }
El puntero
fCanvasImp se verifica dos veces, con una de las comprobaciones ya implementadas en la macro
SafeDelete . Uno de los problemas con las macros es que son difíciles de navegar desde el código, razón por la cual muchos programadores no examinan su contenido antes de usarlo.
Errores de manejo de matriz
V519 A la
variable 'Línea [Cursor]' se le asignan valores dos veces seguidas. Quizás esto sea un error. Líneas de verificación: 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; .... }
Al elemento
Línea [Cursor] se le asigna un nuevo valor, que luego se sobrescribe inmediatamente. Eso no se ve bien ...
V557 Array overrun es posible. El índice 'ivar' apunta más allá del límite de la matriz. BasicMinimizer.cxx 130
bool BasicMinimizer::SetVariableValue(unsigned int ivar, double val) { if (ivar > fValues.size() ) return false; fValues[ivar] = val; return true; }
Cometer este error al verificar los índices de la matriz es una tendencia reciente; Lo vemos en casi cada tercer proyecto. Si bien la indexación en una matriz dentro de un bucle es fácil, generalmente usa el operador '<' para comparar el índice con el tamaño de la matriz, las comprobaciones como la que se muestra arriba requieren el operador '> =', no '>'. De lo contrario, corre el riesgo de indexar un elemento más allá del límite de la matriz.
Este error fue clonado en todo el código varias veces:
- V557 Array overrun es posible. El índice 'ivar' apunta más allá del límite de la matriz. BasicMinimizer.cxx 186
- V557 Array overrun es posible. El índice 'ivar' apunta más allá del límite de la matriz. BasicMinimizer.cxx 194
- V557 Array overrun es posible. El índice 'ivar' apunta más allá del límite de la matriz. BasicMinimizer.cxx 209
- V557 Array overrun es posible. El índice 'ivar' apunta más allá del límite de la matriz. BasicMinimizer.cxx 215
- V557 Array overrun es posible. El índice 'ivar' apunta más allá del límite de la matriz. BasicMinimizer.cxx 230
V621 Considere inspeccionar el operador 'para'. Es posible que el bucle se ejecute incorrectamente o no se ejecute en absoluto. 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);
En el bucle
for , los desarrolladores aparentemente
tenían la intención de comparar la variable
dim con
dm-> fArrayDim en lugar de
fArrayDim . El valor de
fArrayDim es negativo, lo que está garantizado por la condición al comienzo de la función. En consecuencia, este bucle nunca se ejecutará.
V767 Acceso sospechoso al elemento de la matriz 'actual' por un índice constante dentro de un bucle. TClingUtils.cxx 3082
llvm::StringRef ROOT::TMetaUtils::DataMemberInfo__ValidArrayIndex(....) { .... while (current!=0) {
Este código está analizando y verificando alguna cadena. Si el primer carácter de la cadena
actual (es decir, en el índice 0) se ha reconocido como un número, el bucle atravesará todos los caracteres restantes para asegurarse de que todos sean números. Bueno, al menos esa es la idea. El problema es que el contador
i no se usa en el bucle. La condición debe reescribirse para que compruebe
actual [i] en lugar de
actual [0] .
Pérdida de memoria
V773 Se salió de la función sin soltar el puntero 'lista de opciones'. Una pérdida de memoria es posible. TDataMember.cxx 355
void TDataMember::Init(bool afterReading) { .... TList *optionlist = new TList();
El puntero
optionList no se libera antes de regresar de la función. No sé si dicha liberación es necesaria en este caso particular, pero cuando informamos errores como ese, los desarrolladores generalmente los solucionan. Todo depende de si desea o no que su programa siga ejecutándose en caso de error. ROOT tiene un montón de defectos como ese, por lo que aconsejaría a los autores que revisen el proyecto ellos mismos.
memset otra vez
V597 El compilador podría eliminar la llamada de función 'memset', que se utiliza para vaciar el búfer 'x'. La función memset_s () debe usarse para borrar los datos privados. TMD5.cxx 366
void TMD5::Transform(UInt_t buf[4], const UChar_t in[64]) { UInt_t a, b, c, d, x[16]; ....
Muchos piensan que el comentario no llegará al archivo binario después de la compilación, y son absolutamente correctos: D. Lo que algunos pueden no saber es que el compilador también eliminará la función
memset . Y esto sucederá seguro. Si el búfer en cuestión ya no se usa más en el código, el compilador optimizará la llamada a la función. Técnicamente, es una decisión razonable, pero si el búfer estaba almacenando datos privados, esos datos permanecerán allí. Esta es una debilidad de seguridad clásica
CWE-14 .
Misceláneo
V591 La función no nula debería devolver un 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; }
El operador sobrecargado no tiene valor de retorno. Esta es otra tendencia reciente.
V596 El objeto fue creado pero no se está utilizando. Podría faltar la palabra clave 'throw': 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."); } .... }
El problema es que el programador omitió accidentalmente la palabra clave
throw , evitando así que se lance una excepción en caso de condición de error.
Solo hubo dos advertencias de este tipo. Aquí está el segundo:
- V596 El objeto fue creado pero no se está utilizando. Podría faltar la palabra clave 'throw': throw runtime_error (FOO); Forest.hxx 137
V609 Divide por cero. Rango del 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 es similar a los ejemplos de manejo de matrices discutidos anteriormente. La variable
n está limitada al rango de 0 a 100. Pero luego hay una rama que realiza la división por la variable
n que puede tener el valor 0. Creo que los límites del rango de
n deberían fijarse de la siguiente manera:
if (n <= 0 || n > 100) return z;
V646 Considere inspeccionar la lógica de la aplicación. Es posible que falte la palabra clave 'else'. 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'); } } .... }
El analizador informa una declaración
if extrañamente formateada con la palabra clave
else faltante. La apariencia de este código sugiere que necesita ser reparado.
Un par de advertencias más de este tipo:
- V646 Considere inspeccionar la lógica de la aplicación. Es posible que falte la palabra clave 'else'. TFormula_v5.cxx 3702
- V646 Considere inspeccionar la lógica de la aplicación. Es posible que falte la palabra clave 'else'. RooAbsCategory.cxx 604
V663 Infinite loop es posible. La condición 'cin.eof ()' es insuficiente para romper el bucle. Considere agregar la llamada a la función 'cin.fail ()' a la expresión 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; } .... } .... }
Cuando se trabaja con la clase
std :: istream , llamar a la función
eof () no es suficiente para terminar el ciclo. La función
eof () siempre devolverá
falso si los datos no se pueden leer, y no hay otros puntos de terminación en este código. Para garantizar la terminación del bucle, se requiere una verificación adicional del valor devuelto por la función
fail () :
while (!is.eof() && !is.fail()) { .... }
Como alternativa, se puede reescribir de la siguiente manera:
while (is) { .... }
V678 Un objeto se utiliza como argumento de su propio método. Considere verificar el primer argumento real de la función 'Copiar'. TFormLeafInfo.cxx 2414
TFormLeafInfoMultiVarDim::TFormLeafInfoMultiVarDim( const TFormLeafInfoMultiVarDim& orig) : TFormLeafInfo(orig) { fNsize = orig.fNsize; fSizes.Copy(fSizes);
Terminemos el artículo con este simpático error tipográfico. La función
Copiar debería llamarse con
orig.fSizes , no
fSizes .
Conclusión
Hace aproximadamente un año, verificamos el proyecto
NCBI Genome Workbench , que es otro programa utilizado en la investigación científica que se ocupa del análisis del genoma. Menciono esto porque la calidad del software científico es extremadamente crucial, pero los desarrolladores tienden a subestimarlo.
Por cierto, macOS 10.15 Catalina se lanzó el otro día, donde dejaron de admitir aplicaciones de 32 bits. Afortunadamente, PVS-Studio ofrece un amplio conjunto de diagnósticos diseñados específicamente para detectar errores que acompañan la transferencia de programas a sistemas de 64 bits. Obtenga más información en esta
publicación del equipo de PVS-Studio.