PVS-Studio: Análise de solicitações pull no Azure DevOps usando agentes auto-hospedados





A análise de código estático é mais eficaz ao fazer alterações em um projeto, uma vez que os bugs são sempre mais difíceis de corrigir no futuro do que evitar que ocorram nos estágios iniciais. Continuamos a expandir as opções de uso do PVS-Studio em sistemas de desenvolvimento contínuo e mostramos como configurar a análise de solicitações pull usando agentes auto-hospedados no Microsoft Azure DevOps, usando o exemplo do jogo Minetest.



Resumidamente sobre o que estamos lidando



Minetest é um mecanismo de jogo de plataforma cruzada de código aberto contendo cerca de 200.000 linhas de código C, C ++ e Lua. Ele permite que você crie diferentes modos de jogo no espaço voxel. Suporta multijogador e muitos mods de comunidade. O repositório do projeto está hospedado aqui: https://github.com/minetest/minetest .



As seguintes ferramentas foram usadas para configurar uma pesquisa de erro regular:



PVS-Studio é um analisador de código estático em C, C ++, C # e Java para encontrar erros e defeitos de segurança.



O Azure DevOps é uma plataforma baseada em nuvem que oferece a capacidade de desenvolver, executar aplicativos e armazenar dados em servidores remotos.



Você pode usar máquinas virtuais Windows e Linux para executar tarefas de desenvolvimento no Azure. No entanto, a execução de agentes em hardware local tem várias vantagens significativas:



  • Localhost pode ter mais recursos do que a VM do Azure;
  • O agente não "desaparece" após completar sua tarefa;
  • A capacidade de personalizar diretamente o ambiente e controle mais flexível sobre os processos de construção;
  • Armazenar arquivos intermediários localmente tem um efeito positivo na velocidade de construção;
  • Você pode completar mais de 30 tarefas por mês gratuitamente.


Preparando-se para usar um agente auto-hospedado



O processo de introdução ao Azure é descrito em detalhes no artigo " PVS-Studio vai para as nuvens: Azure DevOps ", portanto, irei direto para a criação de um agente auto-hospedado.



Para que os agentes tenham o direito de se conectar a pools de projetos, eles precisam de um token de acesso especial. Você pode obtê-lo na página "Tokens de acesso pessoais", no menu "Configurações do usuário".



image2.png


Após clicar em "Novo token", você precisa especificar um nome e selecionar Ler e gerenciar grupos de agentes (pode ser necessário expandir a lista completa em "Mostrar todos os escopos").



image3.png


Você precisa copiar o token, pois o Azure não o mostrará mais e você terá que fazer um novo.



image4.png


Um contêiner Docker baseado no Windows Server Core será usado como um agente. O host é meu computador de trabalho no Windows 10 x64 com Hyper-V.



Primeiro, você precisa expandir a quantidade de espaço em disco disponível para os contêineres do Docker.



No Windows, para isso, você precisa modificar o arquivo 'C: \ ProgramData \ Docker \ config \ daemon.json' da seguinte forma:



{
  "registry-mirrors": [],
  "insecure-registries": [],
  "debug": true,
  "experimental": false,
  "data-root": "d:\\docker",
  "storage-opts": [ "size=40G" ]
}


Para criar uma imagem Docker para agentes com um sistema de compilação e tudo que você precisa no diretório 'D: \ docker-agent', adicione um arquivo Docker com o seguinte conteúdo:



# escape=`

FROM mcr.microsoft.com/dotnet/framework/runtime

SHELL ["cmd", "/S", "/C"]

ADD https://aka.ms/vs/16/release/vs_buildtools.exe C:\vs_buildtools.exe
RUN C:\vs_buildtools.exe --quiet --wait --norestart --nocache `
  --installPath C:\BuildTools `
  --add Microsoft.VisualStudio.Workload.VCTools `
  --includeRecommended

RUN powershell.exe -Command `
  Set-ExecutionPolicy Bypass -Scope Process -Force; `
  [System.Net.ServicePointManager]::SecurityProtocol =
    [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; `
  iex ((New-Object System.Net.WebClient)
    .DownloadString('https://chocolatey.org/install.ps1')); `
  choco feature enable -n=useRememberedArgumentsForUpgrades;
  
RUN powershell.exe -Command `
  choco install -y cmake --installargs '"ADD_CMAKE_TO_PATH=System"'; `
  choco install -y git --params '"/GitOnlyOnPath /NoShellIntegration"'

RUN powershell.exe -Command `
  git clone https://github.com/microsoft/vcpkg.git; `
  .\vcpkg\bootstrap-vcpkg -disableMetrics; `
  $env:Path += '";C:\vcpkg"'; `
  [Environment]::SetEnvironmentVariable(
    '"Path"', $env:Path, [System.EnvironmentVariableTarget]::Machine); `
  [Environment]::SetEnvironmentVariable(
    '"VCPKG_DEFAULT_TRIPLET"', '"x64-windows"',
  [System.EnvironmentVariableTarget]::Machine)

RUN powershell.exe -Command `
  choco install -y pvs-studio; `
  $env:Path += '";C:\Program Files (x86)\PVS-Studio"'; `
  [Environment]::SetEnvironmentVariable(
    '"Path"', $env:Path, [System.EnvironmentVariableTarget]::Machine)

RUN powershell.exe -Command `
  $latest_agent =
    Invoke-RestMethod -Uri "https://api.github.com/repos/Microsoft/
                          azure-pipelines-agent/releases/latest"; `
  $latest_agent_version =
    $latest_agent.name.Substring(1, $latest_agent.tag_name.Length-1); `
  $latest_agent_url =
    '"https://vstsagentpackage.azureedge.net/agent/"' + $latest_agent_version +
  '"/vsts-agent-win-x64-"' + $latest_agent_version + '".zip"'; `
  Invoke-WebRequest -Uri $latest_agent_url -Method Get -OutFile ./agent.zip; `
  Expand-Archive -Path ./agent.zip -DestinationPath ./agent

USER ContainerAdministrator
RUN reg add hklm\system\currentcontrolset\services\cexecsvc
        /v ProcessShutdownTimeoutSeconds /t REG_DWORD /d 60  
RUN reg add hklm\system\currentcontrolset\control
        /v WaitToKillServiceTimeout /t REG_SZ /d 60000 /f

ADD .\entrypoint.ps1 C:\entrypoint.ps1
SHELL ["powershell", "-Command",
       "$ErrorActionPreference = 'Stop';
     $ProgressPreference = 'SilentlyContinue';"]
ENTRYPOINT .\entrypoint.ps1


O resultado será um sistema de compilação baseado em MSBuild para C ++, com Chocolatey para instalar PVS-Studio, CMake e Git. Para o gerenciamento conveniente das bibliotecas das quais o projeto depende, o Vcpkg é construído. E também a versão mais recente do Azure Pipelines Agent é baixada.



Para inicializar o agente a partir do arquivo ENTRYPOINT Docker, o script PowerShell 'entrypoint.ps1' é chamado, ao qual você precisa adicionar o URL da "organização" do projeto, token de pool do agente e parâmetros de licença PVS-Studio:



$organization_url = "https://dev.azure.com/< Microsoft Azure>"
$agents_token = "<token >"

$pvs_studio_user = "<  PVS-Studio>"
$pvs_studio_key = "< PVS-Studio>"

try
{
  C:\BuildTools\VC\Auxiliary\Build\vcvars64.bat

  PVS-Studio_Cmd credentials -u $pvs_studio_user -n $pvs_studio_key
  
  .\agent\config.cmd --unattended `
    --url $organization_url `
    --auth PAT `
    --token $agents_token `
    --replace;
  .\agent\run.cmd
} 
finally
{
  # Agent graceful shutdown
  # https://github.com/moby/moby/issues/25982
  
  .\agent\config.cmd remove --unattended `
    --auth PAT `
    --token $agents_token
}


Comandos para construir a imagem e iniciar o agente:



docker build -t azure-agent -m 4GB .
docker run -id --name my-agent -m 4GB --cpu-count 4 azure-agent


image5.png


O agente está em execução e pronto para executar tarefas.



image6.png


Execução de análise em um agente auto-hospedado



Para análise de RP, um novo pipeline é criado com o seguinte script:



image7.png


trigger: none

pr:
  branches:
    include:
    - '*'

pool: Default

steps:
- script: git diff --name-only
    origin/%SYSTEM_PULLREQUEST_TARGETBRANCH% >
    diff-files.txt
  displayName: 'Get committed files'

- script: |
    cd C:\vcpkg
    git pull --rebase origin
    CMD /C ".\bootstrap-vcpkg -disableMetrics"
    vcpkg install ^
    irrlicht zlib curl[winssl] openal-soft libvorbis ^
    libogg sqlite3 freetype luajit
    vcpkg upgrade --no-dry-run
  displayName: 'Manage dependencies (Vcpkg)'

- task: CMake@1
  inputs:
    cmakeArgs: -A x64
      -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake
      -DCMAKE_BUILD_TYPE=Release -DENABLE_GETTEXT=0 -DENABLE_CURSES=0 ..
  displayName: 'Run CMake'

- task: MSBuild@1
  inputs:
    solution: '**/*.sln'
    msbuildArchitecture: 'x64'
    platform: 'x64'
    configuration: 'Release'
    maximumCpuCount: true
  displayName: 'Build'

- script: |
    IF EXIST .\PVSTestResults RMDIR /Q/S .\PVSTestResults
    md .\PVSTestResults
    PVS-Studio_Cmd ^
    -t .\build\minetest.sln ^
    -S minetest ^
    -o .\PVSTestResults\minetest.plog ^
    -c Release ^
    -p x64 ^
    -f diff-files.txt ^
    -D C:\caches
    PlogConverter ^
    -t FullHtml ^
    -o .\PVSTestResults\ ^
    -a GA:1,2,3;64:1,2,3;OP:1,2,3 ^
    .\PVSTestResults\minetest.plog
    IF NOT EXIST "$(Build.ArtifactStagingDirectory)" ^
    MKDIR "$(Build.ArtifactStagingDirectory)"
    powershell -Command ^
    "Compress-Archive -Force ^
    '.\PVSTestResults\fullhtml' ^
    '$(Build.ArtifactStagingDirectory)\fullhtml.zip'"
  displayName: 'PVS-Studio analyze'
  continueOnError: true

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)'
    ArtifactName: 'psv-studio-analisys'
    publishLocation: 'Container'
  displayName: 'Publish analysis report'


Este script será executado quando o PR for recebido e será executado nos agentes atribuídos ao pool padrão. Você só precisa dar permissão a ele para trabalhar com esta piscina.



image8.png




image9.png


O script salva a lista de arquivos alterados obtidos usando git diff. Em seguida, as dependências são atualizadas, a solução do projeto é gerada por meio do CMake e é construída.



Se a construção foi bem sucedida, a análise dos arquivos alterados é iniciada (flag '-f diff-files.txt'), ignorando os projetos auxiliares criados pelo CMake (selecione apenas o projeto requerido com o flag '-S minetest'). Para acelerar a busca por links entre o cabeçalho e os arquivos fonte C ++, um cache especial é criado, que será armazenado em um diretório separado (flag '-DC: \ caches').



Assim, podemos agora receber relatórios de análise de mudanças no projeto.



image10.png




image11.png


Como foi dito no início do artigo, um bônus agradável de usar agentes auto-hospedados é uma notável aceleração da execução de tarefas devido ao armazenamento local de arquivos intermediários.



image13.png


Alguns bugs encontrados no Minetest



Resultado de substituição



V519 A variável 'color_name' recebe valores duas vezes sucessivamente. Talvez isso seja um erro. Verifique as linhas: 621, 627. string.cpp 627



static bool parseNamedColorString(const std::string &value,
                                  video::SColor &color)
{
  std::string color_name;
  std::string alpha_string;

  size_t alpha_pos = value.find('#');
  if (alpha_pos != std::string::npos) {
    color_name = value.substr(0, alpha_pos);
    alpha_string = value.substr(alpha_pos + 1);
  } else {
    color_name = value;
  }

  color_name = lowercase(value); // <=

  std::map<const std::string, unsigned>::const_iterator it;
  it = named_colors.colors.find(color_name);
  if (it == named_colors.colors.end())
    return false;
  ....
}


Esta função deve analisar um nome de cor com um parâmetro de transparência (por exemplo, Verde # 77 ) e retornar seu código. Dependendo do resultado da verificação da condição, o resultado da divisão da linha ou uma cópia do argumento da função é passado para a variável color_name . No entanto, não a própria string resultante é convertida em minúsculas, mas o argumento original. Como resultado, ele não pode ser encontrado no dicionário de cores se o parâmetro de transparência estiver presente. Podemos consertar essa linha assim:



color_name = lowercase(color_name);


Verificações de condição



desnecessárias A expressão V547 'mais próximo_emergefull_d == -1' é sempre verdadeira. clientiface.cpp 363



void RemoteClient::GetNextBlocks (....)
{
  ....
  s32 nearest_emergefull_d = -1;
  ....
  s16 d;
  for (d = d_start; d <= d_max; d++) {
    ....
      if (block == NULL || surely_not_found_on_disk || block_is_invalid) {
        if (emerge->enqueueBlockEmerge(peer_id, p, generate)) {
          if (nearest_emerged_d == -1)
            nearest_emerged_d = d;
        } else {
          if (nearest_emergefull_d == -1) // <=
            nearest_emergefull_d = d;
          goto queue_full_break;
        }
  ....
  }
  ....
queue_full_break:
  if (nearest_emerged_d != -1) { // <=
    new_nearest_unsent_d = nearest_emerged_d;
  } else ....
}


A variável mais próxima_emergefull_d não muda durante a operação de loop, e sua verificação não afeta a execução do algoritmo. Ou isso é o resultado de copiar e colar impreciso ou eles se esqueceram de fazer alguns cálculos com ele.



V560 Uma parte da expressão condicional é sempre falsa: y> max_spawn_y. mapgen_v7.cpp 262



int MapgenV7::getSpawnLevelAtPoint(v2s16 p)
{
  ....
  while (iters > 0 && y <= max_spawn_y) {               // <=
    if (!getMountainTerrainAtPoint(p.X, y + 1, p.Y)) {
      if (y <= water_level || y > max_spawn_y)          // <=
        return MAX_MAP_GENERATION_LIMIT; // Unsuitable spawn point

      // y + 1 due to biome 'dust'
      return y + 1;
    }
  ....
}


O valor da variável ' y ' é verificado antes da próxima iteração do loop. A comparação subsequente, oposta, sempre retornará falso e, em geral, não afeta o resultado do teste de condição.



Perda de verificação de ponteiro



V595 do ponteiro 'm_client' foi utilizado antes de ser verificado em relação a nullptr. Verifique as linhas: 183, 187. game.cpp 183



void gotText(const StringMap &fields)
{
  ....
  if (m_formname == "MT_DEATH_SCREEN") {
    assert(m_client != 0);
    m_client->sendRespawn();
    return;
  }

  if (m_client && m_client->modsLoaded())
    m_client->getScript()->on_formspec_input(m_formname, fields);
}


Antes de acessar o ponteiro m_client , é verificado se ele não é nulo usando a macro assert . Mas isso só se aplica à compilação de depuração. Essa precaução, ao construir na liberação, é substituída por um manequim, e há o risco de desreferenciar um ponteiro nulo.



Um pouco ou não?



V616 A constante nomeada '(FT_RENDER_MODE_NORMAL)' com o valor 0 é usada na operação bit a bit. CGUITTFont.h 360



typedef enum  FT_Render_Mode_
{
  FT_RENDER_MODE_NORMAL = 0,
  FT_RENDER_MODE_LIGHT,
  FT_RENDER_MODE_MONO,
  FT_RENDER_MODE_LCD,
  FT_RENDER_MODE_LCD_V,

  FT_RENDER_MODE_MAX
} FT_Render_Mode;

#define FT_LOAD_TARGET_( x )   ( (FT_Int32)( (x) & 15 ) << 16 )
#define FT_LOAD_TARGET_NORMAL  FT_LOAD_TARGET_( FT_RENDER_MODE_NORMAL )

void update_load_flags()
{
  // Set up our loading flags.
  load_flags = FT_LOAD_DEFAULT | FT_LOAD_RENDER;
  if (!useHinting()) load_flags |= FT_LOAD_NO_HINTING;
  if (!useAutoHinting()) load_flags |= FT_LOAD_NO_AUTOHINT;
  if (useMonochrome()) load_flags |= 
    FT_LOAD_MONOCHROME | FT_LOAD_TARGET_MONO | FT_RENDER_MODE_MONO;
  else load_flags |= FT_LOAD_TARGET_NORMAL; // <=
}


A macro FT_LOAD_TARGET_NORMAL se expande para zero, e o bit a bit "ou" não definirá nenhum sinalizador em load_flags , o outro branch pode ser removido.



Divisão de número inteiro arredondado



V636 A expressão 'rect.getHeight () / 16' foi convertida implicitamente do tipo 'int' para o tipo 'float'. Considere a utilização de um tipo explícito de fundição para evitar a perda de uma parte fracionária. Um exemplo: double A = (double) (X) / Y;. hud.cpp 771



void drawItemStack(....)
{
  float barheight = rect.getHeight() / 16;
  float barpad_x = rect.getWidth() / 16;
  float barpad_y = rect.getHeight() / 16;

  core::rect<s32> progressrect(
    rect.UpperLeftCorner.X + barpad_x,
    rect.LowerRightCorner.Y - barpad_y - barheight,
    rect.LowerRightCorner.X - barpad_x,
    rect.LowerRightCorner.Y - barpad_y);
}


Getters rect retorna um valor inteiro. O resultado da divisão de inteiros é gravado em uma variável de ponto flutuante e a parte fracionária é perdida. Parece que há tipos de dados incompatíveis nesses cálculos. Instruções de



ramificação de sequência suspeita



V646 Considere inspecionar a lógica do aplicativo. É possível que a palavra-chave 'else' esteja faltando. treegen.cpp 413



treegen::error make_ltree(...., TreeDef tree_definition)
{
  ....
  std::stack <core::matrix4> stack_orientation;
  ....
    if ((stack_orientation.empty() &&
      tree_definition.trunk_type == "double") ||
      (!stack_orientation.empty() &&
      tree_definition.trunk_type == "double" &&
      !tree_definition.thin_branches)) {
      ....
    } else if ((stack_orientation.empty() &&
      tree_definition.trunk_type == "crossed") ||
      (!stack_orientation.empty() &&
      tree_definition.trunk_type == "crossed" &&
      !tree_definition.thin_branches)) {
      ....
    } if (!stack_orientation.empty()) {                  // <=
  ....
  }
  ....
}


Aqui estão as sequências else-if no algoritmo de geração de árvore. No meio, o próximo bloco if está na mesma linha com o parêntese de fechamento do else anterior . Talvez o código funcione corretamente: antes disso, se -a, são criados os blocos de troncos e depois as folhas; ou talvez eles tenham perdido o outro . Certamente isso só pode ser dito pelo desenvolvedor.



Verificação de alocação de memória incorreta



V668 Não faz sentido testar o ponteiro de 'nuvens' em relação a nulo, já que a memória foi alocada usando o operador 'novo'. A exceção será gerada no caso de erro de alocação de memória. game.cpp 1367



bool Game::createClient(....)
{
  if (m_cache_enable_clouds) {
    clouds = new Clouds(smgr, -1, time(0));
    if (!clouds) {
      *error_message = "Memory allocation error (clouds)";
      errorstream << *error_message << std::endl;
      return false;
    }
  }
}


No caso de new falhar ao criar um objeto, uma exceção std :: bad_alloc será lançada e deve ser tratada por um bloco try-catch . E verificar neste formulário é inútil.



Leitura de uma matriz fora dos



limites V781 O valor do índice 'i' é verificado após ser usado. Talvez haja um erro na lógica do programa. irrString.h 572



bool equalsn(const string<T,TAlloc>& other, u32 n) const
{
  u32 i;
  for(i=0; array[i] && other[i] && i < n; ++i) // <=
    if (array[i] != other[i])
      return false;

  // if one (or both) of the strings was smaller then they
  // are only equal if they have the same length
  return (i == n) || (used == other.used);
}


Os elementos da matriz são acessados ​​antes de verificar o índice, o que pode levar a um erro. Pode valer a pena reescrever o loop assim:



for (i=0; i < n; ++i) // <=
  if (!array[i] || !other[i] || array[i] != other[i])
    return false;


Outros erros



Este artigo é sobre a análise de solicitações pull no Azure DevOps e não se destina a fornecer uma visão geral detalhada dos erros no projeto Minetest. Aqui estão apenas alguns trechos de código que achei interessantes. Sugerimos que os autores do projeto não sigam este artigo para corrigir os erros e realizar uma análise mais aprofundada dos avisos que o PVS-Studio irá emitir.



Conclusão



Graças à configuração flexível no modo de linha de comando, a análise do PVS-Studio pode ser incorporada em uma ampla variedade de cenários de CI / CD. E fazer o uso correto dos recursos disponíveis compensa no aumento da produtividade.



Deve-se observar que o modo de verificação de solicitação pull está disponível apenas na edição Enterprise do analisador. Para obter uma licença Enterprise de demonstração, indique-a nos comentários ao solicitar uma licença na página de download . Mais detalhes sobre a diferença entre as licenças podem ser encontrados na página de pedido do PVS-Studio .





Se você deseja compartilhar este artigo com um público que fala inglês, por favor, use o link de tradução: Alexey Govorov. PVS-Studio: analisando solicitações pull no Azure DevOps usando agentes auto-hospedados .



All Articles