PVS-Studio nas nuvens: Azure DevOps

Quadro 9

Este é um segundo artigo, que se concentra no uso do analisador PVS-Studio em sistemas de CI na nuvem. Desta vez, consideraremos a plataforma Azure DevOps - uma solução de CI \ CD na nuvem da Microsoft. Analisaremos o projeto ShareX.

Vamos precisar de três componentes. O primeiro é o analisador PVS-Studio. O segundo é o Azure DevOps, com o qual integraremos o analisador. O terceiro é o projeto que verificaremos para demonstrar as habilidades do PVS-Studio ao trabalhar em uma nuvem. Então vamos em frente.

O PVS-Studio é um analisador de código estático para encontrar erros e defeitos de segurança. A ferramenta suporta a análise de código C, C ++ e C #.

DevOps do Azure . A plataforma Azure DevOps inclui ferramentas como o Pipeline do Azure, a Placa do Azure, os Artefatos do Azure e outras que aceleram o processo de criação de software e melhoram sua qualidade.

O ShareX é um aplicativo gratuito que permite capturar e gravar qualquer parte da tela. O projeto é escrito em C # e é eminentemente adequado para mostrar a configuração do lançamento do analisador estático. O código fonte do projeto está disponível no GitHub .

A saída do comando cloc para o projeto ShareX:
Linguagem
arquivos
em branco
comentário
Código
C #
696
20658
24423
102565
Script MSBuild
11
1
77
5859
Em outras palavras, o projeto é pequeno, mas suficiente para demonstrar o trabalho do PVS-Studio junto com a plataforma em nuvem.

Vamos começar a configuração


Para começar a trabalhar no DevOps do Azure, siga o link e pressione "Iniciar gratuitamente com o GitHub".

Quadro 2

Conceda ao aplicativo Microsoft acesso aos dados da conta do GitHub.

Quadro 1

Você precisará criar uma conta da Microsoft para concluir seu registro.

Quadro 12

Após o registro, crie um projeto:

Quadro 5

Em seguida, precisamos passar para "Pipelines" - "Builds" e criar um novo pipeline de Build.

Quadro 8

Quando perguntado onde nosso código está localizado, responderemos - GitHub.

Quadro 13

Autorize os pipelines do Azure e escolha o repositório com o projeto, para o qual configuraremos a execução do analisador estático.

Quadro 7

Na janela de seleção de modelos, escolha "Pipeline inicial".

Quadro 17

Podemos executar a análise de código estático do projeto de duas maneiras: usando agentes hospedados pela Microsoft ou auto-hospedados.

Primeiro, usaremos agentes hospedados pela Microsoft. Esses agentes são máquinas virtuais comuns que são lançadas quando executamos nosso pipeline. Eles são removidos quando a tarefa é concluída. O uso de tais agentes nos permite não perder tempo com o suporte e a atualização, mas impõe certas restrições, por exemplo - incapacidade de instalar software adicional usado para criar um projeto.

Vamos substituir a configuração padrão sugerida pela seguinte para usar agentes hospedados pela Microsoft:

# Setting up run triggers # Run only for changes in the master branch trigger: - master # Since the installation of random software in virtual machines # is prohibited, we'll use a Docker container, # launched on a virtual machine with Windows Server 1803 pool: vmImage: 'win1803' container: microsoft/dotnet-framework:4.7.2-sdk-windowsservercore-1803 steps: # Download the analyzer distribution - task: PowerShell@2 inputs: targetType: 'inline' script: 'Invoke-WebRequest -Uri https://files.viva64.com/PVS-Studio_setup.exe -OutFile PVS-Studio_setup.exe' - task: CmdLine@2 inputs: workingDirectory: $(System.DefaultWorkingDirectory) script: | # Restore the project and download dependencies nuget restore .\ShareX.sln # Create the directory, where files with analyzer reports will be saved md .\PVSTestResults # Install the analyzer PVS-Studio_setup.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /COMPONENTS=Core # Create the file with configuration and license information "C:\Program Files (x86)\PVS-Studio\PVS-Studio_Cmd.exe" credentials -u $(PVS_USERNAME) -n $(PVS_KEY) # Run the static analyzer and convert the report in html. "C:\Program Files (x86)\PVS-Studio\PVS-Studio_Cmd.exe" -t .\ShareX.sln -o .\PVSTestResults\ShareX.plog "C:\Program Files (x86)\PVS-Studio\PlogConverter.exe" -t html -o .\PVSTestResults\ .\PVSTestResults\ShareX.plog # Save analyzer reports - task: PublishBuildArtifacts@1 inputs: pathToPublish: PVSTestResults artifactName: PVSTestResults 

Nota: de acordo com a documentação , o contêiner usado deve ser armazenado em cache na imagem da máquina virtual, mas no momento da redação do artigo ele não está funcionando e o contêiner é baixado toda vez que a tarefa é iniciada, o que afeta negativamente o tempo de execução.

Vamos salvar o pipeline e criar variáveis ​​que serão usadas para criar o arquivo de licença. Para fazer isso, abra a janela de edição do pipeline e clique em "Variáveis" no canto superior direito.

Quadro 14

Em seguida, adicione duas variáveis ​​- PVS_USERNAME e PVS_KEY , contendo o nome do usuário e a chave de licença, respectivamente. Ao criar a variável PVS_KEY , não se esqueça de selecionar "Manter este valor em segredo" para criptografar os valores da variável com uma chave RSA de 2048 bits e para suprimir a saída do valor da variável no log de desempenho da tarefa.

Quadro 15

Salve as variáveis ​​e execute o pipeline clicando em "Executar".

A segunda opção para executar a análise - use um agente auto-hospedado. Podemos personalizar e gerenciar agentes auto-hospedados. Esses agentes oferecem mais oportunidades de instalação de software, necessárias para a construção e teste de nosso produto de software.

Antes de usar esses agentes, você deve configurá-los de acordo com as instruções, instalar e configurar o analisador estático.

Para executar a tarefa em um agente auto-hospedado, substituiremos a configuração sugerida pelo seguinte:

 # Setting up triggers # Run the analysis for master-branch trigger: - master # The task is run on a self-hosted agent from the pool 'MyPool' pool: 'MyPool' steps: - task: CmdLine@2 inputs: workingDirectory: $(System.DefaultWorkingDirectory) script: | # Restore the project and download dependencies nuget restore .\ShareX.sln # Create the directory where files with analyzer reports will be saved md .\PVSTestResults # Run the static analyzer and convert the report in html. "C:\Program Files (x86)\PVS-Studio\PVS-Studio_Cmd.exe" -t .\ShareX.sln -o .\PVSTestResults\ShareX.plog "C:\Program Files (x86)\PVS-Studio\PlogConverter.exe" -t html -o .\PVSTestResults\ .\PVSTestResults\ShareX.plog # Save analyzer reports - task: PublishBuildArtifacts@1 inputs: pathToPublish: PVSTestResults artifactName: PVSTestResults 

Após a conclusão da tarefa, você pode fazer o download do arquivo morto com os relatórios do analisador na guia "Resumo" ou usar a extensão Enviar email que permite configurar o envio de email ou considerar outra ferramenta conveniente no Marketplace .

Quadro 21

Resultados da análise


Agora, vejamos alguns erros encontrados no projeto testado, o ShareX.

Verificações excessivas

Para aquecer, vamos começar com falhas simples no código, a saber, com verificações redundantes:

 private void PbThumbnail_MouseMove(object sender, MouseEventArgs e) { .... IDataObject dataObject = new DataObject(DataFormats.FileDrop, new string[] { Task.Info.FilePath }); if (dataObject != null) { Program.MainForm.AllowDrop = false; dragBoxFromMouseDown = Rectangle.Empty; pbThumbnail.DoDragDrop(dataObject, DragDropEffects.Copy | DragDropEffects.Move); Program.MainForm.AllowDrop = true; } .... } 

Aviso do PVS-Studio: V3022 [CWE-571] A expressão 'dataObject! = Null' sempre é verdadeira. TaskThumbnailPanel.cs 415

Vamos prestar atenção na verificação da variável dataObject para null . Por que está aqui? dataObject não pode ser nulo nesse caso, pois é inicializado por uma referência em um objeto criado. Como resultado, temos uma verificação excessiva. Crítico? Não. Parece sucinto? Não. Essa verificação é claramente melhor removida para não bagunçar o código.

Vejamos outro fragmento de código que podemos comentar de maneira semelhante:

 private static Image GetDIBImage(MemoryStream ms) { .... try { .... return new Bitmap(bmp); .... } finally { if (gcHandle != IntPtr.Zero) { GCHandle.FromIntPtr(gcHandle).Free(); } } .... } private static Image GetImageAlternative() { .... using (MemoryStream ms = dataObject.GetData(format) as MemoryStream) { if (ms != null) { try { Image img = GetDIBImage(ms); if (img != null) { return img; } } catch (Exception e) { DebugHelper.WriteException(e); } } } .... } 

Aviso do PVS-Studio: V3022 [CWE-571] A expressão 'img! = Null' sempre é verdadeira. ClipboardHelpers.cs 289

No método GetImageAlternative , a variável img é verificada e não é nula logo após a criação de uma nova instância da classe Bitmap . A diferença do exemplo anterior aqui é que usamos o método GetDIBImage em vez do construtor para inicializar a variável img . O autor do código sugere que uma exceção pode ocorrer nesse método, mas ele declara apenas os blocos try e, finalmente , omitindo catch . Portanto, se ocorrer uma exceção, o método de chamada GetImageAlternative não obterá uma referência a um objeto do tipo Bitmap , mas precisará manipular a exceção em seu próprio bloco de captura . Nesse caso, a variável img não será inicializada e o encadeamento de execução nem alcançará a verificação img! = Null, mas entrará no bloco catch. Conseqüentemente, o analisador apontou para uma verificação excessiva.

Vamos considerar o seguinte exemplo de aviso da V3022 :

 private void btnCopyLink_Click(object sender, EventArgs e) { .... if (lvClipboardFormats.SelectedItems.Count == 0) { url = lvClipboardFormats.Items[0].SubItems[1].Text; } else if (lvClipboardFormats.SelectedItems.Count > 0) { url = lvClipboardFormats.SelectedItems[0].SubItems[1].Text; } .... } 

Aviso do PVS-Studio: V3022 [CWE-571] A expressão 'lvClipboardFormats.SelectedItems.Count> 0' sempre é verdadeira. AfterUploadForm.cs 155

Vamos dar uma olhada na segunda expressão condicional. Lá, verificamos o valor da propriedade Count somente leitura. Esta propriedade mostra o número de elementos na instância da coleção SelectedItems . A condição é executada apenas se a propriedade Count for maior que zero. Tudo ficaria bem, mas na instrução externa if Count já está marcada com 0. A instância da coleção SelectedItems não pode ter o número de elementos menor que zero, portanto, Count é igual ou maior que 0. Desde que nós já executou a verificação de contagem para 0 na primeira instrução if e era falsa, não há sentido em escrever outra verificação de contagem por ser maior que zero no ramo else.

O exemplo final de um aviso da V3022 será o seguinte fragmento de código:

 private void DrawCursorGraphics(Graphics g) { .... int cursorOffsetX = 10, cursorOffsetY = 10, itemGap = 10, itemCount = 0; Size totalSize = Size.Empty; int magnifierPosition = 0; Bitmap magnifier = null; if (Options.ShowMagnifier) { if (itemCount > 0) totalSize.Height += itemGap; .... } .... } 

Aviso do PVS-Studio: A expressão V3022 'itemCount> 0' é sempre falsa. RegionCaptureForm.cs 1100

O analisador notou que a condição itemCount> 0 sempre será falsa, pois a variável itemCount é declarada e, ao mesmo tempo, atribuída zero acima. Essa variável não é usada em nenhum lugar até a própria condição; portanto, o analisador estava certo sobre a expressão condicional, cujo valor é sempre falso.

Bem, vamos agora olhar para algo realmente sapid.

A melhor maneira de entender um bug é visualizando um bug

Parece-nos que um erro bastante interessante foi encontrado neste local:

 public static void Pixelate(Bitmap bmp, int pixelSize) { .... float r = 0, g = 0, b = 0, a = 0; float weightedCount = 0; for (int y2 = y; y2 < yLimit; y2++) { for (int x2 = x; x2 < xLimit; x2++) { ColorBgra color = unsafeBitmap.GetPixel(x2, y2); float pixelWeight = color.Alpha / 255; r += color.Red * pixelWeight; g += color.Green * pixelWeight; b += color.Blue * pixelWeight; a += color.Alpha * pixelWeight; weightedCount += pixelWeight; } } .... ColorBgra averageColor = new ColorBgra((byte)(b / weightedCount), (byte)(g / weightedCount), (byte)(r / weightedCount), (byte)(a / pixelCount)); .... } 

Não gostaria de mostrar todas as cartas e revelar o que nosso analisador encontrou, então vamos deixar isso de lado por um tempo.

Pelo nome do método, é fácil adivinhar o que está fazendo - você fornece uma imagem ou um fragmento de uma imagem e a pixeliza. O código do método é bastante longo, portanto não o citaremos completamente, mas tente explicar seu algoritmo e que tipo de bug o PVS-Studio conseguiu encontrar.

Este método recebe dois parâmetros: um objeto do tipo Bitmap e o valor do tipo int que indica o tamanho da pixelização. O algoritmo de operação é bastante simples:

1) Divida o fragmento de imagem recebido em quadrados com o lado igual ao tamanho da pixelização. Por exemplo, se tivermos um tamanho de pixelação igual a 15, obteremos um quadrado contendo 15x15 = 225 pixels.

2) Além disso, percorremos cada pixel neste quadrado e acumulamos os valores dos campos Vermelho , Verde , Azul e Alfa em variáveis ​​intermediárias e, antes disso, multiplicamos o valor da cor correspondente e o canal alfa pela variável pixelWeight , obtida por dividindo o valor Alpha por 255 (a variável Alpha é do tipo byte ). Além disso, ao atravessar pixels, somamos os valores, escritos em pixelWeight na variável weightedCount . O fragmento de código que executa as ações acima é o seguinte:

 ColorBgra color = unsafeBitmap.GetPixel(x2, y2); float pixelWeight = color.Alpha / 255; r += color.Red * pixelWeight; g += color.Green * pixelWeight; b += color.Blue * pixelWeight; a += color.Alpha * pixelWeight; weightedCount += pixelWeight; 

A propósito, observe que, se o valor da variável Alpha for zero, pixelWeight não adicionará à variável weightedCount nenhum valor para esse pixel. Nós precisaremos disso no futuro.

3) Depois de percorrer todos os pixels no quadrado atual, podemos criar uma cor "média" comum para esse quadrado. O código que faz isso é o seguinte:

 ColorBgra averageColor = new ColorBgra((byte)(b / weightedCount), (byte)(g / weightedCount), (byte)(r / weightedCount), (byte)(a / pixelCount)); 

4) Agora, quando obtivemos a cor final e a escrevemos na variável averageColor , podemos novamente percorrer cada pixel do quadrado e atribuir um valor a partir de averageColor .

5) Volte ao ponto 2 enquanto tivermos quadrados sem tratamento.

Mais uma vez, a variável weightedCount não é igual ao número de todos os pixels em um quadrado. Por exemplo, se uma imagem contiver um pixel completamente transparente (valor zero no canal alfa), a variável pixelWeight será zero para esse pixel ( 0/255 = 0). Portanto, esse pixel não afetará a formação da variável weightedCount . É bastante lógico - não faz sentido levar em conta as cores de um pixel completamente transparente.

Portanto, tudo parece razoável - a pixelização deve funcionar corretamente. E realmente faz. Isso não é apenas para imagens png que incluem pixels com valores no canal alfa abaixo de 255 e desiguais a zero. Observe a imagem pixelizada abaixo:

Quadro 3

Você já viu a pixelização? Nós também não. Ok, agora vamos revelar essa pequena intriga e explicar onde exatamente o bug está oculto neste método. O erro foi arrastado para a linha da computação da variável pixelWeight :

 float pixelWeight = color.Alpha / 255; 

O fato é que, ao declarar a variável pixelWeight como float , o autor do código implica que, ao dividir o campo Alpha por 255, ele obterá números fracionários, além de zero e um. É aqui que o problema se oculta, pois a variável Alpha é do tipo byte . Ao mergulhá-lo em 255, obtemos um valor inteiro. Somente depois disso, ele será convertido implicitamente no tipo float , o que significa que a parte fracionária se perde.

É fácil explicar por que é impossível pixelizar imagens png com alguma transparência. Como para esses pixels, os valores do canal alfa estão no intervalo 0 <Alpha <255, a variável Alpha dividida por 255 sempre resultará em 0. Portanto, os valores das variáveis pixelWeight , r , g , b , a , weightedCount também sempre seja 0. Como resultado, nossa averageColor estará com valores zero em todos os canais: vermelho - 0, azul - 0, verde - 0, alfa - 0. Ao pintar um quadrado nesta cor, não alteramos a cor original dos pixels, como o averageColor é absolutamente transparente. Para corrigir esse erro, precisamos converter explicitamente o campo Alpha para o tipo float . A versão fixa da linha de código pode ser assim:

 float pixelWeight = (float)color.Alpha / 255; 

Bem, é hora de citar a mensagem do PVS-Studio para o código incorreto:

Aviso do PVS-Studio: V3041 [CWE-682] A expressão foi convertida implicitamente do tipo 'int' para o tipo 'float'. Considere utilizar uma conversão de tipo explícita para evitar a perda de uma parte fracionária. Um exemplo: double A = (double) (X) / Y; ImageHelpers.cs 1119

Para comparação, citemos a captura de tela de uma imagem verdadeiramente pixelizada, obtida na versão corrigida do aplicativo:

Quadro 6

Potencial NullReferenceException

 public static bool AddMetadata(Image img, int id, string text) { .... pi.Value = bytesText; if (pi != null) { img.SetPropertyItem(pi); return true; } .... } 

Aviso do PVS-Studio: V3095 [CWE-476] O objeto 'pi' foi usado antes de ser verificado em relação a nulo. Verifique as linhas: 801, 803. ImageHelpers.cs 801

Esse fragmento de código mostra que o autor esperava que a variável pi pudesse ser nula ; por isso, antes de chamar o método SetPropertyItem , a verificação pi! = Null ocorre. É estranho que, antes dessa verificação, a propriedade seja atribuída a uma matriz de bytes, porque se pi for nulo , uma exceção do tipo NullReferenceException será lançada.

Uma situação semelhante foi notada em outro local:

 private static void Task_TaskCompleted(WorkerTask task) { .... task.KeepImage = false; if (task != null) { if (task.RequestSettingUpdate) { Program.MainForm.UpdateCheckStates(); } .... } .... } 

Aviso do PVS-Studio: V3095 [CWE-476] O objeto 'task' foi usado antes de ser verificado contra nulo. Verifique as linhas: 268, 270. TaskManager.cs 268

O PVS-Studio encontrou outro erro semelhante. O ponto é o mesmo; portanto, não há grande necessidade de citar o fragmento de código; a mensagem do analisador será suficiente.

Aviso do PVS-Studio: V3095 [CWE-476] O objeto 'Config.PhotobucketAccountInfo' foi usado antes de ser verificado como nulo. Verifique as linhas: 216, 219. UploadersConfigForm.cs 216

O mesmo valor de retorno

Um fragmento de código suspeito foi encontrado no método EvalWindows da classe WindowsList , que retorna true em todos os casos:

 public class WindowsList { public List<IntPtr> IgnoreWindows { get; set; } .... public WindowsList() { IgnoreWindows = new List<IntPtr>(); } public WindowsList(IntPtr ignoreWindow) : this() { IgnoreWindows.Add(ignoreWindow); } .... private bool EvalWindows(IntPtr hWnd, IntPtr lParam) { if (IgnoreWindows.Any(window => hWnd == window)) { return true; // <= } windows.Add(new WindowInfo(hWnd)); return true; // <= } } 

Aviso do PVS-Studio: V3009 É estranho que esse método sempre retorne um e o mesmo valor de 'true'. WindowsList.cs 82

Parece lógico que, se na lista chamada IgnoreWindows, houver um ponteiro com o mesmo nome que hWnd , o método deverá retornar false .

A lista IgnoreWindows pode ser preenchida ao chamar o construtor WindowsList (IntPtr ignoreWindow) ou diretamente acessando a propriedade como pública. De qualquer forma, de acordo com o Visual Studio, no momento no código essa lista não é preenchida. Este é outro lugar estranho desse método.

Chamada insegura de manipuladores de eventos

 protected void OnNewsLoaded() { if (NewsLoaded != null) { NewsLoaded(this, EventArgs.Empty); } } 

Aviso do PVS-Studio: V3083 [CWE-367] Chamada não segura do evento 'NewsLoaded', NullReferenceException é possível. Considere atribuir um evento a uma variável local antes de invocá-lo. NewsListControl.cs 111

Aqui, um caso muito desagradável pode ocorrer. Depois de verificar a variável NewsLoaded para null, o método, que lida com um evento, pode ser cancelado, por exemplo, em outro encadeamento. Nesse caso, quando entrarmos no corpo da instrução if, a variável NewsLoaded já estará nula. Uma NullReferenceException pode ocorrer ao tentar chamar assinantes do evento NewsLoaded , que é nulo. É muito mais seguro usar um operador condicional nulo e reescrever o código acima da seguinte maneira:

 protected void OnNewsLoaded() { NewsLoaded?.Invoke(this, EventArgs.Empty); } 

O analisador apontou para 68 fragmentos semelhantes. Não descreveremos todos eles - todos eles têm um padrão de chamada semelhante.

Retornar nulo de ToString

Recentemente, descobri em um artigo interessante do meu colega que a Microsoft não recomenda retornar nulo do método substituído ToString . O PVS-Studio está ciente disso:

 public override string ToString() { lock (loggerLock) { if (sbMessages != null && sbMessages.Length > 0) { return sbMessages.ToString(); } return null; } } 

Aviso do PVS-Studio: V3108 Não é recomendável retornar 'null' do método 'ToSting ()'. Logger.cs 167

Por que atribuído se não usado?

 public SeafileCheckAccInfoResponse GetAccountInfo() { string url = URLHelpers.FixPrefix(APIURL); url = URLHelpers.CombineURL(APIURL, "account/info/?format=json"); .... } 

Aviso do PVS-Studio: V3008 A variável 'url' recebe valores duas vezes sucessivamente. Talvez isso seja um erro. Verifique as linhas: 197, 196. Seafile.cs 197

Como podemos ver no exemplo, ao declarar a variável url , é atribuído um valor, retornado do método FixPrefix . Na próxima linha, limpamos o valor obtido, mesmo sem usá-lo em qualquer lugar. Temos algo semelhante ao código morto: funciona, mas não afeta o resultado. Provavelmente, esse erro é resultado de uma cópia e colagem, pois esses fragmentos de código ocorrem em mais 9 métodos. Como exemplo, citaremos dois métodos com uma primeira linha semelhante:

 public bool CheckAuthToken() { string url = URLHelpers.FixPrefix(APIURL); url = URLHelpers.CombineURL(APIURL, "auth/ping/?format=json"); .... } .... public bool CheckAPIURL() { string url = URLHelpers.FixPrefix(APIURL); url = URLHelpers.CombineURL(APIURL, "ping/?format=json"); .... } 

Conclusões


Como podemos ver, a complexidade da configuração das verificações automáticas do analisador não depende do sistema de CI escolhido. Levamos literalmente 15 minutos e vários cliques do mouse para configurar a verificação do código do nosso projeto com um analisador estático.

Concluindo, convidamos você a baixar e experimentar o analisador em seus projetos.

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


All Articles