Verificação do projeto LibrePCB usando o PVS-Studio dentro do contêiner Docker

Recipiente PVS-Studio e Docker

Este é um artigo clássico sobre como nossa equipe testou um projeto aberto do LibrePCB usando o analisador de código estático PVS-Studio. No entanto, o artigo é interessante, pois a verificação foi realizada dentro do contêiner do Docker. Se você usar contêineres, esperamos que o artigo demonstre outra maneira simples de integrar o analisador ao processo de desenvolvimento.

LibrePCB


O LibrePCB é um software gratuito para projetar circuitos eletrônicos e placas de circuito impresso. O código do programa é escrito em C ++ e o Qt5 é usado para criar a interface gráfica. Recentemente, ocorreu o primeiro lançamento oficial desse aplicativo, que marcou a estabilização de seu próprio formato de arquivo (* .lp, * .lplib). Pacotes binários preparados para Linux, macOS e Windows.

LibrePCB


O LibrePCB é um pequeno projeto que contém apenas cerca de 300.000 linhas de código não vazias em C e C ++. Ao mesmo tempo, 25% das linhas não vazias são comentários. A propósito, essa é uma grande porcentagem de comentários. Provavelmente, isso se deve ao fato de o projeto ter muitos arquivos pequenos, uma parte significativa da qual é ocupada por comentários de cabeçalho das informações do projeto e da licença. Código do site GitHub: LibrePCB .

O projeto nos pareceu interessante e decidimos dar uma olhada. Mas os resultados dos testes não eram mais tão interessantes. Sim, houve erros reais. Mas não havia nada de especial sobre o qual certamente devemos contar aos leitores de nossos artigos. Talvez não escrevamos um artigo e nos limitemos a enviar os erros aos desenvolvedores do projeto. No entanto, o ponto interessante foi que o projeto foi testado na imagem do Docker e, portanto, decidimos que ainda valia a pena escrever um artigo.

Docker


Docker - software para automatizar a implantação e o gerenciamento de aplicativos em um ambiente de virtualização no nível do sistema operacional. Ele permite que você "empacote" o aplicativo com todos os seus arredores e dependências em um contêiner. Embora essa tecnologia tenha cerca de cinco anos e muitas empresas implementem o Docker em sua infraestrutura de projeto, no mundo do código aberto isso não era muito perceptível até recentemente.

Nossa empresa trabalha em estreita colaboração com projetos de código aberto, verificando o código-fonte usando nosso próprio analisador estático PVS-Studio. Atualmente, mais de 300 projetos foram verificados. A coisa mais difícil nessa atividade sempre foi a compilação de projetos de outras pessoas, mas a introdução de contêineres do Docker simplificou bastante esse processo.

A primeira experiência de teste de um projeto de código aberto no Docker foi com o Azure Service Fabric . Lá, os desenvolvedores fizeram a instalação do diretório do arquivo de origem no contêiner e a integração do analisador foi limitada à edição de um dos scripts em execução no contêiner:

diff --git a/src/build.sh b/src/build.sh index 290c57d..2a286dc 100755 --- a/src/build.sh +++ b/src/build.sh @@ -193,6 +193,9 @@ BuildDir() cd ${ProjBinRoot}/build.${DirName} + pvs-studio-analyzer analyze --cfg /src/PVS-Studio.cfg \ + -o ./service-fabric-pvs.log -j4 + if [ "false" = ${SkipBuild} ]; then if (( $NumProc <= 0 )); then NumProc=$(($(getconf _NPROCESSORS_ONLN)+0)) 

A diferença entre o projeto LibrePCB é que eles imediatamente forneceram o Dockerfile para criar a imagem e o projeto nela. Acabou sendo ainda mais conveniente para nós. Aqui está a parte do arquivo Docker em que estamos interessados:

 FROM ubuntu:14.04 # install packages RUN DEBIAN_FRONTEND=noninteractive \ apt-get -q update \ && apt-get -qy upgrade \ && apt-get -qy install git g++ qt5-default qttools5-dev-tools qt5-doc \ qtcreator libglu1-mesa-dev dia \ && apt-get clean # checkout librepcb RUN git clone --recursive https://..../LibrePCB.git /opt/LibrePCB \ && cd /opt/LibrePCB .... # build and install librepcb RUN /opt/LibrePCB/dev/docker/make_librepcb.sh .... 

Não faremos a compilação e instalação do projeto durante a montagem da imagem. Assim, coletamos uma imagem na qual o autor do projeto garante a montagem bem-sucedida do projeto.

Após o início do contêiner, o analisador foi instalado e os seguintes comandos foram executados para criar e analisar o projeto:

 cd /opt/LibrePCB mkdir build && cd build qmake -r ../librepcb.pro pvs-studio-analyzer trace -- make -j2 pvs-studio-analyzer analyze -l /mnt/Share/PVS-Studio.lic -r /opt/LibrePCB \ -o /opt/LibrePCB/LibrePCB.log -v -j4 cp -R -L -a /opt/LibrePCB /mnt/Share 

A propósito, todas as ações foram executadas no Windows 10. É muito agradável que os desenvolvedores de todos os sistemas operacionais populares também estejam desenvolvendo nessa direção. Infelizmente, os contêineres com Windows não são tão convenientes. Especialmente devido à impossibilidade de também instalar software facilmente.

Erros encontrados


Agora, uma seção clássica contendo uma descrição dos erros que encontramos usando o analisador de código estático do PVS-Studio. A propósito, aproveitando esta oportunidade, quero lembrá-lo de que recentemente desenvolvemos um analisador no sentido de oferecer suporte à análise de código para sistemas embarcados. Aqui estão alguns artigos que alguns de nossos leitores podem ter pulado:

  1. O PVS-Studio inclui suporte para o GNU Arm Embedded Toolchain ;
  2. PVS-Studio: suporte para os padrões de codificação MISRA C e MISRA C ++ .

Erros de digitação


 SymbolPreviewGraphicsItem::SymbolPreviewGraphicsItem( const IF_GraphicsLayerProvider& layerProvider, const QStringList& localeOrder, const Symbol& symbol, const Component* cmp, const tl::optional<Uuid>& symbVarUuid, const tl::optional<Uuid>& symbVarItemUuid) noexcept { if (mComponent && symbVarUuid && symbVarItemUuid) .... if (mComponent && symbVarItemUuid && symbVarItemUuid) // <= .... } 

PVS-Studio Warning: V501 CWE-571 Existem sub-expressões idênticas 'symbVarItemUuid' à esquerda e à direita do operador '&&'. symbolpreviewgraphicsitem.cpp 74

Erro de digitação clássico : a variável symbVarItemUuid é verificada duas vezes seguidas. Há uma verificação semelhante acima e, olhando para ela, fica claro que a segunda variável a ser verificada deve ser symbVarUuid .

O seguinte trecho de código de erro de digitação:

 void Clipper::DoMaxima(TEdge *e) { .... if (e->OutIdx >= 0) { AddOutPt(e, e->Top); e->OutIdx = Unassigned; } DeleteFromAEL(e); if (eMaxPair->OutIdx >= 0) { AddOutPt(eMaxPair, e->Top); // <= eMaxPair->OutIdx = Unassigned; } DeleteFromAEL(eMaxPair); .... } 

Aviso do PVS-Studio: V778 CWE-682 Dois fragmentos de código semelhantes foram encontrados. Talvez seja um erro de digitação e a variável 'eMaxPair' deva ser usada em vez de 'e'. clipper.cpp 2999

Provavelmente, o código foi escrito usando Copiar e Colar. Devido a uma supervisão no segundo bloco de texto, eles esqueceram de substituir e-> Top por eMaxPair-> Top .

Cheques extras


 static int rndr_emphasis(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) { if (!content || !content->size) return 0; HOEDOWN_BUFPUTSL(ob, "<em>"); if (content) hoedown_buffer_put(ob, content->data, content->size); HOEDOWN_BUFPUTSL(ob, "</em>"); return 1; } 

PVS-Studio Warning: V547 CWE-571 A expressão 'content' é sempre verdadeira. html.c 162

Provavelmente ainda não é um erro, mas simplesmente um código redundante. Não há necessidade de verificar novamente o ponteiro de conteúdo . Se for zero, a função termina imediatamente seu trabalho.

Uma situação semelhante:

 void Clipper::DoMaxima(TEdge *e) { .... else if( e->OutIdx >= 0 && eMaxPair->OutIdx >= 0 ) { if (e->OutIdx >= 0) AddLocalMaxPoly(e, eMaxPair, e->Top); DeleteFromAEL(e); DeleteFromAEL(eMaxPair); } .... } 

Aviso do PVS-Studio: V547 CWE-571 A expressão 'e-> OutIdx> = 0' é sempre verdadeira. clipper.cpp 2983

Verificar novamente (e-> OutIdx> = 0) não faz sentido. No entanto, talvez isso seja um erro. Por exemplo, podemos assumir que a variável e-> Top deve ser verificada. No entanto, isso é apenas um palpite. Não estamos familiarizados com o código do projeto e não podemos distinguir erros do código redundante :).

E outro caso:

 QString SExpression::toString(int indent) const { .... if (child.isLineBreak() && nextChildIsLineBreak) { if (child.isLineBreak() && (i > 0) && mChildren.at(i - 1).isLineBreak()) { // too many line breaks ;) } else { str += '\n'; } } .... } 

PVS-Studio Warning: V571 CWE-571 Verificação recorrente. A condição 'child.isLineBreak ()' já foi verificada na linha 208. sexpression.cpp 209

Erro na lógica


 void FootprintPreviewGraphicsItem::paint(....) noexcept { .... for (const Circle& circle : mFootprint.getCircles()) { layer = mLayerProvider.getLayer(*circle.getLayerName()); if (!layer) continue; // <= if (layer) { // <= pen = QPen(....); painter->setPen(pen); } else painter->setPen(Qt::NoPen); .... } .... } 

PVS-Studio Warning: V547 CWE-571 A expressão 'layer' é sempre verdadeira. footprintpreviewgraphicsitem.cpp 177

Como a condição na segunda se a declaração é sempre verdadeira, o ramo else nunca é satisfeito.

Verificação esquecida do ponteiro


 extern int ZEXPORT unzGetGlobalComment ( unzFile file, char * szComment, uLong uSizeBuf) { .... if (uReadThis>0) { *szComment='\0'; if (ZREAD64(s->z_filefunc,s->filestream,szComment,uReadThis)!=uReadThis) return UNZ_ERRNO; } if ((szComment != NULL) && (uSizeBuf > s->gi.size_comment)) *(szComment+s->gi.size_comment)='\0'; .... } 

Aviso do PVS-Studio: V595 CWE-476 O ponteiro 'szComment' foi utilizado antes de ser verificado no nullptr. Verifique as linhas: 2068, 2073. unzip.c 2068

Se uReadThis> 0 , o ponteiro szComment é desreferenciado . Isso é perigoso, pois esse ponteiro pode ser nulo. O analisador conclui com base no fato de que ainda mais esse ponteiro é verificado quanto à igualdade NULL .

Membro da classe não inicializado


 template <class T> class Edge { public: using VertexType = Vector2<T>; Edge(const VertexType &p1, const VertexType &p2, T w=-1) : p1(p1), p2(p2), weight(w) {}; // <= Edge(const Edge &e) : p1(e.p1), p2(e.p2), weight(e.weight), isBad(false) {}; Edge() : p1(0,0), p2(0,0), weight(0), isBad(false) {} VertexType p1; VertexType p2; T weight=0; bool isBad; }; 

Aviso do PVS-Studio: V730 CWE-457 Nem todos os membros de uma classe são inicializados dentro do construtor. Considere inspecionar: isBad. edge.h 14

Todos os construtores, exceto o primeiro, inicializam o campo da classe isBad . Provavelmente, o primeiro construtor simplesmente esqueceu acidentalmente de fazer essa inicialização. Como resultado, o primeiro construtor cria um objeto incompletamente inicializado, o que pode levar ao comportamento indefinido do programa.

Existem mais 11 gatilhos de diagnóstico V730 . No entanto, como não estamos familiarizados com o código, é difícil dizer se esses avisos indicam problemas reais. Eu acho que esses avisos são melhor estudados pelos autores do projeto.

Vazamento de memória


 template <typename ElementType> void ProjectLibrary::loadElements(....) { .... ElementType* element = new ElementType(elementDir, false); // can throw if (elementList.contains(element->getUuid())) { throw RuntimeError( __FILE__, __LINE__, QString(tr("There are multiple library elements with the same " "UUID in the directory \"%1\"")) .arg(subdirPath.toNative())); } .... } 

Aviso do PVS-Studio: V773 CWE-401 A exceção foi lançada sem liberar o ponteiro 'elemento'. É possível um vazamento de memória. projectlibrary.cpp 245

Se um elemento já estiver na lista, uma exceção será lançada. Mas isso não destrói o objeto criado anteriormente, cujo ponteiro é armazenado na variável do elemento .

Tipo de exceção inválido


 bool CmdRemoveSelectedSchematicItems::performExecute() { .... throw new LogicError(__FILE__, __LINE__); .... } 

PVS-Studio Warning: V1022 CWE-755 Uma exceção foi lançada pelo ponteiro. Considere jogá-lo por valor. cmdremoveselectedschematicitems.cpp 143

O analisador detectou uma exceção lançada pelo ponteiro. Na maioria das vezes, é costume lançar exceções por valor e capturar por referência. Arremessar um ponteiro pode fazer com que a exceção não seja capturada, pois será capturada por referência. Além disso, o uso de um ponteiro força o interceptador a chamar o operador de exclusão para destruir o objeto criado, para que não ocorram vazamentos de memória.

Em geral, o novo operador é escrito aqui por acidente e deve ser excluído. Que isso é um erro é confirmado pelo fato de que em todos os outros lugares diz:

 throw LogicError(__FILE__, __LINE__); 

Uso perigoso de dynamic_cast


 void GraphicsView::handleMouseWheelEvent( QGraphicsSceneWheelEvent* event) noexcept { if (event->modifiers().testFlag(Qt::ShiftModifier)) .... } bool GraphicsView::eventFilter(QObject* obj, QEvent* event) { .... handleMouseWheelEvent(dynamic_cast<QGraphicsSceneWheelEvent*>(event)); .... } 

PVS-Studio Warning: V522 CWE-628 A desreferenciação do ponteiro nulo 'evento' pode ocorrer. O potencial ponteiro nulo é passado para a função 'handleMouseWheelEvent'. Inspecione o primeiro argumento. Verifique as linhas: 143, 252. graphicsview.cpp 143

O ponteiro resultante do operador dynamic_cast é passado para a função handleMouseWheelEvent . Nele, esse ponteiro é desreferenciado sem verificação prévia.

Isso é perigoso, pois o operador dynamic_cast pode retornar nullptr . Acontece que esse código não é melhor do que apenas usar o static_cast mais rápido.

Uma verificação explícita do ponteiro deve ser adicionada a esse código antes do uso.

Além disso, código desse tipo é muito comum:

 bool GraphicsView::eventFilter(QObject* obj, QEvent* event) { .... QGraphicsSceneMouseEvent* e = dynamic_cast<QGraphicsSceneMouseEvent*>(event); Q_ASSERT(e); if (e->button() == Qt::MiddleButton) .... } 

Aviso do PVS-Studio: V522 CWE-690 Pode haver desreferenciação de um ponteiro nulo potencial 'e'. graphicsview.cpp 206

O ponteiro é verificado usando a macro Q_ASSERT . Aqui está a descrição dele:
Imprime uma mensagem de aviso contendo o nome do arquivo de código-fonte e o número da linha, se o teste for falso.

Q_ASSERT () é útil para testar pré e pós-condições durante o desenvolvimento. Não faz nada se QT_NO_DEBUG foi definido durante a compilação.

Q_ASSERT é uma maneira ruim de verificar os ponteiros antes do uso. Como regra, na versão Release QT_NO_DEBUG não está definida. Não sei como estão as coisas no projeto LibrePCB. No entanto, se QT_NO_DEBUG estiver definido no Release, essa é uma solução estranha e não padrão.

Se a macro se expandir para o vazio, acontece que não há verificação. E então não está claro por que usar dynamic_cast . Por que não usar static_cast então ?

Em geral, esse código é inodoro e merece uma revisão de todos os fragmentos de código semelhantes. E há, a propósito, muitos deles. Contei 82 casos semelhantes!

Conclusão


Em geral, o projeto LibrePCB nos parecia de alta qualidade. No entanto, recomendamos que os autores do projeto se armam com a ferramenta PVS-Studio e conduzam independentemente uma Revisão de Código das seções de código indicadas pelo analisador. Estamos prontos para fornecer a eles uma licença gratuita por um mês para uma análise completa do projeto. Além disso, eles podem usar a opção de licenciamento do analisador gratuito, pois o código do projeto é aberto e publicado no site do GitHub. Escreveremos sobre esta opção de licenciamento em breve.



Se você deseja compartilhar este artigo com um público que fala inglês, use o link para a tradução: Andrey Karpov, Svyatoslav Razmyslov. Verificando o LibrePCB com o PVS-Studio dentro de um contêiner Docker .

Source: https://habr.com/ru/post/pt433562/


All Articles