Faz três meses que 2018 terminou. Para muitos, apenas sobrevoou, mas para nós, desenvolvedores do PVS-Studio, foi um ano bastante movimentado. Estávamos trabalhando muito, competindo sem medo por divulgar a análise estática e procurando erros em projetos de código aberto, escritos nas linguagens C, C ++, C # e Java. Neste artigo, reunimos os 10 mais interessantes deles para você!
Para encontrar os lugares mais intrigantes, usamos o analisador de código estático
PVS-Studio . Ele pode detectar erros e possíveis vulnerabilidades no código, escrito nos idiomas listados acima.
Se você estiver entusiasmado com a busca de erros, sempre poderá fazer o download e experimentar o nosso analisador. Fornecemos a
versão gratuita do analisador para estudantes e desenvolvedores entusiasmados, a
licença gratuita para desenvolvedores de projetos de código aberto e também a
versão de teste para todo o mundo e seu cão. Quem sabe, talvez até o próximo ano você consiga criar seu próprio top 10? :)
Nota: convido você a verificar a si mesmo e, antes de olhar para o aviso do analisador, tente revelar os próprios defeitos. Quantos erros você poderá encontrar?
Décimo lugar
Fonte:
Into Space Again: como o unicórnio visitou o StellariumEste erro foi detectado ao verificar um planetário virtual chamado Stellarium.
O fragmento de código acima, embora pequeno, está repleto de um erro bastante complicado:
Plane::Plane(Vec3f &v1, Vec3f &v2, Vec3f &v3) : distance(0.0f), sDistance(0.0f) { Plane(v1, v2, v3, SPolygon::CCW); }
Encontrou?
Aviso do PVS-Studio :
V603 O objeto foi criado, mas não está sendo usado. Se você deseja chamar o construtor, 'this-> Plane :: Plane (....)' deve ser usado. Plane.cpp 29
O autor do código pretendia inicializar os campos de alguns objetos, usando outro construtor, aninhado no principal. Bem, em vez disso, ele apenas conseguiu criar um objeto temporário destruído ao sair de seu escopo. Ao fazer isso, vários campos do objeto permanecerão não inicializados.
O autor deveria ter usado um construtor delegado, introduzido no C ++ 11, em vez de uma chamada de construtor aninhado. Por exemplo, ele poderia ter escrito assim:
Plane::Plane(Vec3f& v1, Vec3f& v2, Vec3f& v3) : Plane(v1, v2, v3, SPolygon::CCW) { distance = 0.0f; sDistance = 0.0f; }
Dessa forma, todos os campos necessários teriam sido inicializados corretamente. Não é maravilhoso?
Nono lugar
Fonte:
Perl 5: Como ocultar erros em macrosUma macro muito notável se destaca em toda a sua beleza no nono lugar.
Ao coletar erros para escrever um artigo, meu colega Svyatoslav encontrou um aviso, emitido pelo analisador, relacionado ao uso de macro. Aqui está:
PP(pp_match) { .... MgBYTEPOS_set(mg, TARG, truebase, RXp_OFFS(prog)[0].end); .... }
Para descobrir o que estava errado, Svyatoslav se aprofundou. Ele abriu a definição de macro e viu que ela continha várias macros aninhadas, algumas das quais também tinham macros aninhadas. Era tão difícil entender isso, então ele teve que usar um arquivo pré-processado. Infelizmente, não ajudou. Isto foi o que Svyatoslav encontrou na linha de código anterior:
(((targ)->sv_flags & 0x00000400) && (!((targ)->sv_flags & 0x00200000) || S_sv_only_taint_gmagic(targ)) ? (mg)->mg_len = ((prog->offs)[0].end), (mg)->mg_flags |= 0x40 : ((mg)->mg_len = (((targ)->sv_flags & 0x20000000) && !__builtin_expect(((((PL_curcop)->cop_hints + 0) & 0x00000008) ? (_Bool)1 :(_Bool)0),(0))) ? (ssize_t)Perl_utf8_length( (U8 *)(truebase), (U8 *)(truebase)+((prog->offs)[0].end)) : (ssize_t)((prog->offs)[0].end), (mg)->mg_flags &= ~0x40));
Aviso do PVS-Studio :
V502 Talvez o operador '?:'
Funcione de maneira diferente do esperado. O operador '?:' Tem uma prioridade mais baixa que o operador '&&'. pp_hot.c 3036
Eu acho que seria um desafio simplesmente perceber esse erro. Estamos estudando esse código há muito tempo, mas, falando francamente, não encontramos nenhum erro nele. Enfim, é um exemplo bastante divertido de código mal legível.
Eles dizem que as macros são más. Claro, existem casos em que as macros são indispensáveis, mas se você pode substituir uma macro por uma função - você definitivamente deve fazê-lo.
Macros aninhadas são especialmente cheias de armadilhas. Não apenas porque é difícil compreendê-los, mas também porque eles podem dar resultados imprevisíveis. Se um programador cometer um erro em tal macro - será muito mais difícil encontrá-lo em uma macro do que em uma função.
Oitavo lugar
Fonte:
Crómio: Outros ErrosO exemplo a seguir foi retirado da série de artigos sobre a análise do projeto Chromium. O erro estava oculto na biblioteca WebRTC.
std::vector<SdpVideoFormat> StereoDecoderFactory::GetSupportedFormats() const { std::vector<SdpVideoFormat> formats = ....; for (const auto& format : formats) { if (cricket::CodecNamesEq(....)) { .... formats.push_back(stereo_format); } } return formats; }
Aviso do PVS-Studio: os iteradores V789 CWE-672 para o contêiner de 'formatos', usados no loop for baseado em intervalo, tornam-se inválidos quando a função 'push_back' é chamada. stereocodecfactory.cc 89
O erro é que o tamanho do vetor de
formatos varia dentro do loop for baseado em intervalo. Os loops baseados em intervalo são baseados em iteradores, é por isso que a alteração do tamanho do contêiner dentro desses loops pode resultar na invalidação desses iteradores.
Este erro persiste, se reescrever o loop com um uso explícito de iteradores. Para maior clareza, posso citar o seguinte código:
for (auto format = begin(formats), __end = end(formats); format != __end; ++format) { if (cricket::CodecNamesEq(....)) { .... formats.push_back(stereo_format); } }
Por exemplo, ao usar o método
push_back , uma realocação de vetor pode ocorrer - dessa maneira, os iteradores abordarão um local de memória inválido.
Para evitar esses erros, siga a regra: nunca altere o tamanho de um contêiner dentro de um loop com condições vinculadas a esse contêiner. Também se refere a loops baseados em intervalo e loops usando iteradores. Você pode ler esta
discussão no StackOverflow, que aborda o tópico de operações que causa a invalidação de iteradores.
Sétimo lugar
Fonte:
Godot: Sobre o uso regular de analisadores estáticosO primeiro exemplo da indústria de jogos será um trecho de código que encontramos no mecanismo de jogos Godot. Provavelmente, levará algum trabalho para perceber o erro, mas tenho certeza de que nossos leitores experientes vão lidar com isso.
void AnimationNodeBlendSpace1D::add_blend_point( const Ref<AnimationRootNode> &p_node, float p_position, int p_at_index) { ERR_FAIL_COND(blend_points_used >= MAX_BLEND_POINTS); ERR_FAIL_COND(p_node.is_null()); ERR_FAIL_COND(p_at_index < -1 || p_at_index > blend_points_used); if (p_at_index == -1 || p_at_index == blend_points_used) { p_at_index = blend_points_used; } else { for (int i = blend_points_used - 1; i > p_at_index; i++) { blend_points[i] = blend_points[i - 1]; } } .... }
Aviso do PVS-Studio: V621 CWE-835 Considere inspecionar o operador 'for'. É possível que o loop seja executado incorretamente ou não seja executado. animation_blend_space_1d.cpp 113
Vamos dar uma olhada na condição do loop. A variável do contador é inicializada pelo valor
blend_points_used - 1 . Além disso, a julgar pelas duas verificações anteriores (em
ERR_FAIL_COND e em
if ), fica claro que, no momento da execução do loop
blend_points_used ,
blend_points_used sempre será maior que
p_at_index . Portanto, a condição do loop é sempre verdadeira ou o loop não é executado.
Se
blend_points_used - 1 == p_at_index , o loop não será executado.
Em todos os outros casos, a verificação
i> p_at_index sempre será verdadeira, pois o contador
i sobe em cada iteração do loop.
Parece que o ciclo é eterno, mas não é assim.
Primeiramente, ocorrerá um estouro inteiro da variável
i (que é um comportamento indefinido). Isso significa que não devemos confiar nisso.
Se
eu não tivesse
assinado int , depois que o contador atingir o maior valor possível, o operador
i ++ o transformaria em
0 . Esse comportamento é definido pelo padrão e é chamado "Invólucro não assinado". No entanto, você deve estar ciente de que o uso desse mecanismo também
não é uma boa ideia .
Foi o primeiro ponto, mas ainda temos o segundo! O caso é que nem chegaremos a um estouro inteiro. O índice da matriz sairá dos limites muito antes. Isso significa que haverá uma tentativa de acessar a memória fora do bloco alocado para a matriz. Qual é o comportamento indefinido também. Um exemplo clássico :)
Posso fazer algumas recomendações para facilitar a prevenção de erros semelhantes:
- Escreva código simples e compreensível
- Revise o código mais detalhadamente e escreva mais testes para o código recém-escrito
- Use analisadores estáticos;)
Sexto lugar
Fonte:
Amazon Lumberyard: Um Grito de AngústiaAqui está outro exemplo da indústria gamedev, a saber, do código-fonte do mecanismo AAA do Amazon Lumberyard.
void TranslateVariableNameByOperandType(....) {
Aviso do PVS-Studio :
V523 A
instrução 'then' é equivalente à instrução 'else'. toglsloperand.c 700
O Amazon Lumberyard é desenvolvido como um mecanismo de plataforma cruzada. Por esse motivo, os desenvolvedores tentam oferecer suporte ao maior número possível de compiladores. Como podemos ver nos comentários, um programador Igor veio contra o compilador Qualcomm.
Não sabemos se ele conseguiu executar sua tarefa e verificou as verificações "paranóicas" do compilador, mas deixou um código muito estranho. O estranho é que tanto os ramos
então - quanto os outros
- da instrução
if contêm código absolutamente idêntico. Provavelmente, esse erro resultou do uso de um método desleixado de copiar e colar.
Eu nem sei o que aconselhar aqui. Então, eu só desejo aos desenvolvedores do Amazon Lumberyard tudo de melhor na correção de erros e boa sorte para o desenvolvedor Igor!
Quinto lugar
Fonte:
Mais uma vez, o analisador PVS-Studio provou ser mais atento do que uma pessoaUma história interessante aconteceu com o próximo exemplo. Meu colega Andrey Karpov estava preparando um artigo sobre outra verificação da estrutura do Qt. Ao escrever alguns erros notáveis, ele tropeçou no aviso do analisador, que considerou falso. Aqui está esse fragmento de código e o aviso para ele:
QWindowsCursor::CursorState QWindowsCursor::cursorState() { enum { cursorShowing = 0x1, cursorSuppressed = 0x2 }; CURSORINFO cursorInfo; cursorInfo.cbSize = sizeof(CURSORINFO); if (GetCursorInfo(&cursorInfo)) { if (cursorInfo.flags & CursorShowing)
Aviso do PVS-Studio: V616 CWE-480 A constante nomeada 'CursorShowing' com o valor de 0 é usada na operação bit a bit. qwindowscursor.cpp 669
O que significa que o PVS-Studio estava reclamando no local, o que obviamente não tinha erro! É impossível que a constante
CursorShowing seja
0 , pois apenas algumas linhas acima dela são inicializadas por
1 .
Como Andrey estava usando uma versão instável do analisador, ele questionou a exatidão do aviso. Ele examinou cuidadosamente esse trecho de código e ainda não encontrou um bug. Ele acabou dando um falso positivo no rastreador de bugs para que outros colegas pudessem remediar a situação.
Apenas uma análise detalhada mostrou que o PVS-Studio acabou sendo mais cuidadoso do que uma pessoa novamente. O valor
0x1 é atribuído a uma constante nomeada chamada
cursorShowing enquanto
CursorShowing participa de uma operação "e" bit a bit. Estas são duas constantes totalmente diferentes, a primeira começa com uma letra minúscula, a segunda - com uma maiúscula.
O código é compilado com êxito, porque a classe
QWindowsCursor realmente contém uma constante com esse nome. Aqui está sua definição:
class QWindowsCursor : public QPlatformCursor { public: enum CursorState { CursorShowing, CursorHidden, CursorSuppressed }; .... }
Se você não atribuir um valor a uma constante enum explicitamente, ele será inicializado por padrão. Como
CursorShowing é o primeiro elemento na enumeração, ele será atribuído
0 .
Para evitar esses erros, você não deve dar nomes muito semelhantes às entidades. Você deve seguir particularmente essa regra se as entidades forem do mesmo tipo ou puderem ser implicitamente convertidas uma para a outra. Como nesses casos, será quase impossível perceber o erro, mas o código incorreto ainda será compilado e viverá na rua fácil dentro do seu projeto.
Quarto lugar
Fonte:
Dê um tiro no pé ao manusear dados de entradaEstamos nos aproximando dos três principais finalistas e o próximo da fila é o erro do projeto FreeSWITCH.
static const char *basic_gets(int *cnt) { .... int c = getchar(); if (c < 0) { if (fgets(command_buf, sizeof(command_buf) - 1, stdin) != command_buf) { break; } command_buf[strlen(command_buf)-1] = '\0'; break; } .... }
Aviso do PVS-Studio: V1010 CWE-20 Dados contaminados não verificados são usados no índice: 'strlen (command_buf)'.
O analisador avisa que alguns dados não verificados são usados na expressão
strlen (command_buf) - 1 . De fato: se
command_buf é uma string vazia em termos da linguagem C (contendo o único caractere - '\ 0'),
strlen (command_buf) retornará
0 . Nesse caso, o
comando_buf [-1] será acessado, o que é um comportamento indefinido. Isso é ruim!
O entusiasmo real deste erro não é
por que ocorre, mas
como . Este erro é um desses exemplos mais agradáveis, que você "toca" sozinho, reproduz. Você pode executar o FreeSwitch, realizar algumas ações que levarão à execução do fragmento de código mencionado acima e passar uma string vazia para a entrada do programa.
Como resultado, com um movimento sutil da mão, um programa de trabalho se transforma em um não trabalho! Você pode encontrar detalhes sobre como reproduzir esse erro no artigo de origem no link fornecido acima. Enquanto isso, deixe-me fornecer um resultado revelador:
Lembre-se de que os dados de saída podem ser qualquer coisa, portanto, você deve sempre verificar. Dessa forma, o analisador não irá reclamar e o programa se tornará mais confiável.
Agora é hora de escolher o vencedor: estamos no final do jogo agora! A propósito, os finalistas de bugs já esperaram uma longa espera, depois ficaram entediados e até começaram a ser pequeninos. Veja o que eles fizeram enquanto estávamos fora!
Terceiro lugar
Fonte:
NCBI Genome Workbench: Pesquisa científica sob ameaçaUm trecho de código do projeto NCBI Genome Workbench, que é um conjunto de ferramentas para estudar e analisar dados genéticos, abre os três principais vencedores. Mesmo que você não precise ser um super-humano geneticamente modificado para encontrar esse bug, poucas pessoas sabem sobre a contingência para cometer um erro aqui.
void tds_answer_challenge(....) { .... if (ntlm_v == 1) { .... memset(hash, 0, sizeof(hash)); memset(passwd_buf, 0, sizeof(passwd_buf)); ... } else { .... } }
Avisos do PVS-Studio:- V597 O compilador pode excluir a chamada de função 'memset', usada para liberar o buffer 'hash'. A função memset_s () deve ser usada para apagar os dados privados. challenge.c 365
- V597 O compilador pode excluir a chamada de função 'memset', usada para liberar o buffer 'passwd_buf'. A função memset_s () deve ser usada para apagar os dados privados. challenge.c 366
Você encontrou um bug? Se sim, você é um attaboy! .. ou um super-humano geneticamente modificado.
O fato é que os compiladores de otimização modernos são capazes de fazer muito para permitir que um programa criado funcione mais rapidamente. Incluindo o fato de que os compiladores agora podem rastrear que um buffer, passado ao
memset , não é usado em nenhum outro lugar.
Nesse caso, eles podem remover a chamada "desnecessária" do
memset , tendo todos os direitos para isso. Em seguida, o buffer que armazena dados importantes pode permanecer na memória para o deleite dos atacantes.
Nesse contexto, esse comentário nerd "com segurança é melhor ser pedante" soa ainda mais engraçado. A julgar por um pequeno número de avisos, dados para este projeto, seus desenvolvedores fizeram o possível para serem precisos e escrever um código seguro. No entanto, como podemos ver, é possível ignorar facilmente esse defeito de segurança. De acordo com a Enumeração de fraqueza comum, esse defeito é classificado como
CWE-14 : Remoção de código do compilador para limpar buffers.
Você deve usar a função
memset_s () para que a desalocação de memória fique segura. A função é mais segura que
memset () e não pode ser ignorada por um compilador.
Segundo lugar
Fonte:
Como o PVS-Studio se mostrou mais atento do que três programadores e meioUm medalhista de prata foi gentilmente enviado a nós por um de nossos clientes. Ele tinha certeza de que o analisador emitiu alguns falsos positivos.
Evgeniy recebeu o e-mail, olhou através dele e enviou para Svyatoslav. Svyatoslav examinou atentamente o código, enviado pelo cliente e pensou: "como é possível que o analisador tenha cometido tal erro?". Então ele foi pedir conselhos a Andrey. Ele também verificou o local e determinou: de fato, o analisador gerou falsos positivos.
Então é isso, que precisava de conserto. Somente depois que Svyatoslav começou a fazer exemplos sintéticos para criar a tarefa em nosso rastreador de erros, ele entendeu o que estava errado.
Nenhum dos programadores conseguiu encontrar os erros, mas eles realmente estavam no código. Francamente, o autor deste artigo também não conseguiu encontrá-los, apesar do fato de que o analisador emitiu claramente avisos sobre locais errados!
Você encontrará um bug tão astuto? Teste-se em vigilância e atenção.
Aviso do PVS-Studio:- V560 Uma parte da expressão condicional é sempre falsa: (ch> = 0x0FF21). decodew.cpp 525
- V560 Uma parte da expressão condicional é sempre verdadeira: (ch <= 0x0FF3A). decodew.cpp 525
- V560 Uma parte da expressão condicional é sempre falsa: (ch> = 0x0FF41). decodew.cpp 525
- V560 Uma parte da expressão condicional é sempre verdadeira: (ch <= 0x0FF5A). decodew.cpp 525
Se você fez isso - parabéns para você!
O erro está no fato de que o operador de negação lógica (!) Não é aplicado a toda a condição, mas apenas à sua primeira subexpressão:
!((ch >= 0x0FF10) && (ch <= 0x0FF19))
Se essa condição for verdadeira, o valor da variável
ch estará no intervalo [0x0FF10 ... 0x0FF19]. Assim, quatro comparações adicionais já não têm sentido: elas sempre serão verdadeiras ou falsas.
Para evitar esses erros, vale a pena seguir algumas regras. Em primeiro lugar, é muito conveniente e informativo alinhar o código como uma tabela. Em segundo lugar, você não deve sobrecarregar as expressões entre parênteses. Por exemplo, esse código pode ser reescrito assim:
const bool isLetterOrDigit = (ch >= 0x0FF10 && ch <= 0x0FF19)
Dessa forma, haverá menos parênteses e, por outro lado - você provavelmente perceberá um erro ocasional.
Aí vem a cereja no topo - vamos para o primeiro lugar!
Primeiro lugar
Fonte:
Sistema chocado: erros interessantes no código-fonte do choque lendário do sistemaO principal finalista de hoje é um erro do lendário System Shock! É um jogo lançado há muito tempo em 1994, que se tornou um antecessor e inspiração para jogos icônicos, como Dead Space, BioShock e Deus Ex.
Mas primeiro tenho algo a confessar. O que vou mostrar agora, não contém erros. Na verdade, não é nem um pedaço de código, mas simplesmente não pude deixar de compartilhá-lo com você!
O fato é que, ao analisar o código-fonte do jogo, minha colega Victoria descobriu muitos comentários fascinantes. Em fragmentos diferentes, ela encontrou alguns comentários espirituosos e irônicos e até poesia.
É assim que os comentários deixados nos jogos pelos desenvolvedores nos últimos anos 90 se parecem ... Aliás, Doug Church - designer-chefe do System Shock, também estava ocupado escrevendo código. Quem sabe, talvez alguns desses comentários tenham sido escritos por ele? Espero que coisas de homens de toalha não sejam obra dele :)
Conclusão
Concluindo, gostaria de agradecer aos
meus colegas por procurarem novos bugs e escrever sobre eles em artigos. Obrigado pessoal! Sem você, este artigo não seria tão interessante.
Também gostaria de contar um pouco sobre nossas realizações, já que durante todo o ano não estivemos ocupados apenas pesquisando erros. Também desenvolvemos e melhoramos o analisador, o que resultou em mudanças significativas.
Por exemplo, adicionamos suporte a vários novos compiladores e expandimos a lista de regras de diagnóstico. Também implementamos o suporte inicial dos padrões
MISRA C e MISRA C ++ . O novo recurso mais importante e demorado foi o suporte a um novo idioma. Sim, agora podemos analisar o código em
Java ! E mais, temos um
ícone renovado :)
Também quero agradecer aos nossos leitores. Obrigado por ler nossos artigos e escrever para nós! Você é tão sensível e tão importante para nós!
Nossos 10 principais erros de C ++ de 2018 chegaram ao fim. Quais fragmentos você mais gostou e por quê? Você encontrou alguns exemplos interessantes em 2018?
Tudo de bom, até a próxima!