使用 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

這個新的有趣的診斷可以識別一段代碼已被複製並且其中的某些名稱已開始更改,但在一個地方尚未更正的情況。

請注意,在第二個區塊中他們改變了 在0在1。 但在一個地方他們沒有解決這個問題。 最有可能的是應該這樣寫:

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

多變的 指針 可能相等 空指針,正如檢查所證明的:

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 中的錯誤.

來源: www.habr.com

添加評論