在斯德哥尔摩举办第118届诺贝尔周的时候,我坐在办公室里,我们在那儿开发了PVS-Studio静态分析仪,对ROOT项目(科学研究中使用的大数据处理框架)进行分析审查。 当然,该代码不会赢得任何奖励,但是作者绝对可以指望对最有趣的缺陷进行详细的审查,再加上免费的许可证,可以自己对项目进行彻底检查。
引言
ROOT是模块化的科学软件工具包。 它提供了处理大数据处理,统计分析,可视化和存储所需的所有功能。 它主要是用C ++编写的。 ROOT出生于
CERN ,是高能物理研究的核心。 每天,成千上万的物理学家使用ROOT应用程序分析其数据或执行模拟。
PVS-Studio是用于检测用C,C ++,C#和Java编写的程序的源代码中的软件错误和潜在漏洞的工具。 它可以在64位Windows,Linux和macOS上运行,并且可以分析为32位,64位和嵌入式ARM平台编写的源代码。
新型诊断仪的首次亮相
V1046在操作'&='中不安全地使用bool'和'int'类型。 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; }
首先,这是我用于本次评测的PVS-Studio beta版发现的一个奇妙错误。
期望值 SetFunctionList函数遍历迭代器列表。 如果至少一个迭代器无效,则该函数返回
false ,否则返回
true 。
现实性 即使对于有效的迭代器,
SetFunctionList函数也可以返回
false 。 让我们找出原因。
AddFunction函数返回
fFunctions列表上有效迭代器的数量。 也就是说,添加非null迭代器将导致列表的大小逐渐增加:1、2、3、4,依此类推。 这是该错误起作用的地方:
ret &= AddFunction(*f);
由于该函数返回的是
int类型而不是
bool的值 ,因此'&='操作将对偶数返回
false ,因为偶数的最低有效位始终设置为零。 这是一个微妙的错误可以破坏
SetFunctionsList的返回值的方式,即使其参数有效。
条件表达式中的错误
V501 '&&'运算符的左侧和右侧有相同的子表达式: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); .... }
让我们从危害最小的错误开始。
模块指针检查两次。 其中一项检查可能是多余的,但对其进行修复以避免将来出现任何混乱仍然是明智的。
V501在'||'的左侧和右侧有相同的子表达式'strchr(fHostAuth-> GetHost(),'*')' 操作员。 TAuthenticate.cxx 300
TAuthenticate::TAuthenticate(TSocket *sock, const char *remote, const char *proto, const char *user) { ....
扫描
fHostAuth-> GetHost()字符串两次以查找'*'字符。 这些检查之一可能是要查找“?” 字符,因为这两个字符通常是用于指定各种通配符掩码的字符。
V517检测到使用'if(A){...} else if(A){...}'模式。 存在逻辑错误的可能性。 检查行: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) {
fSummaryVrs变量与零进行两次比较,因此执行永远不会到达
else-if分支中的代码。 那里有很多代码...
V523'then '语句等效于'else'语句。 TKDTree.cxx 805
template <typename Index, typename Value> void TKDTree<Index, Value>::UpdateRange(....) { .... if (point[fAxis[inode]]<=fValue[inode]){
无论条件如何,都将执行相同的代码块,即复制粘贴克隆。 我猜
左右两个词之间有些混淆。
该项目充满了诸如此类的可疑点:
- V523'then'语句等效于'else'语句。 TContainerConverters.cxx 51
- V523'then'语句等效于'else'语句。 TWebFile.cxx 1310
- V523'then'语句等效于'else'语句。 方法MLP.cxx 423
- V523'then'语句等效于'else'语句。 RooAbsCategory.cxx 394
V547表达式'!File_name_value.empty()'始终为false。 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()) {
这可能不是错误; 分析器刚刚发现一些可以简化的代码。 由于在循环开始时已经检查了
file_name_value.empty()的返回值,因此可以删除第二个重复检查,从而丢弃了大量不必要的代码。
V590考虑检查'!File1 || c <= 0 || c =='*'|| c!='(''表达式。表达式过多或打印错误。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; } .... }
这是分析器报告的条件表达式的问题部分:
if (.... || c == '*' || c != '(') { .... }
星号字符的检查不会影响条件的结果。 这部分对于'(')以外的其他任何字符都是正确的。您可以通过绘制真值表轻松地自己检查一下。
关于带有奇怪逻辑的条件的另外两个警告:
- V590考虑检查此表达式。 表达式过多或打印错误。 TFile.cxx 3963
- V590考虑检查此表达式。 表达式过多或打印错误。 TStreamerInfoActions.cxx 3084
V593考虑查看“ A = B <C”类型的表达式。 该表达式的计算公式如下:“ 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); } .... }
仅当程序出现错误时,此错误才会显示出来。
ret变量应该存储
AddWorkers函数的返回代码,并在出现错误情况时将该值写入日志。 但是它没有按预期工作。 该条件缺少附加的括号,无法强制执行所需的评估顺序。
ret变量实际存储的不是返回码,而是逻辑比较的结果,即0或1。
另一个类似的问题:
- V593考虑查看“ A = B <C”类型的表达式。 该表达式的计算公式如下:“ A =(B <C)”。 TProofServ.cxx 3897
V768枚举常量'kCostComplexityPruning'用作布尔型变量。 方法DT.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; } .... }
嗯...为什么要否定常数
kCostComplexityPruning ? 我怀疑否定字符是一个错字,现在使执行逻辑失真。
指针处理错误
V522可能会
取消引用空指针“ 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); }
我尽了最大的努力来理解这个奇怪的代码,似乎是为了避免为
fpre字段分配新值。 如果是这样,则程序员意外检查了错误的指针。 如果将
nullptr值传递给
SetPre函数,则当前实现将导致取消引用空指针。
我认为该代码段应固定如下:
void TSynapse::SetPre(TNeuron * pre) { if (fpre) { Error("SetPre","this synapse is already assigned to a pre-neuron."); return; } fpre = pre; pre->AddPost(this); }
但是,这不会阻止将空指针传递给该函数,但是至少此版本比原始版本在逻辑上更加一致。
可以在另一个位置找到此代码的稍作修改的克隆:
- V522可能会取消引用空指针“ post”。 TSynapse.cxx 74
V595在针对nullptr进行验证之前,已使用了'N'指针。 检查行:484、488。Scanner.cxx 484
bool RScanner::shouldVisitDecl(clang::NamedDecl *D) { if (auto M = D->getOwningModule()) {
这是一段极其危险的代码!
N指针在第一次被取消引用之前不会被检查为空。 而且,由于取消引用发生在
shouldVisitDecl函数内,因此您看不到它在这里发生。
传统上,此诊断会生成一堆相关的警告。 这里只是几个例子:
- V595在针对nullptr进行验证之前,已使用“文件”指针。 检查行:141、153。TFileCacheRead.cxx 141
- V595在对nullptr进行验证之前,已使用了“ fFree”指针。 检查行:2029、2038。TFile.cxx 2029
- V595在针对nullptr对其进行验证之前,已使用了“ tbuf”指针。 检查行:586、591。TGText.cxx 586
- V595在针对nullptr对其进行验证之前,已使用了“ fPlayer”指针。 检查行:3425、3430。TProof.cxx 3425
- V595在针对nullptr对其进行验证之前,已使用了'gProofServ'指针。 检查行:1192、1194。TProofPlayer.cxx 1192
- V595在针对nullptr对其进行验证之前,已使用了'projDataTmp'指针。 检查行:791、804。RooSimultaneous.cxx 791
下一个不是bug,而是宏如何
鼓励编写错误或冗余代码的另一个示例。
V571定期检查。 “ if(fCanvasImp)”条件已在第799行中得到验证。TCanvas.cxx 800
#define SafeDelete(p) { if (p) { delete p; p = 0; } } void TCanvas::Close(Option_t *option) { .... if (fCanvasImp) SafeDelete(fCanvasImp); .... }
fCanvasImp指针被检查两次,其中一项检查已在
SafeDelete宏中实现。 宏的问题之一是它们很难从代码中导航,这就是许多程序员在使用前不检查其内容的原因。
数组处理错误
V519 '行[光标]'变量连续两次被赋值。 也许这是一个错误。 检查行: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; .... }
元素
行[Cursor]被分配了一个新值,然后立即被覆盖。 看起来不对劲...
V557阵列可能超限。 “ ivar”索引指向数组界限之外。 BasicMinimizer.cxx 130
bool BasicMinimizer::SetVariableValue(unsigned int ivar, double val) { if (ivar > fValues.size() ) return false; fValues[ivar] = val; return true; }
最近在检查数组索引时犯了这个错误。 我们几乎在每三个项目中都看到它。 虽然在循环内索引到数组很容易-您通常使用'<'运算符将索引与数组的大小进行比较-上面显示的检查要求使用'> ='运算符,而不是'>'。 否则,您可能要为超出数组范围的一个元素建立索引。
此错误已在整个代码中多次克隆:
- V557阵列可能超限。 “ ivar”索引指向数组界限之外。 BasicMinimizer.cxx 186
- V557阵列可能超限。 “ ivar”索引指向数组界限之外。 BasicMinimizer.cxx 194
- V557阵列可能超限。 “ ivar”索引指向数组界限之外。 BasicMinimizer.cxx 209
- V557阵列可能超限。 “ ivar”索引指向数组界限之外。 BasicMinimizer.cxx 215
- V557阵列可能超限。 “ ivar”索引指向数组界限之外。 BasicMinimizer.cxx 230
V621考虑检查“ for”操作员。 循环有可能执行不正确或根本不执行。 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);
在
for循环中,开发人员显然打算将
dim变量与
dm-> fArrayDim而不是
fArrayDim进行比较 。
fArrayDim的值为负,这由函数开头的条件保证。 因此,此循环将永远不会执行。
V767通过循环内的常量索引可疑访问“当前”数组的元素。 TClingUtils.cxx 3082
llvm::StringRef ROOT::TMetaUtils::DataMemberInfo__ValidArrayIndex(....) { .... while (current!=0) {
这段代码正在解析并检查一些字符串。 如果
当前字符串的第一个字符(即索引0)已被识别为数字,则循环将遍历所有其余字符以确保它们均为数字。 好吧,至少那是个主意。 问题是,循环中未使用
i计数器。 应该重写条件,以便检查
当前[i]而不是
当前[0] 。
内存泄漏
V773在不释放'optionlist'指针的情况下退出了该函数。 可能发生内存泄漏。 TDataMember.cxx 355
void TDataMember::Init(bool afterReading) { .... TList *optionlist = new TList();
从函数返回之前,不会释放
optionList指针。 我不知道在这种特殊情况下是否需要这种释放,但是当我们报告这样的错误时,开发人员通常会对其进行修复。 这完全取决于您是否希望程序在出现错误情况时继续运行。 ROOT有很多这样的缺陷,所以我建议作者自己重新检查该项目。
再次记忆
V597编译器可能会删除“ memset”函数调用,该函数调用用于刷新“ x”缓冲区。 memset_s()函数应用于擦除私有数据。 TMD5.cxx 366
void TMD5::Transform(UInt_t buf[4], const UChar_t in[64]) { UInt_t a, b, c, d, x[16]; ....
许多人认为编译后注释不会进入二进制文件,并且它们是绝对正确的:D. 有些人可能不知道的是,编译器也会删除
memset函数。 这肯定会发生。 如果该缓冲区不再在代码中进一步使用,则编译器将优化函数调用。 从技术上讲,这是一个合理的决定,但是如果缓冲区正在存储任何私有数据,这些数据将保留在那里。 这是经典的安全漏洞
CWE-14 。
杂项
V591非无效函数应返回一个值。 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; }
重载的运算符没有返回值。 这是最近的另一趋势。
V596对象已创建,但未使用。 可能没有'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."); } .... }
问题是程序员不小心遗漏了
throw关键字,因此可以防止在出现错误情况时引发异常。
只有两种此类警告。 这是第二个:
- V596对象已创建,但未使用。 可能没有'throw'关键字:throw runtime_error(FOO); 第137章
V609除以零。 分母范围[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);
这类似于前面讨论的数组处理示例。
n变量的范围限制为0到100。但是有一个分支执行除以
n变量的除法,其值可能为0。我认为
n的范围限制应固定如下:
if (n <= 0 || n > 100) return z;
V646考虑检查应用程序的逻辑。 可能缺少“ 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'); } } .... }
分析器报告缺少格式
else关键字的
if语句格式异常。 该代码的外观表明确实需要对其进行修复。
此类型的更多警告:
- V646考虑检查应用程序的逻辑。 可能缺少“ else”关键字。 TFormula_v5.cxx 3702
- V646考虑检查应用程序的逻辑。 可能缺少“ else”关键字。 RooAbsCategory.cxx 604
V663可能出现无限循环。 'cin.eof()'条件不足以使它脱离循环。 考虑将'cin.fail()'函数调用添加到条件表达式中。 方法KNN.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; } .... } .... }
使用
std :: istream类时,调用
eof()函数不足以终止循环。 如果无法读取数据,则
eof()函数将始终返回
false ,并且此代码中没有其他终止点。 为了保证循环的终止,需要对
fail()函数返回的值进行额外的检查:
while (!is.eof() && !is.fail()) { .... }
或者,可以将其重写如下:
while (is) { .... }
V678对象被用作其自身方法的参数。 考虑检查“复制”功能的第一个实际参数。 TFormLeafInfo.cxx 2414
TFormLeafInfoMultiVarDim::TFormLeafInfoMultiVarDim( const TFormLeafInfoMultiVarDim& orig) : TFormLeafInfo(orig) { fNsize = orig.fNsize; fSizes.Copy(fSizes);
让我们用这个漂亮的小错字来结束本文。 应该使用
orig.fSizes而不是
fSizes调用
Copy函数。
结论
大约一年前,我们检查了
NCBI Genome Workbench项目,这是科学研究中用于处理基因组分析的另一个程序。 我之所以这样说是因为科学软件的质量至关重要,但是开发人员往往会低估它。
顺便说一句,macOS 10.15 Catalina是在前一天发布的,他们停止了对32位应用程序的支持。 幸运的是,PVS-Studio提供了大量诊断程序,这些诊断程序专门用于检测与程序移植到64位系统有关的错误。 在PVS-Studio团队的这篇
文章中了解更多
信息 。