Verificação do Clang 11 com PVS-Studio

(28 de outubro de 2020 )

De vez em quando, temos que escrever artigos sobre como verificamos outra versão nova de algum compilador. Isso não é muito divertido. No entanto, como mostra a prática, se pararmos de fazer isso por um tempo, as pessoas começarão a duvidar se o PVS-Studio vale o título de bom coletor de bugs e vulnerabilidades. E se o novo compilador também puder fazer isso? Claro, os compiladores evoluem, mas o mesmo acontece com o PVS-Studio – e ele prova, repetidamente, sua capacidade de detectar bugs mesmo em projetos de alta qualidade, como compiladores.

É hora de verificar novamente o Clang

Para falar a verdade, escrevi este artigo com base na postagem anterior “ Verificando o compilador GCC 10 com PVS-Studio ”. Portanto, se alguns parágrafos parecem familiares, é porque você já os leu antes :).

Não é nenhum segredo que os compiladores empregam seus próprios analisadores de código estático integrados, e eles também estão em desenvolvimento. É por isso que escrevemos artigos de vez em quando para mostrar que nosso analisador estático, PVS-Studio, pode encontrar bugs até mesmo dentro de compiladores e que valemos a pena :).

Na verdade, você não pode compare analisadores estáticos clássicos com compiladores. Os analisadores estáticos não apenas detectam bugs no código-fonte, mas também envolvem uma infraestrutura altamente desenvolvida. Por um lado, ele inclui integração com sistemas como SonarQube, PlatformIO, Azure DevOps, Travis CI, CircleCI, GitLab CI / CD, Jenkins e Visual Studio. Inclui mecanismos para supressão em massa de avisos, o que permite que você comece a usar o PVS-Studio imediatamente, mesmo em um grande projeto. Inclui o envio de notificações por e-mail. E assim por diante. Mas a primeira pergunta que os desenvolvedores ainda farão é: “Seu PVS-Studio pode encontrar algo que os compiladores não podem?” E isso significa que estamos condenados a escrever artigos sobre como verificamos os próprios compiladores continuamente.

Vamos voltar ao Clang. Não há necessidade de insistir no assunto e dizer do que se trata o projeto. Na verdade, verificamos não apenas o código do Clang 11 em si, mas também o código da biblioteca LLVM 11 na qual ele é baseado. Do ponto de vista deste artigo, não importa se um defeito foi encontrado no código do compilador ou da biblioteca.

Achei o código do Clang / LLVM muito mais claro do que o do GCC. Pelo menos não está repleto de todas aquelas macros terríveis e emprega extensivamente os recursos modernos do C ++.

Mesmo assim, o projeto ainda é grande o suficiente para tornar o exame do relatório de análise tedioso sem personalização prévia. O que mais atrapalha são os positivos “semi-falsos”. Por positivos “semi-falsos”, quero dizer casos em que o analisador é tecnicamente correto para apontar certos problemas, mas esses avisos não têm uso prático. Por exemplo, muitos desses avisos referem-se a testes de unidade e código gerado.

Aqui está um exemplo de testes de unidade:

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);

O o analisador nos avisa que as variáveis ​​são atribuídas aos mesmos valores que já possuem:

  • V1048 A variável Spaces.SpacesInParentheses foi atribuída com o mesmo valor. FormatTest.cpp 11554
  • V1048 A variável ‘Spaces.SpacesInCStyleCastParentheses’ recebeu o mesmo valor. FormatTest.cpp 11556

Tecnicamente, esse aviso é direto e o snippet precisa ser simplificado ou corrigido. Mas também está claro que este código está bem como está e não há motivo para consertar nada nele.

Aqui está outro exemplo: o analisador emite uma tonelada de avisos no arquivo gerado automaticamente Options.inc. Observe a “parede” do código que contém:

Este volume de código dispara uma enxurrada de avisos:

  • V501 Existem subexpressões idênticas à esquerda e à direita do operador ==: nullptr == nullptr Options.inc 26
  • V501 Existem subexpressões idênticas à esquerda e à direita do operador ==: nullptr == nullptr Options.inc 27
  • V501 There são subexpressões idênticas à esquerda e à direita do operador ==: nullptr == nullptr Options.inc 28
  • e assim por diante – um aviso por linha…

No entanto, tudo isso não é grande coisa. Ele pode ser resolvido excluindo arquivos irrelevantes da análise, marcando certas macros e funções, suprimindo certos tipos de diagnóstico e assim por diante. Sim, pode, mas não é um trabalho muito interessante de se fazer quando você escreve um artigo. É por isso que fiz a mesma coisa que no artigo sobre a verificação do compilador GCC: Continuei lendo o relatório até coletar 11 exemplos interessantes para incluir no artigo. Por que 11? Eu apenas pensei que por ser a 11ª versão do Clang, eu precisava de 11 exemplos :).

11 snippets de código suspeitos

Snippet 1, operação do módulo em 1

Este é legal! Gosto de bugs como esse!

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);
}
....
}

Mensagem de diagnóstico do PVS-Studio: V1063 A operação módulo por 1 não tem sentido. O resultado sempre será zero. llvm-stress.cpp 631

O programador está usando uma operação de módulo para obter um valor aleatório de 0 ou 1. Mas o valor 1 parece confundir os desenvolvedores e faça com que escrevam o anti-padrão clássico no qual a operação do módulo é realizada em 1 em vez de 2. A operação X% 1 não tem sentido, pois sempre avalia como 0 . Esta é a versão corrigida:

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

O diagnóstico V1063 adicionado recentemente é terrivelmente simples, mas, como você pode ver, funciona perfeitamente.

Sabemos que os desenvolvedores de compiladores ficam de olho em nosso trabalho e pegam nossas ideias emprestadas. Isso é totalmente bom. É bom saber que PVS-Studio é a força motriz por trás do progresso . Vamos ver quanto será necessário para um diagnóstico semelhante aparecer no Clang e no GCC :).

Snippet 2, um erro de digitação em uma condição

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;
}

Mensagem de diagnóstico do PVS-Studio: V501 Existem subexpressões idênticas à esquerda e à direita do Operador & &:! T1.isNull () & &! T1.isNull () SemaOverload.cpp 9493

A verificação ! T1.isNull () é executada duas vezes. Isso é obviamente um erro de digitação; a segunda parte da condição deve verificar a variável T2 .

Snippet 3, potencial array-index-out-of- bounds

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();
....
}

Mensagem de diagnóstico do PVS-Studio: V557 Array overrun é possível. O índice ‘Índice’ aponta além do limite da matriz. ASTReader.cpp 7318

Suponha que a matriz armazene um elemento e o valor da variável Índice também seja 1. Então a condição (1> 1) é falso e, portanto, a matriz será indexada além de seus limites. Esta é a verificação correta:

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

Snippet 4, ordem de avaliação do argumento

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

Mensagem de diagnóstico do PVS-Studio: V567 Comportamento não especificado. A ordem de avaliação do argumento não é definida para a função ‘addSection’. Considere inspecionar a variável SecNo. Object.cpp 1223

Observe que o argumento SecNo é usado duas vezes, sendo incrementado enquanto isso. O problema é que você não pode dizer em que ordem exata os argumentos serão avaliados. O resultado irá, portanto, variar dependendo da versão do compilador ou dos parâmetros de compilação.

Aqui está um exemplo sintético para ilustrar este ponto:

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

Dependendo do compilador, este código pode produzir “1, 2” ou “2, 1”. Eu o executei no Compiler Explorer e obtive as seguintes saídas:

  • quando compilado com Clang 11.0.0, o programa saídas 1 , 1.
  • quando compilado com GCC 10.2, o programa output s 2, 1.

Curiosamente, este caso simples faz com que o Clang emita um aviso:

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

Por alguma razão, porém, esse aviso não foi emitido no código real. Ou ele está desativado por não ser muito prático ou esse caso é muito complicado para o compilador lidar.

Fragmento 5, uma verificação duplicada estranha

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;
}
....
}

Mensagem de diagnóstico do PVS-Studio: V571 Verificação recorrente. A condição ‘if (! NameOrErr)’ já foi verificada na linha 4666. ELFDumper.cpp 4667

A segunda verificação é um clone da primeira e é, portanto, redundante. Talvez pudesse ser removido com segurança. Mas o mais provável é que ele contém um erro de digitação e foi feito para verificar alguma outra variável.

Snippet 6, possível desreferência de ponteiro nulo

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);
....
}

Mensagem de diagnóstico do PVS-Studio: V595 O ponteiro CDecl foi utilizado antes de ser verificado em relação a nullptr. Linhas de verificação: 5275, 5284. RewriteObjC.cpp 5275

Ao realizar a primeira verificação, o desenvolvedor nunca hesita em cancelar a referência do ponteiro CDecl :

if (CDecl->isImplicitInterfaceDecl())

Mas se você olhar o código algumas linhas adiante, ficará claro que o ponteiro pode ser nulo:

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

A primeira verificação provavelmente deveria ter a seguinte aparência:

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

Snippet 7, possível desreferência de ponteiro nulo

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());
....
}

Mensagem de diagnóstico do PVS-Studio: V595 O ponteiro ND foi utilizado antes de ser verificado nullptr.Verifique as linhas: 2803, 2805. SemaTemplateInstantiate.cpp 2803

Este erro é semelhante ao anterior. É perigoso cancelar a referência de um ponteiro sem uma verificação prévia quando seu valor é adquirido usando um elenco de tipo dinâmico. Mais do que isso, o código subsequente confirma que essa verificação é necessária.

Snippet 8, uma função que continua em execução apesar de um estado de erro

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();
....
}

Mensagem de diagnóstico do PVS-Studio: V1004 O ponteiro V foi usado sem segurança após ter sido verificado em relação ao nullptr. Verifique as linhas: 61, 65. TraceTests.cpp 65

O ponteiro V pode ser um ponteiro nulo. Este é obviamente um estado de erro, que é relatado até mesmo com uma mensagem de erro. Mas a função continuará rodando como se nada tivesse acontecido e acabará desreferenciando aquele ponteiro nulo. O programador provavelmente queria que a função parasse neste ponto, caso em que deveria ser corrigida da seguinte forma:

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, um erro de digitação

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); // <=
}
}

Mensagem de diagnóstico do PVS-Studio: V1001 The T variável é atribuída, mas não é usada no final da função. CommonArgs.cpp 873

Observe as últimas linhas da função. A variável local T muda, mas não é usada de forma alguma. Deve ser um erro de digitação e a função provavelmente deve terminar da seguinte maneira:

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

Snippet 10, zero as o divisor

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;
}
....
}

Mensagens de diagnóstico do PVS-Studio:

  • V609 Mod por zero. Denominador ‘d.s.low’ == 0. udivmoddi4.c 61
  • V609 Divida por zero. Denominador ‘d.s.low’ == 0. udivmoddi4.c 62

Não sei se isso é um bug ou alguma engenhoca complicada, mas o código parece estranho. Possui duas variáveis ​​inteiras regulares, uma das quais é dividida pela outra. Mas a parte interessante é que a operação de divisão ocorre apenas se ambas as variáveis ​​forem zeros. Que tarefa ele deve realizar?

Snippet 11, copiar e colar

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;
}
....
}

Mensagem de diagnóstico do PVS-Studio: V581 As expressões condicionais das instruções if situadas lado a lado são idênticas. Verifique as linhas: 3108, 3113. MallocChecker.cpp 3113

Um fragmento de código foi clonado, mas nunca modificado depois. Este clone deve ser removido ou modificado para realizar alguma verificação útil.

Conclusão

Lembre-se de que você pode usar esta licença gratuita opção para verificar projetos de código aberto. Fornecemos outras formas de usar o PVS-Studio gratuitamente, algumas delas até permitindo a análise de código proprietário. Veja a lista completa de opções aqui: “ Maneiras de obter uma licença PVS-Studio grátis ”. Obrigado por ler!

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *