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:
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".
Conceda ao aplicativo Microsoft acesso aos dados da conta do GitHub.
Você precisará criar uma conta da Microsoft para concluir seu registro.
Após o registro, crie um projeto:
Em seguida, precisamos passar para "Pipelines" - "Builds" e criar um novo pipeline de Build.
Quando perguntado onde nosso código está localizado, responderemos - GitHub.
Autorize os pipelines do Azure e escolha o repositório com o projeto, para o qual configuraremos a execução do analisador estático.
Na janela de seleção de modelos, escolha "Pipeline inicial".
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.
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.
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 .
Resultados da análise
Agora, vejamos alguns erros encontrados no projeto testado, o ShareX.
Verificações excessivasPara 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 bugParece-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:
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:
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 retornoUm 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;
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 ToStringRecentemente, 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.