Melhorias na análise estática no GCC 14
(developers.redhat.com)- O
-fanalyzerdo GCC 14 acompanha vários caminhos de execução do código C por meio de execução simbólica, com foco em encontrar, no momento da compilação, defeitos difíceis de capturar antes da execução - Esta atualização se divide em detecção de loops infinitos, visualização de buffer overflow, rastreamento de strings em C e ativação padrão da análise de contaminação, com foco em tornar os diagnósticos mais fáceis de entender
-Wanalyzer-out-of-boundsvai além de um simples aviso e mostra, com um diagrama textual, a relação entre o buffer e a posição de escrita, além de indicar, em strings UTF-8, em que ponto do caractere ocorre o estouro- O novo atributo
null_terminated_string_arg(PARAM_IDX)informa ao analisador e a quem lê o código as condições de uma API que espera strings terminadas em nulo, expondo melhor os caminhos em que um buffer sem terminação nula é passado - A análise de contaminação passa a ser ativada por padrão ao selecionar
-fanalyzer, facilitando identificar fluxos em que valores controlados por atacante são usados em tamanho, índice, deslocamento etc. sem verificação de limite superior
Escopo dos problemas tratados por -fanalyzer
-fanalyzeré um passe de análise estática do GCC que tenta encontrar defeitos no momento da compilação por meio de execução simbólica de vários caminhos de execução do código-fonte C- As melhorias do GCC 14 foram organizadas antes do lançamento oficial de abril de 2024 e, no momento da redação do texto, esperava-se o lançamento do GCC 14.1 ainda em abril de 2024
- A pré-versão do GCC 14.0 já estava em uso no Fedora 40 Beta
- Os exemplos podem ser testados no Compiler Explorer com as novas opções do compilador
Detecção simples de loop infinito
- O GCC 14 adiciona o novo aviso
-Wanalyzer-infinite-loop - O código de exemplo tem
foraninhados em que a condição do segundo loop éj < n, mas a expressão de incremento permanece comoi++- Isso corresponde ao erro de copiar o primeiro
fore não trocar oiporjna expressão de incremento - O analisador continua seguindo o ramo verdadeiro de
j < nnesse caminho e avisa sobre a repetição do loop
- Isso corresponde ao erro de copiar o primeiro
- Atualmente, a saída de diagnóstico fica mais fácil de entender se os eventos numerados de
(1)a(5)forem lidos em sequência - No GCC 15, continua como objetivo desejado melhorar a legibilidade com algo como arte ASCII destacando o caminho do fluxo de controle
- Esse código pode ser testado no exemplo do Compiler Explorer
Visualizando buffer overflow em texto
- No GCC 13, o analisador passou a oferecer verificação de limites por meio de
-Wanalyzer-out-of-bounds - No GCC 14, ele pode exibir a relação espacial de um buffer overflow previsto com um diagrama baseado em texto
- No exemplo que executa
strcpy(buf, "hello")emchar buf[10]e depois chamastrcat(buf, " world!"), é detectado um buffer overflow baseado em pilha- A mensagem anterior indicava que
buftinha capacidade de 10 bytes e que ocorria uma escrita fora dos limites do byte 10 ao byte 12 - O diagrama do GCC 14 mostra junto o buffer de destino preenchido por
strcpye o byteNULterminador existente que serve de ponto inicial parastrcat
- A mensagem anterior indicava que
- Também em exemplos com strings não ASCII, ele acompanha a representação UTF-8 e indica a posição do overflow
- O exemplo usa código que copia
"サツキ"parachar buf[11]e depois concatena"メイ" - O diagrama mostra que o overflow ocorre no meio do caractere
メ, isto é, U+30E1
- O exemplo usa código que copia
- O código relacionado pode ser visto no exemplo com string ASCII e no exemplo com string não ASCII
Rastreamento reforçado de operações com strings em C
- O analisador do GCC 14 aprimora o rastreamento de operações com strings em C, simulando APIs que percorrem o buffer em busca do byte de terminação nula
- Ele emite aviso se houver um caminho em que o ponteiro aponta para um buffer sem terminação nula e ainda assim é passado para essa API
- O novo atributo de função
null_terminated_string_arg(PARAM_IDX)informa ao analisador e a quem lê o código que um parâmetro específico deve ser uma string terminada em nulo - No exemplo,
example_fn(const char *p)recebe os atributosnull_terminated_string_arg(1)enonnullchar str[3] = "abc";não tem espaço para armazenar o byte de terminação nula- Ao chamar
example_fn(str), o analisador avisa sobre uma leitura excessiva de buffer baseada em pilha de 1 byte após o fim destr - O diagnóstico também inclui uma nota informando que o argumento 1 de
example_fndeve ser um ponteiro para string terminada em nulo
- Esse exemplo pode ser executado no Compiler Explorer
Análise de contaminação ativada por padrão
- A análise de contaminação do analisador rastreia entradas controladas por atacante, pontos de saneamento e pontos de uso sem saneamento
- Nas versões anteriores do GCC, ela não vinha ativada por padrão devido a bugs e muitos falsos positivos, ficando escondida atrás de um argumento de linha de comando separado
- No GCC 14, após várias correções, a análise de contaminação passa a ser ativada por padrão ao selecionar
-fanalyzer - Com essa mudança, os seis avisos baseados em contaminação abaixo também passam a ser ativados
Caso de análise de kernel com a CVE-2011-2210
- Um trecho do kernel Linux referente à CVE-2011-2210 é usado como exemplo de análise de contaminação
- Ao adicionar
__attribute__((tainted_args))à macro__SYSCALL_DEFINEx, o analisador é informado de que os argumentos deosf_getsysinfovieram através de um limite de confiança e devem ser tratados como valores contaminados - O analisador do GCC 14 avisa em
copy_to_user(buffer, hwrpb, nbytes)que o valornbytes, controlado por atacante, está sendo usado como tamanho sem verificação de limite superior- O diagnóstico mostra que
nbytessó recebeu uma verificação de limite inferior emif (nbytes < sizeof(*hwrpb)) - O terceiro parâmetro de
copy_to_useré identificado como parâmetro de tamanho pelo atributoaccess(write_only, 1, 3)
- O diagnóstico mostra que
- O problema está no fato de a condição de saneamento ter sido escrita como
if (nbytes < sizeof(*hwrpb))- A condição pretendida era algo como
if (nbytes > sizeof(*hwrpb)) - Na versão com a condição corrigida, o analisador não emite aviso
- A condição pretendida era algo como
- O trabalho de rodar o analisador no kernel para encontrar vulnerabilidades e corrigir falsos positivos do analisador continua em andamento
1 comentários
Comentários do Hacker News
Para mim, o fanalyzer é um dos recursos matadores do GCC em comparação com o Clang. Ele explica os erros, o que torna programar em C muito mais fácil, e as mensagens de erro começaram até a parecer com as do Rust no sentido de serem amigáveis para desenvolvedores
Um dos maiores motivos para desistir de aprender uma tecnologia costuma ser erro frustrante ou pouco claro. Fugindo um pouco do assunto, eu queria dizer que gosto de C e gostaria ainda mais se ele tivesse mensagens de erro no nível do Rust
error: missing semicolon, às vezes vinham mais de 1000 linhas de mensagens de erro sobre instanciação de templateUsei o analisador do GCC algumas vezes, mas não consegui encontrar um frontend decente que deixasse a saída agradável de ler. O Clang tem várias opções, como uma saída HTML bem razoável, CodeChecker, integração com o Xcode etc.; então fico curioso sobre como vocês leem a saída do lado do GCC. Além disso, o GCC parece gerar muito mais falsos positivos que o Clang
Há mais 36 comentários em outro tópico: https://news.ycombinator.com/item?id=39918278
“GCC 14 Boasts Nice ASCII Art for Visualizing Buffer Overflows (phoronix.com)”, publicado há 2 horas
Alguns meses atrás criei um pequeno utilitário para Linux. Era um shim drop-in para substituir qualquer executável arbitrário; quando chamado, ele fingia ser o programa original, dava fork no original e se conectava ao stdout/stderr dele
A saída de erro era enviada para um assistente GPT personalizado que conhecia o contexto daquele programa, e o assistente reescrevia o erro original em um formato legível por humanos antes de enviá-lo ao stderr do shim. Eu o usava porque estava cansado de olhar dumps aninhados de compilador relacionados a concepts/templates no GCC/Clang, mas, se quisesse, poderia usá-lo com qualquer programa. Funcionava bem, mas fiquei gravemente doente e não consegui continuar trabalhando nisso; parece um bom projeto se alguém quiser refazê-lo direito e generalizá-lo
Seria bom ter um formato de saída melhor para os resultados da análise. Do jeito que está hoje, é um inferno para leitores de tela
Um exemplo da saída pode ser visto aqui: https://godbolt.org/z/aan6Kfxds
Basta adicionar a opção de linha de comando
-fdiagnostics-format=sarif-stderrao primeiro exemplo do post. Também fiz experimentos para gerar os diagramas como SVG, mas não ficaram polidos o suficiente para entrar no GCC 14O código original retornava
-1senbytes < sizeof(*hwrpb), e retornava-2secopy_to_user(buffer, hwrpb, nbytes)falhasse. A correção aplicada foi verificarnbytes > sizeof(*hwrpb), mas, na minha opinião, a correção certa seriacopy_to_user(buffer, hwrpb, sizeof(*hwrpb))Não faz sentido copiar a partir do ponteiro
hwrpbuma quantidade diferente desizeof(*hwrpb)nbytes = 4esizeof(hwrpb)for 16 bytes, então ele vai copiar 12 bytes a mais do buffer do chamador e pode ler memória que não lhe pertence. Acho que isso deve ser evitadoUma solução melhor seria copiar apenas o mínimo de bytes suportado tanto pelo chamador quanto pelo chamado. Algo como
nbytes = MIN(nbytes, sizeof(hwrpb));serviria. Assumindo que a informação de versão emhwrpb->sizeseja respeitada, isso pode garantir compatibilidade retroativa e futura mesmo se parte da struct não estiver inicializadanbytesMuito impressionante. Parece uma quantidade enorme de trabalho. O nível de dificuldade parece comparável ao de introduzir fat pointer/visão de array na biblioteca padrão e no padrão da linguagem C
-Wstringop-overflowtem falsos positivos demais, então é o primeiro aviso que eu desativo. Duvido que uma variante do analisador seja melhorMuito legal. Não faço tanto desenvolvimento em C hoje em dia, então fico me perguntando com que frequência
strcpyestrcatainda são usados. Da última vez que vi, eles eram quase tão tabu quantogotoClaro, sei que no desenvolvimento de kernel
gotocostuma ser preferido. Fico curioso sobre o quanto a análise de strings em C realmente ajudagotoé claramente o jeito certo e elegante. Tentar evitá-lo a qualquer custo pode resultar em código feio, enrolado e difícil de manter. Não é comum, mas tem usos válidosFunções como
strcpysão menos recomendadas, mas também há situações em que se sabe que elas estão corretas, contanto que invariantes mais fortes — por exemplo, invariantes no nível da linguagem — não sejam quebradas. Se esse tipo de coisa falhou, então já existe um problema muito maior. É raro, mas dá até para argumentar que, em alguns casos, uma alternativa nominalmente mais segura pode ser um pouco menos eficiente sem trazer benefíciogotocontinuam sendo idiomáticos em C. É preciso cuidado, mas, enfim, é CJá
longjmpeu não gosto nem um poucogoto. Já a família strxcpy é completamente horrível e não deveria ser usada por motivo nenhum. O fato de ser usada no kernel é assustadorEssas funções e todas as tentativas fracassadas de “corrigi-las” deveriam ter sido lançadas em órbita
gotoé aceitável se usado com cuidado.strcpyestrcattambém são “aceitáveis” no sentido de que você sabe que o código está certo e, se estiver errado, haverá um grande problema. Infelizmente, essa descrição vale para a maior parte do Cgotono mesmo nível de tabu questrcatestrcpy.gotoé ok, estrcatestrcpyusados sem ummallocdo tamanho exato no mesmo escopo são praticamente um code smellMuito bom. Fico feliz que tudo venha com relatórios detalhados explicando o que deu errado