使用 PVS-Studio 分析器查找 LLVM 8 中的错误

使用 PVS-Studio 分析器查找 LLVM 8 中的错误
自上次使用我们的 PVS-Studio 分析器检查 LLVM 项目代码以来已经过去了两年多。 让我们确保 PVS-Studio 分析器仍然是识别错误和潜在漏洞的领先工具。 为此,我们将检查并发现 LLVM 8.0.0 版本中的新错误。

待写文章

说实话,我本来不想写这篇文章的。 写一个我们已经检查过好几次的项目并没有什么意思(1, 2, 3)。 最好写一些新的东西,但我别无选择。

每次 LLVM 新版本发布或更新时 Clang 静态分析器,我们在邮件中收到以下类型的问题:

看,新版本的 Clang Static Analyzer 学会了发现新的错误! 在我看来,使用 PVS-Studio 的相关性正在下降。 Clang 发现了比以前更多的错误,并赶上了 PVS-Studio 的能力。 你怎么看待这件事?

对此我总是想回答这样的问题:

我们也不闲着! 我们显着改进了 PVS-Studio 分析仪的功能。 所以不用担心,我们会像以前一样继续领先。

不幸的是,这是一个糟糕的答案。 里面没有证据。 这就是我现在写这篇文章的原因。 于是,再次对LLVM项目进行了检查,发现其中存在多种错误。 我现在将展示那些我觉得有趣的内容。 Clang Static Analyzer 无法发现这些错误(或者在它的帮助下发现这些错误非常不方便)。 但我们可以。 而且,我在一个晚上的时间里发现并写下了所有这些错误。

但写这篇文章花了几周的时间。 我只是无法让自己把所有这些都写成文字:)。

顺便说一句,如果您对 PVS-Studio 分析器中使用哪些技术来识别错误和潜在漏洞感兴趣,那么我建议您熟悉一下 笔记.

新旧诊断

正如已经指出的,大约两年前,LLVM 项目再次进行了检查,发现的错误得到了纠正。 现在这篇文章将呈现一批新的错误。 为什么会发现新的错误? 原因有3个:

  1. LLVM 项目正在不断发展,更改旧代码并添加新代码。 自然,修改和编写的代码中还会出现新的错误。 这清楚地表明静态分析应该定期使用,而不是偶尔使用。 我们的文章很好地展示了PVS-Studio分析器的功能,但这与提高代码质量和降低修复错误的成本无关。 定期使用静态代码分析器!
  2. 我们正在最终确定和改进现有的诊断方法。 因此,分析器可以识别在之前的扫描中没有注意到的错误。
  3. PVS-Studio 中出现了两年前不存在的新诊断功能。 我决定在单独的部分中突出显示它们,以清楚地展示 PVS-Studio 的开发。

通过诊断发现 2 年前存在的缺陷

片段 N1:复制粘贴

static bool ShouldUpgradeX86Intrinsic(Function *F, StringRef Name) {
  if (Name == "addcarryx.u32" || // Added in 8.0
    ....
    Name == "avx512.mask.cvtps2pd.128" || // Added in 7.0
    Name == "avx512.mask.cvtps2pd.256" || // Added in 7.0
    Name == "avx512.cvtusi2sd" || // Added in 7.0
    Name.startswith("avx512.mask.permvar.") || // Added in 7.0     // <=
    Name.startswith("avx512.mask.permvar.") || // Added in 7.0     // <=
    Name == "sse2.pmulu.dq" || // Added in 7.0
    Name == "sse41.pmuldq" || // Added in 7.0
    Name == "avx2.pmulu.dq" || // Added in 7.0
  ....
}

PVS-Studio 警告: V501 [CWE-570] '||' 的左侧和右侧有相同的子表达式 'Name.startswith("avx512.mask.permvar.")' 操作员。 自动升级.cpp 73

仔细检查名称是否以子字符串“avx512.mask.permvar.”开头。 在第二次检查中,他们显然想写点别的东西,却忘了更正复制的文字。

片段 N2:拼写错误

enum CXNameRefFlags {
  CXNameRange_WantQualifier = 0x1,
  CXNameRange_WantTemplateArgs = 0x2,
  CXNameRange_WantSinglePiece = 0x4
};

void AnnotateTokensWorker::HandlePostPonedChildCursor(
    CXCursor Cursor, unsigned StartTokenIndex) {
  const auto flags = CXNameRange_WantQualifier | CXNameRange_WantQualifier;
  ....
}

警告 PVS-Studio:V501 “|”的左侧和右侧有相同的子表达式“CXNameRange_WantQualifier” 操作员。 CIndex.cpp 7245

由于拼写错误,同一个命名常量被使用了两次 CXNameRange_WantQualifier.

片段 N3:与运算符优先级混淆

int PPCTTIImpl::getVectorInstrCost(unsigned Opcode, Type *Val, unsigned Index) {
  ....
  if (ISD == ISD::EXTRACT_VECTOR_ELT && Index == ST->isLittleEndian() ? 1 : 0)
    return 0;
  ....
}

PVS-Studio 警告: V502 [CWE-783] 也许“?:”运算符的工作方式与预期不同。 “?:”运算符的优先级低于“==”运算符。 PPCTargetTransformInfo.cpp 404

在我看来,这是一个非常美丽的错误。 是的,我知道我对美有奇怪的想法:)。

现在,根据 操作员优先级,表达式计算如下:

(ISD == ISD::EXTRACT_VECTOR_ELT && (Index == ST->isLittleEndian())) ? 1 : 0

从实践的角度来看,这样的条件没有意义,因为它可以简化为:

(ISD == ISD::EXTRACT_VECTOR_ELT && Index == ST->isLittleEndian())

这是一个明显的错误。 最有可能的是,他们想将 0/1 与变量进行比较 Index。 要修复代码,您需要在三元运算符周围添加括号:

if (ISD == ISD::EXTRACT_VECTOR_ELT && Index == (ST->isLittleEndian() ? 1 : 0))

顺便说一句,三元运算符非常危险,会引发逻辑错误。 请务必小心,不要贪心括号。 我更详细地研究了这个主题 这里,在“注意 ?: 运算符并将其括在括号中”一章中。

片段N4、N5:空指针

Init *TGParser::ParseValue(Record *CurRec, RecTy *ItemType, IDParseMode Mode) {
  ....
  TypedInit *LHS = dyn_cast<TypedInit>(Result);
  ....
  LHS = dyn_cast<TypedInit>(
    UnOpInit::get(UnOpInit::CAST, LHS, StringRecTy::get())
      ->Fold(CurRec));
  if (!LHS) {
    Error(PasteLoc, Twine("can't cast '") + LHS->getAsString() +
                    "' to string");
    return nullptr;
  }
  ....
}

PVS-Studio 警告: V522 [CWE-476] 可能会取消引用空指针“LHS”。 TGParser.cpp 2152

如果指针 LHS 为空,则应发出警告。 然而,相反,这个相同的空指针将被取消引用: LHS->getAsString().

这是一种非常典型的情况,错误隐藏在错误处理程序中,因为没有人测试它们。 静态分析器检查所有可访问的代码,无论使用频率如何。 这是静态分析如何补充其他测试和错误保护技术的一个很好的例子。

类似的指针处理错误 RHS 下面的代码中允许:V522 [CWE-476] 可能会发生空指针“RHS”的取消引用。 TGParser.cpp 2186

片段 N6:移动后使用指针

static Expected<bool>
ExtractBlocks(....)
{
  ....
  std::unique_ptr<Module> ProgClone = CloneModule(BD.getProgram(), VMap);
  ....
  BD.setNewProgram(std::move(ProgClone));                                // <=
  MiscompiledFunctions.clear();

  for (unsigned i = 0, e = MisCompFunctions.size(); i != e; ++i) {
    Function *NewF = ProgClone->getFunction(MisCompFunctions[i].first);  // <=
    assert(NewF && "Function not found??");
    MiscompiledFunctions.push_back(NewF);
  }
  ....
}

PVS-Studio 警告:V522 [CWE-476] 可能会取消引用空指针“ProgClone”。 错误编译.cpp 601

一开始是一个智能指针 程序克隆 不再拥有该对象:

BD.setNewProgram(std::move(ProgClone));

事实上,现在 程序克隆 是一个空指针。 因此,空指针取消引用应该发生在下面:

Function *NewF = ProgClone->getFunction(MisCompFunctions[i].first);

但实际上,这不会发生! 请注意,循环并未实际执行。

在容器的开头 错误编译的函数 清除:

MiscompiledFunctions.clear();

接下来,在循环条件中使用该容器的大小:

for (unsigned i = 0, e = MisCompFunctions.size(); i != e; ++i) {

很容易看出循环没有启动。 我认为这也是一个错误,代码应该以不同的方式编写。

看来我们遇到了那个著名的奇偶校验错误! 一个错误掩盖了另一个错误:)。

片段 N7:移动后使用指针

static Expected<bool> TestOptimizer(BugDriver &BD, std::unique_ptr<Module> Test,
                                    std::unique_ptr<Module> Safe) {
  outs() << "  Optimizing functions being tested: ";
  std::unique_ptr<Module> Optimized =
      BD.runPassesOn(Test.get(), BD.getPassesToRun());
  if (!Optimized) {
    errs() << " Error running this sequence of passes"
           << " on the input program!n";
    BD.setNewProgram(std::move(Test));                       // <=
    BD.EmitProgressBitcode(*Test, "pass-error", false);      // <=
    if (Error E = BD.debugOptimizerCrash())
      return std::move(E);
    return false;
  }
  ....
}

PVS-Studio 警告:V522 [CWE-476] 可能会发生空指针“测试”的取消引用。 错误编译.cpp 709

同样的情况又出现了。 首先,对象的内容被移动,然后就可以像什么都没发生一样使用它。 在C++中出现运动语义之后,我在程序代码中越来越频繁地看到这种情况。 这就是我喜欢C++语言的原因! 有越来越多的新方法可以射断自己的腿。 PVS-Studio 分析仪将始终发挥作用:)。

片段 N8:空指针

void FunctionDumper::dump(const PDBSymbolTypeFunctionArg &Symbol) {
  uint32_t TypeId = Symbol.getTypeId();
  auto Type = Symbol.getSession().getSymbolById(TypeId);
  if (Type)
    Printer << "<unknown-type>";
  else
    Type->dump(*this);
}

PVS-Studio 警告:V522 [CWE-476] 可能会发生空指针“Type”的取消引用。 PrettyFunctionDumper.cpp 233

除了错误处理程序之外,通常不会测试调试打印输出功能。 我们面前就有这样一个案例。 该功能正在等待用户,用户不会解决自己的问题,而是被迫修复它。

更正:

if (Type)
  Type->dump(*this);
else
  Printer << "<unknown-type>";

片段 N9:空指针

void SearchableTableEmitter::collectTableEntries(
    GenericTable &Table, const std::vector<Record *> &Items) {
  ....
  RecTy *Ty = resolveTypes(Field.RecType, TI->getType());
  if (!Ty)                                                              // <=
    PrintFatalError(Twine("Field '") + Field.Name + "' of table '" +
                    Table.Name + "' has incompatible type: " +
                    Ty->getAsString() + " vs. " +                       // <=
                    TI->getType()->getAsString());
   ....
}

PVS-Studio 警告:V522 [CWE-476] 可能会发生空指针“Ty”的取消引用。 SearchableTableEmitter.cpp 614

我想一切都很清楚,不需要解释。

片段 N10:拼写错误

bool FormatTokenLexer::tryMergeCSharpNullConditionals() {
  ....
  auto &Identifier = *(Tokens.end() - 2);
  auto &Question = *(Tokens.end() - 1);
  ....
  Identifier->ColumnWidth += Question->ColumnWidth;
  Identifier->Type = Identifier->Type;                    // <=
  Tokens.erase(Tokens.end() - 1);
  return true;
}

PVS-Studio 警告: V570 “标识符->类型”变量被分配给其自身。 FormatTokenLexer.cpp 249

将变量分配给自身是没有意义的。 他们很可能想写:

Identifier->Type = Question->Type;

片段 N11:可疑的中断

void SystemZOperand::print(raw_ostream &OS) const {
  switch (Kind) {
    break;
  case KindToken:
    OS << "Token:" << getToken();
    break;
  case KindReg:
    OS << "Reg:" << SystemZInstPrinter::getRegisterName(getReg());
    break;
  ....
}

PVS-Studio 警告: V622 [CWE-478] 考虑检查“switch”语句。 第一个“case”运算符可能丢失。 SystemZAsmParser.cpp 652

一开始就有一个非常可疑的操作者 打破。 你是不是忘记在这里写点别的东西了?

片段 N12:取消引用后检查指针

InlineCost AMDGPUInliner::getInlineCost(CallSite CS) {
  Function *Callee = CS.getCalledFunction();
  Function *Caller = CS.getCaller();
  TargetTransformInfo &TTI = TTIWP->getTTI(*Callee);

  if (!Callee || Callee->isDeclaration())
    return llvm::InlineCost::getNever("undefined callee");
  ....
}

PVS-Studio 警告: V595 [CWE-476] 在针对 nullptr 进行验证之前,使用了“Callee”指针。 检查行:172、174。AMDGPUInline.cpp 172

索引 被叫方 开始时在调用函数时取消引用 获取TTI.

然后事实证明应该检查这个指针是否相等 空指针:

if (!Callee || Callee->isDeclaration())

但已经太晚了……

片段 N13 - N...:取消引用后检查指针

前面的代码片段中讨论的情况并不是唯一的。 它出现在这里:

static Value *optimizeDoubleFP(CallInst *CI, IRBuilder<> &B,
                               bool isBinary, bool isPrecise = false) {
  ....
  Function *CalleeFn = CI->getCalledFunction();
  StringRef CalleeNm = CalleeFn->getName();                 // <=
  AttributeList CalleeAt = CalleeFn->getAttributes();
  if (CalleeFn && !CalleeFn->isIntrinsic()) {               // <=
  ....
}

PVS-Studio 警告:V595 [CWE-476] 在针对 nullptr 进行验证之前,使用了“CalleeFn”指针。 检查行:1079、1081。SimplifyLibCalls.cpp 1079

在这里:

void Sema::InstantiateAttrs(const MultiLevelTemplateArgumentList &TemplateArgs,
                            const Decl *Tmpl, Decl *New,
                            LateInstantiatedAttrVec *LateAttrs,
                            LocalInstantiationScope *OuterMostScope) {
  ....
  NamedDecl *ND = dyn_cast<NamedDecl>(New);
  CXXRecordDecl *ThisContext =
    dyn_cast_or_null<CXXRecordDecl>(ND->getDeclContext());         // <=
  CXXThisScopeRAII ThisScope(*this, ThisContext, Qualifiers(),
                             ND && ND->isCXXInstanceMember());     // <=
  ....
}

PVS-Studio 警告:V595 [CWE-476] 在针对 nullptr 进行验证之前使用了“ND”指针。 检查行:532、534。SemaTemplateInstantiateDecl.cpp 532

在这里:

  • V595 [CWE-476] 在针对 nullptr 进行验证之前使用了“U”指针。 检查行:404、407。DWARFormValue.cpp 404
  • V595 [CWE-476] 在针对 nullptr 进行验证之前使用了“ND”指针。 检查行:2149、2151。SemaTemplateInstantiate.cpp 2149

然后我对研究编号为 V595 的警告不再感兴趣。 所以我不知道除了这里列出的错误之外是否还有更多类似的错误。 最有可能的是。

片段 N17、N18:可疑的转变

static inline bool processLogicalImmediate(uint64_t Imm, unsigned RegSize,
                                           uint64_t &Encoding) {
  ....
  unsigned Size = RegSize;
  ....
  uint64_t NImms = ~(Size-1) << 1;
  ....
}

PVS-Studio 警告: V629 [CWE-190] 考虑检查“~(Size - 1) << 1”表达式。 32 位值的位移,随后扩展到 64 位类型。 AArch64AddressingModes.h 260

这可能不是一个错误,并且代码完全按照预期工作。 但这显然是一个非常可疑的地方,需要检查。

我们来说一下变量 尺寸 等于16,然后代码的作者打算把它放在一个变量中 NImms 价值:

1111111111111111111111111111111111111111111111111111111111100000

然而,实际上结果将是:

0000000000000000000000000000000011111111111111111111111111100000

事实上,所有计算均使用 32 位无符号类型进行。 只有这样,这个 32 位无符号类型才会被隐式扩展为 uint64_t。 在这种情况下,最高有效位将为零。

您可以这样解决这种情况:

uint64_t NImms = ~static_cast<uint64_t>(Size-1) << 1;

类似情况:V629 [CWE-190] 考虑检查 'Immr << 6' 表达式。 32 位值的位移,随后扩展到 64 位类型。 AArch64AddressingModes.h 269

片段 N19:缺少关键字 其他?

void AMDGPUAsmParser::cvtDPP(MCInst &Inst, const OperandVector &Operands) {
  ....
  if (Op.isReg() && Op.Reg.RegNo == AMDGPU::VCC) {
    // VOP2b (v_add_u32, v_sub_u32 ...) dpp use "vcc" token.
    // Skip it.
    continue;
  } if (isRegOrImmWithInputMods(Desc, Inst.getNumOperands())) {    // <=
    Op.addRegWithFPInputModsOperands(Inst, 2);
  } else if (Op.isDPPCtrl()) {
    Op.addImmOperands(Inst, 1);
  } else if (Op.isImm()) {
    // Handle optional arguments
    OptionalIdx[Op.getImmTy()] = I;
  } else {
    llvm_unreachable("Invalid operand type");
  }
  ....
}

PVS-Studio 警告: V646 [CWE-670] 考虑检查应用程序的逻辑。 可能缺少“else”关键字。 AMDGPUAsmParser.cpp 5655

这里没有错误。 从第一个的 then 块开始 if 以。。结束 继续,那就没关系了,有一个关键字 其他 或不。 无论哪种方式,代码的工作方式都是一样的。 还是错过了 其他 使代码更加不清楚和危险。 如果将来 继续 消失后,代码将开始以完全不同的方式工作。 我认为最好添加 其他.

片段N20:同一类型的四个错别字

LLVM_DUMP_METHOD void Symbol::dump(raw_ostream &OS) const {
  std::string Result;
  if (isUndefined())
    Result += "(undef) ";
  if (isWeakDefined())
    Result += "(weak-def) ";
  if (isWeakReferenced())
    Result += "(weak-ref) ";
  if (isThreadLocalValue())
    Result += "(tlv) ";
  switch (Kind) {
  case SymbolKind::GlobalSymbol:
    Result + Name.str();                        // <=
    break;
  case SymbolKind::ObjectiveCClass:
    Result + "(ObjC Class) " + Name.str();      // <=
    break;
  case SymbolKind::ObjectiveCClassEHType:
    Result + "(ObjC Class EH) " + Name.str();   // <=
    break;
  case SymbolKind::ObjectiveCInstanceVariable:
    Result + "(ObjC IVar) " + Name.str();       // <=
    break;
  }
  OS << Result;
}

PVS-Studio 警告:

  • V655 [CWE-480] 字符串已连接但未使用。 考虑检查“Result + Name.str()”表达式。 符号.cpp 32
  • V655 [CWE-480] 字符串已连接但未使用。 考虑检查 'Result + "(ObjC Class)" + Name.str()' 表达式。 符号.cpp 35
  • V655 [CWE-480] 字符串已连接但未使用。 考虑检查 'Result + "(ObjC Class EH) " + Name.str()' 表达式。 符号.cpp 38
  • V655 [CWE-480] 字符串已连接但未使用。 考虑检查 'Result + "(ObjC IVar)" + Name.str()' 表达式。 符号.cpp 41

意外地,使用了 + 运算符而不是 += 运算符。 结果是设计毫无意义。

片段 N21:未定义的行为

static void getReqFeatures(std::map<StringRef, int> &FeaturesMap,
                           const std::vector<Record *> &ReqFeatures) {
  for (auto &R : ReqFeatures) {
    StringRef AsmCondString = R->getValueAsString("AssemblerCondString");

    SmallVector<StringRef, 4> Ops;
    SplitString(AsmCondString, Ops, ",");
    assert(!Ops.empty() && "AssemblerCondString cannot be empty");

    for (auto &Op : Ops) {
      assert(!Op.empty() && "Empty operator");
      if (FeaturesMap.find(Op) == FeaturesMap.end())
        FeaturesMap[Op] = FeaturesMap.size();
    }
  }
}

尝试自己找出危险代码。 这是一张分散注意力的图片,以免立即看答案:

使用 PVS-Studio 分析器查找 LLVM 8 中的错误

PVS-Studio 警告: V708 [CWE-758] 使用了危险的构造:“FeaturesMap[Op] = FeaturesMap.size()”,其中“FeaturesMap”属于“map”类。 这可能会导致未定义的行为。 RISCVCompressInstEmitter.cpp 490

问题线:

FeaturesMap[Op] = FeaturesMap.size();

如果元素 Op 没有找到,则在映射中创建一个新元素,并将该映射中的元素数量写入其中。 只是不知道该函数是否会被调用 尺寸 添加新元素之前或之后。

片段 N22-N24:重复分配

Error MachOObjectFile::checkSymbolTable() const {
  ....
  } else {
    MachO::nlist STE = getSymbolTableEntry(SymDRI);
    NType = STE.n_type;                              // <=
    NType = STE.n_type;                              // <=
    NSect = STE.n_sect;
    NDesc = STE.n_desc;
    NStrx = STE.n_strx;
    NValue = STE.n_value;
  }
  ....
}

PVS-Studio 警告: V519 [CWE-563] 'NType' 变量被连续赋值两次。 也许这是一个错误。 检查行:1663、1664。MachOObjectFile.cpp 1664

我认为这里没有真正的错误。 只是不必要的重复分配。 但仍然是一个错误。

类似于:

  • V519 [CWE-563] 'B.NDesc' 变量被连续赋值两次。 也许这是一个错误。 检查行:1488、1489。llvm-nm.cpp 1489
  • V519 [CWE-563] 变量连续赋值两次。 也许这是一个错误。 检查行:59、61。coff2yaml.cpp 61

片段 N25-N27:更多重新分配

现在让我们看一下稍微不同的重新分配版本。

bool Vectorizer::vectorizeLoadChain(
    ArrayRef<Instruction *> Chain,
    SmallPtrSet<Instruction *, 16> *InstructionsProcessed) {
  ....
  unsigned Alignment = getAlignment(L0);
  ....
  unsigned NewAlign = getOrEnforceKnownAlignment(L0->getPointerOperand(),
                                                 StackAdjustedAlignment,
                                                 DL, L0, nullptr, &DT);
  if (NewAlign != 0)
    Alignment = NewAlign;
  Alignment = NewAlign;
  ....
}

PVS-Studio 警告:V519 [CWE-563] 'Alignment' 变量连续赋值两次。 也许这是一个错误。 检查行:1158、1160。LoadStoreVectorizer.cpp 1160

这是非常奇怪的代码,显然包含逻辑错误。 一开始,变量 对准 根据条件分配一个值。 然后分配再次发生,但现在没有任何检查。

类似的情况可以在这里看到:

  • V519 [CWE-563] 'Effects' 变量连续赋值两次。 也许这是一个错误。 检查行:152、165。WebAssemblyRegStackify.cpp 165
  • V519 [CWE-563] 'ExpectNoDerefChunk' 变量连续赋值两次。 也许这是一个错误。 检查行:4970、4973。SemaType.cpp 4973

片段 N28:始终为真条件

static int readPrefixes(struct InternalInstruction* insn) {
  ....
  uint8_t byte = 0;
  uint8_t nextByte;
  ....
  if (byte == 0xf3 && (nextByte == 0x88 || nextByte == 0x89 ||
                       nextByte == 0xc6 || nextByte == 0xc7)) {
    insn->xAcquireRelease = true;
    if (nextByte != 0x90) // PAUSE instruction support             // <=
      break;
  }
  ....
}

PVS-Studio 警告: V547 [CWE-571] 表达式“nextByte != 0x90”始终为 true。 X86DisassemblerDecoder.cpp 379

检查没有意义。 多变的 下一个字节 总是不等于该值 0x90,这是从之前的检查得出的。 这是某种逻辑错误。

片段 N29 - N...:始终为真/假条件

分析仪发出许多警告,表明整个情况(V547)或其一部分(V560) 总是 true 或 false。 通常这些并不是真正的错误,而只是草率的代码、宏展开的结果等等。 然而,查看所有这些警告是有意义的,因为真正的逻辑错误确实时常发生。 例如,这一段代码就很可疑:

static DecodeStatus DecodeGPRPairRegisterClass(MCInst &Inst, unsigned RegNo,
                                   uint64_t Address, const void *Decoder) {
  DecodeStatus S = MCDisassembler::Success;

  if (RegNo > 13)
    return MCDisassembler::Fail;

  if ((RegNo & 1) || RegNo == 0xe)
     S = MCDisassembler::SoftFail;
  ....
}

PVS-Studio 警告: V560 [CWE-570] 条件表达式的一部分始终为 false:RegNo == 0xe。 ARM反汇编程序.cpp 939

常量 0xE 是十进制值 14。 考试 注册号 == 0xe 没有意义,因为如果 注册号 > 13,那么该函数将完成其执行。

ID 为 V547 和 V560 的还有许多其他警告,但与 V595,我没有兴趣研究这些警告。 很明显我有足够的材料来写一篇文章:)。 因此,不知道使用 PVS-Studio 在 LLVM 中可以识别多少此类错误。

我将举一个例子来说明为什么研究这些触发因素很无聊。 分析器对以下代码发出警告是绝对正确的。 但这并不是一个错误。

bool UnwrappedLineParser::parseBracedList(bool ContinueOnSemicolons,
                                          tok::TokenKind ClosingBraceKind) {
  bool HasError = false;
  ....
  HasError = true;
  if (!ContinueOnSemicolons)
    return !HasError;
  ....
}

PVS-Studio 警告:V547 [CWE-570] 表达式“!HasError”始终为 false。 UnwrappedLineParser.cpp 1635

片段N30:可疑回归

static bool
isImplicitlyDef(MachineRegisterInfo &MRI, unsigned Reg) {
  for (MachineRegisterInfo::def_instr_iterator It = MRI.def_instr_begin(Reg),
      E = MRI.def_instr_end(); It != E; ++It) {
    return (*It).isImplicitDef();
  }
  ....
}

PVS-Studio 警告: V612 [CWE-670] 循环内的无条件“返回”。 R600OptimizeVectorRegisters.cpp 63

这要么是一个错误,要么是一种旨在向阅读代码的程序员解释某些内容的特定技术。 这个设计对我来说没有任何解释,而且看起来很可疑。 最好不要这样写:)。

疲劳的? 然后是泡茶或咖啡的时间了。

使用 PVS-Studio 分析器查找 LLVM 8 中的错误

新诊断发现的缺陷

我认为旧诊断激活 30 次就足够了。 现在让我们看看分析仪中出现的新诊断可以发现哪些有趣的事情 检查。 在此期间,C++ 分析器中总共添加了 66 个通用诊断。

片段 N31:无法访问的代码

Error CtorDtorRunner::run() {
  ....
  if (auto CtorDtorMap =
          ES.lookup(JITDylibSearchList({{&JD, true}}), std::move(Names),
                    NoDependenciesToRegister, true))
  {
    ....
    return Error::success();
  } else
    return CtorDtorMap.takeError();

  CtorDtorsByPriority.clear();

  return Error::success();
}

PVS-Studio 警告: V779 [CWE-561] 检测到无法访问的代码。 可能存在错误。 执行实用程序.cpp 146

如您所见,运算符的两个分支 if 以呼叫接线员结束 回报。 据此,容器 按优先级排序的成员 永远不会被清除。

片段 N32:无法访问的代码

bool LLParser::ParseSummaryEntry() {
  ....
  switch (Lex.getKind()) {
  case lltok::kw_gv:
    return ParseGVEntry(SummaryID);
  case lltok::kw_module:
    return ParseModuleEntry(SummaryID);
  case lltok::kw_typeid:
    return ParseTypeIdEntry(SummaryID);                        // <=
    break;                                                     // <=
  default:
    return Error(Lex.getLoc(), "unexpected summary kind");
  }
  Lex.setIgnoreColonInIdentifiers(false);                      // <=
  return false;
}

PVS-Studio 警告:V779 [CWE-561] 检测到无法访问的代码。 可能存在错误。 LLParser.cpp 835

有趣的情况。 我们先看看这个地方:

return ParseTypeIdEntry(SummaryID);
break;

乍一看,这里似乎没有错误。 看起来像运营商 打破 这里多了一个,直接删除即可。 然而,事情并非如此简单。

分析器在以下几行发出警告:

Lex.setIgnoreColonInIdentifiers(false);
return false;

事实上,这段代码是无法访问的。 所有病例均在 开关 以接线员的呼叫结束 回报。 现在独自无知 打破 看起来并不那么无害! 也许其中一个分支应该以 打破但不是 回报?

片段N33:高位随机复位

unsigned getStubAlignment() override {
  if (Arch == Triple::systemz)
    return 8;
  else
    return 1;
}

Expected<unsigned>
RuntimeDyldImpl::emitSection(const ObjectFile &Obj,
                             const SectionRef &Section,
                             bool IsCode) {
  ....
  uint64_t DataSize = Section.getSize();
  ....
  if (StubBufSize > 0)
    DataSize &= ~(getStubAlignment() - 1);
  ....
}

PVS-Studio 警告: V784 位掩码的大小小于第一操作数的大小。 这将导致高位的丢失。 运行时Dyld.cpp 815

请注意该函数 获取StubAlignment 返回类型 无符号。 让我们计算表达式的值,假设函数返回值 8:

~(getStubAlignment() - 1)

~(8u-1)

0xFFFFFFFF8u

现在请注意变量 数据大小 具有 64 位无符号类型。 事实证明,当执行DataSize & 0xFFFFFFF8u操作时,所有0个高位将被重置为零。 这很可能不是程序员想要的。 我怀疑他想计算:DataSize & 8xFFFFFFFFFFFFFFFXNUMXu。

要修复该错误,您应该这样写:

DataSize &= ~(static_cast<uint64_t>(getStubAlignment()) - 1);

或者如此:

DataSize &= ~(getStubAlignment() - 1ULL);

片段 N34:显式类型转换失败

template <typename T>
void scaleShuffleMask(int Scale, ArrayRef<T> Mask,
                      SmallVectorImpl<T> &ScaledMask) {
  assert(0 < Scale && "Unexpected scaling factor");
  int NumElts = Mask.size();
  ScaledMask.assign(static_cast<size_t>(NumElts * Scale), -1);
  ....
}

PVS-Studio 警告: V1028 [CWE-190] 可能溢出。 考虑将“NumElts * Scale”运算符的操作数转换为“size_t”类型,而不是结果。 X86ISelLowering.h 1577

显式类型转换用于避免类型变量相乘时溢出 INT。 然而,这里的显式类型转换并不能防止溢出。 首先,变量将被相乘,然后乘法的 32 位结果将扩展为类型 尺寸_t.

片段 N35:复制粘贴失败

Instruction *InstCombiner::visitFCmpInst(FCmpInst &I) {
  ....
  if (!match(Op0, m_PosZeroFP()) && isKnownNeverNaN(Op0, &TLI)) {
    I.setOperand(0, ConstantFP::getNullValue(Op0->getType()));
    return &I;
  }
  if (!match(Op1, m_PosZeroFP()) && isKnownNeverNaN(Op1, &TLI)) {
    I.setOperand(1, ConstantFP::getNullValue(Op0->getType()));        // <=
    return &I;
  }
  ....
}

V778 [CWE-682] 发现两个相似的代码片段。 也许,这是一个拼写错误,应该使用“Op1”变量而不是“Op0”。 InstCombineCompares.cpp 5507

这个新的有趣的诊断可以识别一段代码已被复制并且其中的某些名称已开始更改,但在一个地方尚未更正的情况。

请注意,在第二个块中他们改变了 01。 但在一个地方他们没有解决这个问题。 最有可能的是应该这样写:

if (!match(Op1, m_PosZeroFP()) && isKnownNeverNaN(Op1, &TLI)) {
  I.setOperand(1, ConstantFP::getNullValue(Op1->getType()));
  return &I;
}

片段 N36:变量混乱

struct Status {
  unsigned Mask;
  unsigned Mode;

  Status() : Mask(0), Mode(0){};

  Status(unsigned Mask, unsigned Mode) : Mask(Mask), Mode(Mode) {
    Mode &= Mask;
  };
  ....
};

PVS-Studio 警告: V1001 [CWE-563] 已分配“Mode”变量,但函数末尾未使用该变量。 SIModeRegister.cpp 48

将函数参数与类成员同名是非常危险的。 这很容易让人困惑。 我们面前就有这样一个案例。 这个表达式没有意义:

Mode &= Mask;

函数参数发生变化。 就这样。 该论证已不再使用。 最有可能的是你应该这样写:

Status(unsigned Mask, unsigned Mode) : Mask(Mask), Mode(Mode) {
  this->Mode &= Mask;
};

片段 N37:变量混乱

class SectionBase {
  ....
  uint64_t Size = 0;
  ....
};

class SymbolTableSection : public SectionBase {
  ....
};

void SymbolTableSection::addSymbol(Twine Name, uint8_t Bind, uint8_t Type,
                                   SectionBase *DefinedIn, uint64_t Value,
                                   uint8_t Visibility, uint16_t Shndx,
                                   uint64_t Size) {
  ....
  Sym.Value = Value;
  Sym.Visibility = Visibility;
  Sym.Size = Size;
  Sym.Index = Symbols.size();
  Symbols.emplace_back(llvm::make_unique<Symbol>(Sym));
  Size += this->EntrySize;
}

警告 PVS-Studio:V1001 [CWE-563] 已分配“大小”变量,但函数末尾未使用该变量。 对象.cpp 424

情况与上一类似。 应该写成:

this->Size += this->EntrySize;

片段 N38-N47:他们忘记检查索引

之前,我们查看了诊断触发的示例 V595。 它的本质是指针在开始时被取消引用,然后才被检查。 年轻的诊断 V1004 意思相反,而且也暴露出很多错误。 它标识了在开始时检查指针然后忘记这样做的情况。 让我们看看 LLVM 中发现的此类案例。

int getGEPCost(Type *PointeeType, const Value *Ptr,
               ArrayRef<const Value *> Operands) {
  ....
  if (Ptr != nullptr) {                                            // <=
    assert(....);
    BaseGV = dyn_cast<GlobalValue>(Ptr->stripPointerCasts());
  }
  bool HasBaseReg = (BaseGV == nullptr);

  auto PtrSizeBits = DL.getPointerTypeSizeInBits(Ptr->getType());  // <=
  ....
}

PVS-Studio 警告:V1004 [CWE-476] 'Ptr' 指针在针对 nullptr 进行验证后使用不安全。 检查行:729、738。TargetTransformInfoImpl.h 738

变量 PTR 可能相等 空指针,正如检查所证明的:

if (Ptr != nullptr)

然而,下面这个指针在没有初步检查的情况下被取消引用:

auto PtrSizeBits = DL.getPointerTypeSizeInBits(Ptr->getType());

让我们考虑另一个类似的案例。

llvm::DISubprogram *CGDebugInfo::getFunctionFwdDeclOrStub(GlobalDecl GD,
                                                          bool Stub) {
  ....
  auto *FD = dyn_cast<FunctionDecl>(GD.getDecl());
  SmallVector<QualType, 16> ArgTypes;
  if (FD)                                                                // <=
    for (const ParmVarDecl *Parm : FD->parameters())
      ArgTypes.push_back(Parm->getType());
  CallingConv CC = FD->getType()->castAs<FunctionType>()->getCallConv(); // <=
  ....
}

PVS-Studio 警告:V1004 [CWE-476] 'FD' 指针在针对 nullptr 进行验证后使用不安全。 检查行:3228、3231。CGDebugInfo.cpp 3231

注意标志 FD。 我确信问题是显而易见的,不需要特殊解释。

还有更多:

static void computePolynomialFromPointer(Value &Ptr, Polynomial &Result,
                                         Value *&BasePtr,
                                         const DataLayout &DL) {
  PointerType *PtrTy = dyn_cast<PointerType>(Ptr.getType());
  if (!PtrTy) {                                                   // <=
    Result = Polynomial();
    BasePtr = nullptr;
  }
  unsigned PointerBits =
      DL.getIndexSizeInBits(PtrTy->getPointerAddressSpace());     // <=
  ....
}

PVS-Studio 警告:V1004 [CWE-476] 'PtrTy' 指针在针对 nullptr 进行验证后使用不安全。 检查行:960、965。InterleavedLoadCombinePass.cpp 965

如何保护自己免受此类错误的影响? 更加关注代码审查并使用 PVS-Studio 静态分析器定期检查您的代码。

引用具有此类错误的其他代码片段是没有意义的。 我只会在文章中留下警告列表:

  • V1004 [CWE-476] 'Expr' 指针在针对 nullptr 进行验证后被不安全地使用。 检查行:1049、1078。DebugInfoMetadata.cpp 1078
  • V1004 [CWE-476] “PI”指针在针对 nullptr 进行验证后被不安全地使用。 检查行:733、753。LegacyPassManager.cpp 753
  • V1004 [CWE-476] “StatepointCall”指针在针对 nullptr 进行验证后被不安全地使用。 检查行:4371、4379。Verifier.cpp 4379
  • V1004 [CWE-476] 'RV' 指针在针对 nullptr 进行验证后被不安全地使用。 检查行:2263、2268。TGParser.cpp 2268
  • V1004 [CWE-476] 'CalleeFn' 指针在针对 nullptr 进行验证后被不安全地使用。 检查行:1081、1096。SimplifyLibCalls.cpp 1096
  • V1004 [CWE-476] 'TC' 指针在针对 nullptr 进行验证后被不安全地使用。 检查行:1819、1824。Driver.cpp 1824

片段 N48-N60:不重要,但存在缺陷(可能存在内存泄漏)

std::unique_ptr<IRMutator> createISelMutator() {
  ....
  std::vector<std::unique_ptr<IRMutationStrategy>> Strategies;
  Strategies.emplace_back(
      new InjectorIRStrategy(InjectorIRStrategy::getDefaultOps()));
  ....
}

PVS-Studio 警告: V1023 [CWE-460] 没有所有者的指针通过“emplace_back”方法添加到“策略”容器中。 如果出现异常,就会发生内存泄漏。 llvm-isel-fuzzer.cpp 58

将元素添加到容器的末尾,例如 std::向量 > 你不能只写 xxx.push_back(新X),因为没有隐式转换 X* в std::unique_ptr.

一个常见的解决方案是写 xxx.emplace_back(新X)因为它编译:方法 emplace_back 直接从参数构造一个元素,因此可以使用显式构造函数。

这是不安全的。 如果向量已满,则重新分配内存。 内存重新分配操作可能会失败,导致抛出异常 std::bad_alloc。 在这种情况下,指针将丢失,并且创建的对象将永远不会被删除。

一个安全的解决方案是创建 唯一指针在向量尝试重新分配内存之前它将拥有指针:

xxx.push_back(std::unique_ptr<X>(new X))

从 C++14 开始,您可以使用 'std::make_unique':

xxx.push_back(std::make_unique<X>())

这种类型的缺陷对于 LLVM 来说并不重要。 如果无法分配内存,编译器将停止。 然而,对于长的应用 正常运行时间,如果内存分配失败,它就不能终止,这可能是一个真正令人讨厌的错误。

因此,尽管此代码不会对 LLVM 构成实际威胁,但我发现讨论此错误模式很有用,而且 PVS-Studio 分析器已经学会识别它。

此类其他警告:

  • V1023 [CWE-460] 没有所有者的指针通过“emplace_back”方法添加到“Passes”容器中。 如果出现异常,就会发生内存泄漏。 通行证管理器.h 546
  • V1023 [CWE-460] 没有所有者的指针通过“emplace_back”方法添加到“AAs”容器中。 如果出现异常,就会发生内存泄漏。 别名分析.h 324
  • V1023 [CWE-460] 没有所有者的指针通过“emplace_back”方法添加到“Entries”容器中。 如果出现异常,就会发生内存泄漏。 DWARFDebugFrame.cpp 519
  • V1023 [CWE-460] 没有所有者的指针通过“emplace_back”方法添加到“AllEdges”容器中。 如果出现异常,就会发生内存泄漏。 CFGMST.h 268
  • V1023 [CWE-460] 没有所有者的指针通过“emplace_back”方法添加到“VMaps”容器中。 如果出现异常,就会发生内存泄漏。 SimpleLoopUnswitch.cpp 2012
  • V1023 [CWE-460] 没有所有者的指针通过“emplace_back”方法添加到“Records”容器中。 如果出现异常,就会发生内存泄漏。 FDRLogBuilder.h 30
  • V1023 [CWE-460] 没有所有者的指针通过“emplace_back”方法添加到“PendingSubmodules”容器中。 如果出现异常,就会发生内存泄漏。 模块映射.cpp 810
  • V1023 [CWE-460] 没有所有者的指针通过“emplace_back”方法添加到“对象”容器中。 如果出现异常,就会发生内存泄漏。 调试映射.cpp 88
  • V1023 [CWE-460] 没有所有者的指针通过“emplace_back”方法添加到“策略”容器中。 如果出现异常,就会发生内存泄漏。 llvm-isel-fuzzer.cpp 60
  • V1023 [CWE-460] 没有所有者的指针通过 'emplace_back' 方法添加到 'Modifiers' 容器中。 如果出现异常,就会发生内存泄漏。 llvm-应力.cpp 685
  • V1023 [CWE-460] 没有所有者的指针通过 'emplace_back' 方法添加到 'Modifiers' 容器中。 如果出现异常,就会发生内存泄漏。 llvm-应力.cpp 686
  • V1023 [CWE-460] 没有所有者的指针通过 'emplace_back' 方法添加到 'Modifiers' 容器中。 如果出现异常,就会发生内存泄漏。 llvm-应力.cpp 688
  • V1023 [CWE-460] 没有所有者的指针通过 'emplace_back' 方法添加到 'Modifiers' 容器中。 如果出现异常,就会发生内存泄漏。 llvm-应力.cpp 689
  • V1023 [CWE-460] 没有所有者的指针通过 'emplace_back' 方法添加到 'Modifiers' 容器中。 如果出现异常,就会发生内存泄漏。 llvm-应力.cpp 690
  • V1023 [CWE-460] 没有所有者的指针通过 'emplace_back' 方法添加到 'Modifiers' 容器中。 如果出现异常,就会发生内存泄漏。 llvm-应力.cpp 691
  • V1023 [CWE-460] 没有所有者的指针通过 'emplace_back' 方法添加到 'Modifiers' 容器中。 如果出现异常,就会发生内存泄漏。 llvm-应力.cpp 692
  • V1023 [CWE-460] 没有所有者的指针通过 'emplace_back' 方法添加到 'Modifiers' 容器中。 如果出现异常,就会发生内存泄漏。 llvm-应力.cpp 693
  • V1023 [CWE-460] 没有所有者的指针通过 'emplace_back' 方法添加到 'Modifiers' 容器中。 如果出现异常,就会发生内存泄漏。 llvm-应力.cpp 694
  • V1023 [CWE-460] 没有所有者的指针通过“emplace_back”方法添加到“操作数”容器中。 如果出现异常,就会发生内存泄漏。 GlobalISelEmitter.cpp 1911
  • V1023 [CWE-460] 没有所有者的指针通过“emplace_back”方法添加到“Stash”容器中。 如果出现异常,就会发生内存泄漏。 GlobalISelEmitter.cpp 2100
  • V1023 [CWE-460] 没有所有者的指针通过“emplace_back”方法添加到“Matchers”容器中。 如果出现异常,就会发生内存泄漏。 GlobalISelEmitter.cpp 2702

结论

我一共发出了60次警告,然后就停止了。 PVS-Studio 分析器在 LLVM 中检测到其他缺陷吗? 是的,我有。 然而,当我为这篇文章编写代码片段时,已经是傍晚,甚至是晚上了,我决定是时候到此为止了。

我希望您觉得它很有趣并且想要尝试 PVS-Studio 分析器。

您可以下载分析器并获取扫雷密钥: 此页.

最重要的是,定期使用静态分析。 一次性检查我们为了普及静态分析方法而进行的,PVS-Studio并不是一个正常的场景。

祝您在提高代码质量和可靠性方面好运!

使用 PVS-Studio 分析器查找 LLVM 8 中的错误

如果您想与英语读者分享这篇文章,请使用翻译链接:Andrey Karpov。 使用 PVS-Studio 查找 LLVM 8 中的错误.

来源: habr.com

添加评论