Kontrola Clang 11 s PVS-Studio

(28. října 2020) )

Občas musíme psát články o tom, jak jsme zkontrolovali jinou čerstvou verzi nějakého kompilátoru. To není moc legrace. Jak však ukazuje praxe, pokud to na chvíli přestaneme dělat, lidé začnou pochybovat, zda PVS-Studio stojí za svůj název dobrého chytače chyb a zranitelností. Co když to dokáže i nový kompilátor? Jistě, kompilátory se vyvíjejí, ale také PVS-Studio – a znovu a znovu dokazuje jeho schopnost chytat chyby i ve vysoce kvalitních projektech, jako jsou kompilátory.

Je čas znovu zkontrolovat Clang

Abych řekl pravdu, napsal jsem tento článek na základě k dřívějšímu příspěvku „ Kontrola kompilátoru GCC 10 s PVS-Studio “. Pokud se vám tedy některé odstavce zdají povědomé, je to proto, že jste si je již přečetli :).

Není žádným tajemstvím, že kompilátoři používají své vlastní integrované analyzátory statického kódu a ty se také vyvíjejí. Proto tu a tam píšeme články, abychom ukázali, že náš statický analyzátor, PVS-Studio, dokáže najít chyby i uvnitř kompilátorů a že si stojíme za svou sůl :).

Ve skutečnosti nemůžete porovnat klasické statické analyzátory s kompilátory. Statické analyzátory nejen detekují chyby ve zdrojovém kódu, ale zahrnují také vysoce rozvinutou infrastrukturu. Za prvé zahrnuje integraci s takovými systémy, jako jsou SonarQube, PlatformIO, Azure DevOps, Travis CI, CircleCI, GitLab CI / CD, Jenkins a Visual Studio. Zahrnuje mechanismy pro hromadné potlačení varování, což vám umožní začít používat PVS-Studio hned ve velkém projektu. Zahrnuje zasílání upozornění e-mailem. A tak dále a tak dále. První otázka, kterou si vývojáři budou stále klást, je: „Může vaše PVS-Studio najít něco, co kompilátoři nemohou?“ A to znamená, že jsme odsouzeni psát články o tom, jak znovu a znovu kontrolujeme samotné překladače.

Vraťme se zpět k Clangu. Není třeba se zabývat tématem a říkat vám, o čem projekt je. Ve skutečnosti jsme zkontrolovali nejen kód samotného Clangu 11, ale také kód knihovny LLVM 11, na které je založen. Z hlediska tohoto článku nezáleží na tom, zda byla v kódu kompilátoru nebo v knihovně nalezena chyba.

Našel jsem kód Clang / LLVM mnohem jasnější než kód GCC. Přinejmenším to není plné všech těch hrozných maker a ve velké míře využívá moderní funkce C ++.

Přesto je projekt stále dostatečně velký na to, aby zkoumání analytické zprávy bylo zdlouhavé bez předchozího přizpůsobení. To, co většinou stojí v cestě, jsou „polo-falešná“ pozitiva. „Polofalešnými“ pozitivy mám na mysli případy, kdy je analyzátor technicky správný, aby poukázal na určité problémy, ale tato varování nemají praktický význam. Mnoho takových varování se například týká jednotkových testů a generovaného kódu.

Zde je příklad jednotkových testů:

Spaces.SpacesInParentheses = false; // <=
Spaces.SpacesInCStyleCastParentheses = true; // <=
verifyFormat("Type *A = ( Type * )P;", Spaces);
verifyFormat("Type *A = ( vector )P;", Spaces);
verifyFormat("x = ( int32 )y;", Spaces);
verifyFormat("int a = ( int )(2.0f);", Spaces);
verifyFormat("#define AA(X) sizeof((( X * )NULL)->a)", Spaces);
verifyFormat("my_int a = ( my_int )sizeof(int);", Spaces);
verifyFormat("#define x (( int )-1)", Spaces);// Run the first set of tests again with:
Spaces.SpacesInParentheses = false; // <=
Spaces.SpaceInEmptyParentheses = true;
Spaces.SpacesInCStyleCastParentheses = true; // <=
verifyFormat("call(x, y, z);", Spaces);
verifyFormat("call( );", Spaces);

analyzátor nás varuje, že proměnným jsou přiřazeny stejné hodnoty, jaké již mají:

  • V1048 Proměnné „Spaces.SpacesInParentheses“ byla přiřazena stejná hodnota. FormatTest.cpp 11554
  • V1048 Proměnné „Spaces.SpacesInCStyleCastParentheses“ byla přiřazena stejná hodnota. FormatTest.cpp 11556

Technicky je toto varování k věci a úryvek potřebuje zjednodušení nebo opravu. Je však také jasné, že tento kód je v pořádku, tak jak je, a nemá smysl v něm nic opravovat.

Zde je další příklad: analyzátor vydá spoustu varování na automaticky generovaný soubor Options.inc. Podívejte se na „zeď“ kódu, který obsahuje:

Tato část kódu spouští záplavu varování:

  • V501 Vlevo a napravo od operátoru == jsou shodné dílčí výrazy: nullptr == nullptr Options.inc 26
  • V501 Existují identické podvýrazy vlevo a vpravo od operátoru ==: nullptr == nullptr Options.inc 27
  • V501 There jsou shodné dílčí výrazy nalevo a napravo od operátoru ==: nullptr == nullptr Options.inc 28
  • atd. – jedno varování na řádek …

Přesto to není velký problém. To lze vyřešit vyloučením irelevantních souborů z analýzy, označením určitých maker a funkcí, potlačením určitých diagnostických typů atd. Ano, může, ale není to příliš zajímavá práce, když píšete článek. Proto jsem udělal totéž jako v článku o kontrole kompilátoru GCC: Četl jsem zprávu, dokud jsem neshromáždil 11 zajímavých příkladů, které jsem do článku zahrnul. Proč 11? Jen jsem si myslel, že protože to byla jedenáctá verze Clangu, potřeboval jsem 11 příkladů :).

11 fragmentů podezřelého kódu

Úryvek 1, operace modulo na 1

Toto je super! Mám rád takové chyby!

void Act() override {
....
// If the value type is a vector, and we allow vector select, then in 50%
// of the cases generate a vector select.
if (isa(Val0->getType()) && (getRandom() % 1)) {
unsigned NumElem =
cast(Val0->getType())->getNumElements();
CondTy = FixedVectorType::get(CondTy, NumElem);
}
....
}

Diagnostická zpráva PVS-Studio: V1063 Operace modulo by 1 nemá smysl. Výsledek bude vždy nulový. llvm-stress.cpp 631

Programátor používá operaci modulo k získání náhodné hodnoty buď 0 nebo 1. Zdá se však, že hodnota 1 matí vývojáře a přimět je, aby napsali klasický anti-vzor, ​​ve kterém se operace modulo provádí na 1 místo na 2. Operace X% 1 nemá smysl, protože se vždy vyhodnotí na 0 . Toto je pevná verze:

if (isa(Val0->getType()) && (getRandom() % 2)) {

Nedávno přidaná diagnostika V1063 je strašně jednoduchá, ale jak vidíte, funguje perfektně.

Víme, že vývojáři překladačů sledují naši práci a půjčují si naše nápady. To je v pořádku. Je hezké vědět, že PVS-Studio je hnací silou pokroku . Podívejme se, kolik bude trvat, než se podobná diagnostika objeví v Clang a GCC :).

Snippet 2, překlep v podmínce

class ReturnValueSlot {
....
bool isNull() const { return !Addr.isValid(); }
....
};static bool haveSameParameterTypes(ASTContext &Context, const FunctionDecl *F1,
const FunctionDecl *F2, unsigned NumParams) {
....
unsigned I1 = 0, I2 = 0;
for (unsigned I = 0; I != NumParams; ++I) {
QualType T1 = NextParam(F1, I1, I == 0);
QualType T2 = NextParam(F2, I2, I == 0);
if (!T1.isNull() && !T1.isNull() && !Context.hasSameUnqualifiedType(T1, T2))
return false;
}
return true;
}

Diagnostická zpráva PVS-Studio: V501 Vlevo a napravo od & & operátor:! T1.isNull () & &! T1.isNull () SemaOverload.cpp 9493

Kontrola ! T1.isNull () se provádí dvakrát. To je samozřejmě překlep; druhá část podmínky musí zkontrolovat proměnnou T2 .

Úryvek 3, potenciální pole-index-out-of- hranice

std::vector DeclsLoaded;SourceLocation ASTReader::getSourceLocationForDeclID(GlobalDeclID ID) {
....
unsigned Index = ID - NUM_PREDEF_DECL_IDS; if (Index > DeclsLoaded.size()) {
Error("declaration ID out-of-range for AST file");
return SourceLocation();
} if (Decl *D = DeclsLoaded[Index])
return D->getLocation();
....
}

Diagnostická zpráva PVS-Studio: Je možné překročení pole V557. Index „Index“ směřuje za hranice pole. ASTReader.cpp 7318

Předpokládejme, že pole obsahuje jeden prvek a hodnota proměnné Index je také 1. Pak podmínka (1> 1) je false, a proto bude pole indexováno za jeho hranice. Zde je správná kontrola:

if (Index >= DeclsLoaded.size()) {

Snippet 4, pořadí vyhodnocení argumentů

void IHexELFBuilder::addDataSections() {
....
uint32_t SecNo = 1;
....
Section = &Obj->addSection(
".sec" + std::to_string(SecNo++), RecAddr,
ELF::SHF_ALLOC | ELF::SHF_WRITE, SecNo);
....
}

Diagnostická zpráva PVS-Studio: V567 Nespecifikované chování. Pořadí vyhodnocení argumentů není definováno pro funkci „addSection“. Zvažte kontrolu proměnné „SecNo“. Object.cpp 1223

Všimněte si, že argument SecNo se používá dvakrát a mezitím se zvyšuje. Problém je, že nemůžete určit, v jakém přesném pořadí budou argumenty hodnoceny. Výsledek se proto bude lišit v závislosti na verzi kompilátoru nebo parametrech kompilace.

Zde je syntetický příklad pro ilustraci tohoto bodu:

#include 
int main()
{
int i = 1;
printf("%d, %d\n", i, i++);
return 0;
}

V závislosti na kompilátoru může tento kód obsahovat buď „1, 2“, nebo „2, 1“. Spustil jsem to v Průzkumníku kompilátorů a dostal jsem následující výstupy:

  • při kompilaci s Clang 11.0.0 program výstupy 1 , 1.
  • při kompilaci s GCC 10.2 bude program výstup s 2, 1.

Je zajímavé, že tento jednoduchý případ způsobí, že Clang vydá varování:

:6:26: warning:
unsequenced modification and access to "i" [-Wunsequenced]
printf("%d, %d\n", i, i++);

Z nějakého důvodu však toto varování nebylo vydáno na skutečný kód. Buď je deaktivován jako nepříliš praktický, nebo je tento případ příliš komplikovaný na to, aby si s ním překladač poradil.

Snippet 5, podivná kontrola duplikátů

template <class ELFT>
void GNUStyle::printVersionSymbolSection(const ELFFile *Obj,
const Elf_Shdr *Sec) { ....
Expected NameOrErr =
this->dumper()->getSymbolVersionByIndex(Ndx, IsDefault);
if (!NameOrErr) {
if (!NameOrErr) {
unsigned SecNdx = Sec - &cantFail(Obj->sections()).front();
this->reportUniqueWarning(createError(
"unable to get a version for entry " + Twine(I) +
" of SHT_GNU_versym section with index " + Twine(SecNdx) + ": " +
toString(NameOrErr.takeError())));
}
Versions.emplace_back("");
continue;
}
....
}

Diagnostická zpráva PVS-Studio: V571 Opakovaná kontrola. Podmínka „if (! NameOrErr)“ byla již ověřena na řádku 4666. ELFDumper.cpp 4667

Druhá kontrola je klonem první a je proto nadbytečná. Možná by to mohlo být bezpečně odstraněno. Pravděpodobnější však je, že obsahuje překlep a měl zkontrolovat nějakou jinou proměnnou.

Úryvek 6, potenciální dereference nulového ukazatele

void RewriteObjCFragileABI::RewriteObjCClassMetaData(
ObjCImplementationDecl *IDecl, std::string &Result)
{
ObjCInterfaceDecl *CDecl = IDecl->getClassInterface(); if (CDecl->isImplicitInterfaceDecl()) {
RewriteObjCInternalStruct(CDecl, Result);
} unsigned NumIvars = !IDecl->ivar_empty()
? IDecl->ivar_size()
: (CDecl ? CDecl->ivar_size() : 0);
....
}

Diagnostická zpráva PVS-Studio: V595 Ukazatel CDecl byl použit před ověřením proti nullptr. Kontrolní řádky: 5275, 5284. RewriteObjC.cpp 5275

Při provádění první kontroly vývojář nikdy neváhá s dereferencí ukazatele CDecl :

if (CDecl->isImplicitInterfaceDecl())

Pokud se však podíváte na kód o několik řádků dále, bude jasné, že ukazatel může mít hodnotu null:

(CDecl ? CDecl->ivar_size() : 0)

První kontrola měla pravděpodobně vypadat takto:

if (CDecl && CDecl->isImplicitInterfaceDecl())

Úryvek 7, potenciální dereference nulového ukazatele

bool
Sema::InstantiateClass(....)
{
....
NamedDecl *ND = dyn_cast(I->NewDecl);
CXXRecordDecl *ThisContext =
dyn_cast_or_null(ND->getDeclContext());
CXXThisScopeRAII ThisScope(*this, ThisContext, Qualifiers(),
ND && ND->isCXXInstanceMember());
....
}

Diagnostická zpráva PVS-Studio: V595 Ukazatel ND byl použit před ověřením proti nullptr.Zkontrolujte řádky: 2803, 2805. SemaTemplateInstantiate.cpp 2803

Tato chyba je podobná té předchozí. Je nebezpečné dereference ukazatele bez předchozí kontroly, když je jeho hodnota získávána pomocí dynamického přetypování. Následující kód navíc potvrzuje, že je taková kontrola nutná.

Úryvek 8, funkce, která běží i přes chybový stav

bool VerifyObject(llvm::yaml::Node &N,
std::map Expected) {
....
auto *V = llvm::dyn_cast_or_null(Prop.getValue());
if (!V) {
ADD_FAILURE() << KS << " is not a string";
Match = false;
}
std::string VS = V->getValue(Tmp).str();
....
}

Diagnostická zpráva PVS-Studio: V1004 Po ověření proti nullptr byl nebezpečně použit ukazatel V. Zkontrolujte řádky: 61, 65. TraceTests.cpp 65

Ukazatel V může být nulový ukazatel. Toto je zjevně chybový stav, který je dokonce hlášen chybovou zprávou. Ale funkce bude stále fungovat, jako by se nic nestalo, a skončí dereferencí toho velmi nulového ukazatele. Programátor pravděpodobně chtěl, aby se funkce v tomto bodě zastavila. V takovém případě by měla být opravena následovně:

auto *V = llvm::dyn_cast_or_null(Prop.getValue());
if (!V) {
ADD_FAILURE() << KS << " is not a string";
Match = false;
return false;
}
std::string VS = V->getValue(Tmp).str();

Snippet 9, a typo

const char *tools::SplitDebugName(const ArgList &Args, const InputInfo &Input,
const InputInfo &Output) {
if (Arg *A = Args.getLastArg(options::OPT_gsplit_dwarf_EQ))
if (StringRef(A->getValue()) == "single")
return Args.MakeArgString(Output.getFilename()); Arg *FinalOutput = Args.getLastArg(options::OPT_o);
if (FinalOutput && Args.hasArg(options::OPT_c)) {
SmallString<128> T(FinalOutput->getValue());
llvm::sys::path::replace_extension(T, "dwo");
return Args.MakeArgString(T);
} else {
// Use the compilation dir.
SmallString<128> T(
Args.getLastArgValue(options::OPT_fdebug_compilation_dir));
SmallString<128> F(llvm::sys::path::stem(Input.getBaseInput()));
llvm::sys::path::replace_extension(F, "dwo");
T += F;
return Args.MakeArgString(F); // <=
}
}

Diagnostická zpráva PVS-Studio: V1001 The T proměnná je přiřazena, ale na konci funkce se nepoužívá. CommonArgs.cpp 873

Podívejte se na poslední řádky funkce. Místní proměnná T se mění, ale nijak se nepoužívá. Musí to být překlep a funkce by měla pravděpodobně skončit následovně:

T += F;
return Args.MakeArgString(T);

Snippet 10, nula jako dělitel

typedef int32_t si_int;
typedef uint32_t su_int;typedef union {
du_int all;
struct {
#if _YUGA_LITTLE_ENDIAN
su_int low;
su_int high;
#else
su_int high;
su_int low;
#endif // _YUGA_LITTLE_ENDIAN
} s;
} udwords;COMPILER_RT_ABI du_int __udivmoddi4(du_int a, du_int b, du_int *rem) {
....
if (d.s.low == 0) {
if (d.s.high == 0) {
// K X
// ---
// 0 0
if (rem)
*rem = n.s.high % d.s.low;
return n.s.high / d.s.low;
}
....
}

diagnostické zprávy PVS-Studio:

  • V609 Mod by nula. Denominátor ‚d.s.low == 0. udivmoddi4.c 61
  • V609 Vydělte nulou. Denominator ‚d.s.low == 0. udivmoddi4.c 62

Nevím, jestli se jedná o chybu nebo nějakou záludnou chytrost, ale kód vypadá divně. Má dvě běžné celočíselné proměnné, z nichž jedna je rozdělena druhou. Zajímavou částí však je, že operace dělení probíhá, pouze pokud jsou obě proměnné nulové. Jakého úkolu má dosáhnout?

Snippet 11, copy-paste

bool MallocChecker::mayFreeAnyEscapedMemoryOrIsModeledExplicitly(....)
{
....
StringRef FName = II->getName();
....
if (FName == "postEvent" &&
FD->getQualifiedNameAsString() == "QCoreApplication::postEvent") {
return true;
} if (FName == "postEvent" &&
FD->getQualifiedNameAsString() == "QCoreApplication::postEvent") {
return true;
}
....
}

Diagnostická zpráva PVS-Studio: V581 Podmíněné výrazy výrazů if umístěných vedle sebe jsou totožné. Zkontrolujte řádky: 3108, 3113. MallocChecker.cpp 3113

Fragment kódu byl klonován, ale poté už nebyl upraven. Tento klon by měl být buď odstraněn, nebo upraven, aby bylo možné provést nějakou užitečnou kontrolu.

Závěr

Nezapomeňte, že můžete použít tuto bezplatnou licenci možnost pro kontrolu open-source projektů. Poskytujeme další způsoby, jak používat PVS-Studio také zdarma, některé dokonce umožňují analýzu patentovaného kódu. Úplný seznam možností naleznete zde: „ Způsoby získání bezplatné licence PVS-Studio “. Děkujeme za přečtení!

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *