Compartilhar via


Passo a passo: usar AddressSanitizer Continue On Error para encontrar problemas de segurança de memória

Neste passo a passo, crie builds marcados que localizam e relatam erros de segurança de memória.

Erros de segurança de memória, como leituras e gravações de memória fora dos limites, uso de memória depois que ela foi liberada, NULL desreferências de ponteiro e assim por diante, são uma das principais preocupações para o código C/C++. AddressSanitizer (ASAN) é um compilador e uma tecnologia de runtime que expõe esses tipos de bugs difíceis de encontrar e faz isso sem falsos positivos. Para obter uma visão geral do ASAN, consulte AddressSanitizer.

O COE (Continuar em Erro) é um novo recurso ASAN que diagnostica e relata automaticamente erros de segurança de memória à medida que o aplicativo é executado. Quando o programa é encerrado, um resumo dos erros exclusivos de segurança de memória é enviado para stdout, stderrou para um arquivo de log de sua escolha. Quando você cria uma compilação padrão verificada em C++ com -fsanitizer=addresschamadas para alocadores, os alocadores como free, memcpy, memsete assim por diante, são encaminhados para o tempo de execução do ASAN. O runtime ASAN fornece a mesma semântica para essas funções, mas monitora o que acontece com a memória. O ASAN diagnostica e relata erros de segurança de memória ocultos, sem falsos positivos, à medida que o aplicativo é executado.

Uma vantagem significativa do COE é que, ao contrário do comportamento anterior do ASAN, seu programa não para de ser executado quando o primeiro erro de memória é encontrado. Em vez disso, o ASAN observa o erro e seu aplicativo continua a ser executado. Depois que o aplicativo for encerrado, um resumo de todos os problemas de memória será gerado.

É uma boa prática criar um build verificado do seu aplicativo C ou C++ com o ASAN ativado e, em seguida, executar seu aplicativo em seu equipamento de teste. À medida que seus testes exercitam os caminhos de código em seu aplicativo em busca de bugs, você também descobrirá se esses caminhos de código abrigam problemas de segurança de memória sem interferir nos testes.

Quando seu aplicativo terminar, você receberá um resumo dos problemas de memória. Com o COE, você pode compilar e implantar um aplicativo existente em produção limitada para encontrar problemas de segurança de memória. Você pode executar o build verificado por dias para exercitar totalmente o código, embora o aplicativo seja executado mais lentamente devido à instrumentação ASAN.

Você pode usar esse recurso para criar um novo portão de expedição. Se todos os testes existentes forem aprovados, mas o COE relatar um erro de segurança de memória ou um vazamento, não envie o novo código ou integre-o a uma ramificação pai.

Não implante uma compilação com COE habilitado na produção! O COE destina-se a ser usado apenas em ambientes de teste e desenvolvimento. Você não deve usar um build habilitado para ASAN em produção devido ao impacto no desempenho da instrumentação adicionada para detectar erros de memória, o risco de expor a implementação interna se erros forem relatados e para evitar aumentar a área de superfície de possíveis explorações de segurança enviando as funções de biblioteca que o ASAN substitui para alocação de memória, liberando, e assim por diante.

Nos exemplos a seguir, você cria compilações verificadas e define uma variável de ambiente para gerar as informações do sanitizador de endereços para stdout ver os erros de segurança de memória relatados pelo ASAN.

Pré-requisitos

Para concluir este passo a passo, você precisa do Visual Studio 2022 17.6 ou posterior com a carga de trabalho Desenvolvimento da área de trabalho com C++ instalada.

Exemplo duplo livre

Neste exemplo, você cria um build com o ASAN habilitado para testar o que acontece quando a memória é liberada duas vezes. O ASAN detecta esse erro e o relata. Neste exemplo, o programa continua a ser executado depois que o erro é detectado, o que leva a um segundo erro, usando a memória que foi liberada. Um resumo dos erros é enviado para stdout quando o programa é encerrado.

Crie o exemplo:

  1. Abra um prompt de comando do desenvolvedor: abra o menu Iniciar , digite Desenvolvedor e selecione o prompt de comando mais recente, como Prompt de Comando do Desenvolvedor para VS 2022 na lista de correspondências.

  2. Crie um diretório em sua máquina para executar este exemplo. Por exemplo, %USERPROFILE%\Desktop\COE.

  3. Nesse diretório, crie um arquivo de origem vazio. Por exemplo, doublefree.cpp

  4. Cole o seguinte código no arquivo:

    #include <stdio.h>
    #include <stdlib.h>
    
    void BadFunction(int *pointer)
    {
        free(pointer);
        free(pointer); // double-free!
    }
    
    int main(int argc, const char *argv[])
    {
        int *pointer = static_cast<int *>(malloc(4));
        BadFunction(pointer);
    
        // Normally we'd crash before this, but with COE we can see heap-use-after-free error as well
        printf("\n\n******* Pointer value: %d\n", *pointer);
    
        return 1;
    }
    

No código anterior, pointer é liberado duas vezes. Este é um exemplo artificial, mas liberações duplas são um erro fácil de cometer em código C++ mais complexo.

Crie uma compilação do código anterior com o COE ativado com as seguintes etapas:

  1. Compile o código no prompt de comando do desenvolvedor que você abriu anteriormente: cl -fsanitize=address -Zi doublefree.cpp. O -fsanitize=address comutador ativa o ASAN e -Zi cria um arquivo PDB separado que o AddressSanitizer usa para exibir informações de local de erro de memória.
  2. Envie a saída ASAN para stdout definindo a ASAN_OPTIONS variável de ambiente no prompt de comando do desenvolvedor da seguinte maneira: set ASAN_OPTIONS=continue_on_error=1
  3. Execute o código de teste com: doublefree.exe

A saída mostra que houve um erro de liberação dupla e a pilha de chamadas em que isso aconteceu. O relatório começa com uma pilha de chamadas que mostra que o erro ocorreu em BadFunction:

==22976==ERROR: AddressSanitizer: attempting double-free on 0x01e03550 in thread T0:
    #0  free                           D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp(69)
    #1  BadFunction                    C:\Users\xxx\Desktop\COE\doublefree.cpp(8)
    #2  main                           C:\Users\xxx\Desktop\COE\doublefree.cpp(14)
    #3  __scrt_common_main_seh         D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
    #4  BaseThreadInitThunk            Windows
    #5  RtlInitializeExceptionChain    Windows

Em seguida, há informações sobre a memória liberada e uma pilha de chamadas para onde a memória foi alocada:

0x01e03550 is located 0 bytes inside of 4-byte region [0x01e03550,0x01e03554)
freed by thread T0 here:
    #0  free                           D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp(69)
    #1  BadFunction                    C:\Users\xxx\Desktop\COE\doublefree.cpp(7)
    #2  main                           C:\Users\xxx\Desktop\COE\doublefree.cpp(14)
    #3  __scrt_common_main_seh         D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
    #4  BaseThreadInitThunk            Windows
    #5  RtlInitializeExceptionChain    Windows

previously allocated by thread T0 here:
    #0  malloc                         D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp(85)
    #1  main                           C:\Users\xxx\Desktop\COE\doublefree.cpp(13)
    #2  __scrt_common_main_seh         D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
    #3  BaseThreadInitThunk            Windows
    #4  RtlInitializeExceptionChain    Windows

Em seguida, há informações sobre o erro heap-use-after-free. Isso se refere ao uso *pointer na printf() chamada porque a memória pointer se refere a foi liberada anteriormente. A pilha de chamadas em que ocorre o erro é listada, assim como as pilhas de chamadas em que essa memória foi alocada e liberada:

==35680==ERROR: AddressSanitizer: heap-use-after-free on address 0x02a03550 at pc 0x00e91097 bp 0x012ffc64 sp 0x012ffc58READ of size 4 at 0x02a03550 thread T0
         #0  main                           C:\Users\xxx\Desktop\Projects\ASAN\doublefree.cpp(18)
         #1  __scrt_common_main_seh         D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
         #2  BaseThreadInitThunk            Windows
         #3  RtlInitializeExceptionChain    Windows

0x02a03550 is located 0 bytes inside of 4-byte region [0x02a03550,0x02a03554)
freed by thread T0 here:
         #0  free                           D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp(69)
         #1  BadFunction                    C:\Users\xxx\Desktop\Projects\ASAN\doublefree.cpp(7)
         #2  main                           C:\Users\xxx\Desktop\Projects\ASAN\doublefree.cpp(14)
         #3  __scrt_common_main_seh         D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
         #4  BaseThreadInitThunk            Windows
         #5  RtlInitializeExceptionChain    Windows

previously allocated by thread T0 here:
         #0  malloc                         D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp(85)
         #1  main                           C:\Users\xxx\Desktop\Projects\ASAN\doublefree.cpp(13)
         #2  __scrt_common_main_seh         D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
         #3  BaseThreadInitThunk            Windows
         #4  RtlInitializeExceptionChain    Windows

Em seguida, há informações sobre os bytes de sombra nas proximidades do estouro do buffer. Para obter mais informações sobre bytes de sombra, consulte Bytes-sombra do AddressSanitizer.

Seguindo as informações de byte de sombra, você verá a saída do programa, o que indica que ele continuou em execução depois que o ASAN detectou o erro:

******* Pointer value: xxx

Em seguida, há um resumo dos arquivos de origem em que ocorreu o erro de memória. Ele é classificado pelas pilhas de chamadas exclusivas para os erros de memória nesse arquivo. Uma pilha de chamadas exclusiva é determinada pelo tipo de erro e pela pilha de chamadas em que o erro ocorreu.

Essa classificação prioriza os problemas de segurança de memória que podem ser os mais preocupantes. Por exemplo, cinco pilhas de chamadas exclusivas que levam a diferentes erros de segurança de memória no mesmo arquivo são potencialmente mais preocupantes do que um erro que ocorre muitas vezes. O resumo é assim:

=== Files in priority order ===

File: D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp Unique call stacks: 1
File: C:\Users\xxx\Desktop\COE\doublefree.cpp Unique call stacks: 1

Por fim, o relatório contém um resumo de onde ocorreram os erros de memória:

=== Source Code Details: Unique errors caught at instruction offset from source line number, in functions, in the same file. ===

File: D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp
        Func: free()
                Line: 69 Unique call stacks (paths) leading to error at line 69 : 1
                        Bug: double-free at instr 19 bytes from start of line
File: C:\Users\xxx\Desktop\COE\doublefree.cpp
        Func: main()
                Line: 18 Unique call stacks (paths) leading to error at line 18 : 1
                        Bug: heap-use-after-free at instr 55 bytes from start of line

>>>Total: 2 Unique Memory Safety Issues (based on call stacks not source position) <<<

#0 C:\Users\xxx\Desktop\COE\doublefree.cpp Function: main(Line:18)
        Raw HitCnt: 1  On Reference: 4-byte-read-heap-use-after-free
#1 D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp Function: free(Line:69)
        Raw HitCnt: 1

Exemplo de acesso à memória fora dos limites

Neste exemplo, você cria um build com o ASAN habilitado para testar o que acontece quando um aplicativo acessa a memória que está fora dos limites. O ASAN detecta esse erro e relata um resumo dos erros para stdout quando o programa é encerrado.

Crie o exemplo:

  1. Abra um prompt de comando do desenvolvedor: abra o menu Iniciar , digite Desenvolvedor e selecione o prompt de comando mais recente, como Prompt de Comando do Desenvolvedor para VS 2022 na lista de correspondências.

  2. Crie um diretório em sua máquina para executar este exemplo. Por exemplo, %USERPROFILE%\Desktop\COE.

  3. Nesse diretório, crie um arquivo de origem, por exemplo, coe.cppe cole o seguinte código:

    #include <stdlib.h> 
    
    char* func(char* buf, size_t sz)
    { 
        char* local = (char*)malloc(sz); 
        for (auto ii = 0; ii <= sz; ii++) // bad loop exit test 
        {
            local[ii] = ~buf[ii]; // Two memory safety errors 
        }
    
        return local; 
    } 
    
    char buffer[10] = {0,1,2,3,4,5,6,7,8,9}; 
    
    int main()
    {   
        char* inverted_buf= func(buffer, 10); 
    }
    

No código anterior, o parâmetro sz é 10 e o buffer original é 10 bytes. Há dois erros de segurança de memória:

  • uma carga fora dos limites do buf loop for
  • um armazenamento fora dos limites para local no for loop

O estouro do buffer é devido ao teste <=szde saída do loop . Quando este exemplo é executado, ele é seguro por coincidência. Isso ocorre devido à alocação excessiva e ao alinhamento feitos pela maioria das implementações de tempo de execução do C++. Quando sz % 16 == 0, a gravação final corrompe a local[ii] memória. Outros casos apenas lêem/gravam no "malloc slop", que é a memória extra alocada devido à maneira como o C Runtime (CRT) preenche as alocações para um limite 0 mod 16.

Os erros só são observáveis se a página após a alocação não for mapeada ou após o uso de dados corrompidos. Todos os outros casos são silenciosos neste exemplo. Com Continuar em caso de erro, os erros ficam visíveis no resumo após a conclusão do programa.

Crie uma compilação do código anterior com o COE ativado:

  1. Compile o código com cl -fsanitize=address -Zi coe.cpp. O -fsanitize=address comutador ativa o ASAN e -Zi cria um arquivo PDB separado que o AddressSanitizer usa para exibir informações de local de erro de memória.
  2. Envie a saída ASAN para stdout definindo a ASAN_OPTIONS variável de ambiente no prompt de comando do desenvolvedor da seguinte maneira: set ASAN_OPTIONS=continue_on_error=1
  3. Execute o código de teste com: coe.exe

A saída mostra que houve dois erros de estouro de buffer de memória e fornece a pilha de chamadas para onde eles ocorreram. O relatório começa assim:

==9776==ERROR: AddressSanitizer: global-buffer-overflow on address 0x0047b08a at pc 0x003c121b bp 0x012ffaec sp 0x012ffae0
READ of size 1 at 0x0047b08a thread T0
	 #0  func                           C:\Users\xxx\Desktop\COE\coe.cpp(8)
	 #1  main                           C:\Users\xxx\Desktop\COE\coe.cpp(18)
	 #2  __scrt_common_main_seh         D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl(288)
	 #3  BaseThreadInitThunk            Windows
	 #4  RtlInitializeExceptionChain    Windows

Em seguida, há informações sobre os bytes de sombra nas proximidades do estouro do buffer. Para obter mais informações sobre bytes de sombra, consulte Bytes-sombra do AddressSanitizer.

Após o relatório de byte de sombra, há um resumo dos arquivos de origem em que ocorreram os erros de memória. Ele é classificado pelas pilhas de chamadas exclusivas para os erros de memória nesse arquivo. Uma pilha de chamadas exclusiva é determinada pelo tipo de erro e pela pilha de chamadas em que o erro ocorreu.

Essa classificação prioriza os problemas de segurança de memória que podem ser os mais preocupantes. Por exemplo, cinco pilhas de chamadas exclusivas que levam a diferentes erros de segurança de memória no mesmo arquivo são potencialmente mais preocupantes do que um erro que ocorre muitas vezes.

O resumo é assim:

=== Files in priority order ===

File: C:\Users\xxx\Desktop\COE\coe.cpp Unique call stacks: 2

Por fim, o relatório contém um resumo de onde ocorreram os erros de memória. Continuar em caso de erro relata dois erros distintos que ocorrem na mesma linha de origem. O primeiro erro lê a memória em um endereço global na .data seção e o outro grava na memória alocada do heap.

O relatório é assim:

=== Source Code Details: Unique errors caught at instruction offset from source line number, in functions, in the same file. === 

File: C:\Users\xxx\Desktop\COE\coe.cpp 
	Func: func()
		Line: 8 Unique call stacks (paths) leading to error at line 8 : 2
			Bug: heap-buffer-overflow at instr 124 bytes from start of line

>>>Total: 2 Unique Memory Safety Issues (based on call stacks not source position) <<<

#0 C:\Users\xxx\Desktop\COE\coe.cpp Function: func(Line:8) 
	Raw HitCnt: 1  On Reference: 1-byte-read-global-buffer-overflow 
#1 C:\Users\xxx\Desktop\COE\coe.cpp Function: func(Line:8) 
	Raw HitCnt: 1  On Reference: 1-byte-write-heap-buffer-overflow 

O comportamento de runtime do AddressSanitizer padrão encerra o aplicativo depois de relatar o primeiro erro encontrado. Ele não permite que a instrução "ruim" da máquina seja executada. O novo runtime do AddressSanitizer diagnostica e relata erros, mas executa instruções subsequentes.

O COE tenta retornar automaticamente o controle de volta ao aplicativo depois de relatar cada erro de segurança de memória. Há situações em que não pode, como quando há uma AV (violação de acesso à memória) ou uma alocação de memória com falha. O COE não continua após violações de acesso que o tratamento de exceção estruturado do programa não captura. Se o COE não puder retornar a execução para o aplicativo, uma CONTINUE CANCELLED - Deadly Signal. Shutting down. mensagem será gerada.

Selecione para onde enviar a saída ASAN

Use a variável de ambiente para determinar para onde enviar a ASAN_OPTIONS saída ASAN da seguinte maneira:

  • Saída para stdout: set ASAN_OPTIONS=continue_on_error=1
  • Saída para stderr: set ASAN_OPTIONS=continue_on_error=2
  • Saída para um arquivo de log de sua escolha: set COE_LOG_FILE=yourfile.log

Manipulando comportamento indefinido

O runtime ASAN não imita todos os comportamentos indefinidos das funções de alocação/desalocação C e C++. O exemplo a seguir demonstra como a versão ASAN do _alloca difere da versão de runtime C:

#include <cstdio>
#include <cstring>
#include <malloc.h>
#include <excpt.h>
#include <windows.h>

#define RET_FINISH 0
#define RET_STACK_EXCEPTION 1
#define RET_OTHER_EXCEPTION 2

int foo_redundant(unsigned long arg_var)
{
    char *a;
    int ret = -1;

    __try
    {
        if ((arg_var+3) > arg_var)
        {
            // Call to _alloca using parameter from main
            a = (char *) _alloca(arg_var);
            memset(a, 0, 10);
        }
        ret = RET_FINISH;
    }
    __except(1)
    {
        ret = RET_OTHER_EXCEPTION;
        int i = GetExceptionCode();
        if (i == EXCEPTION_STACK_OVERFLOW)
        {
            ret = RET_STACK_EXCEPTION;
        }
    }
    return ret;
}

int main()
{
    int cnt = 0;

    if (foo_redundant(0xfffffff0) == RET_STACK_EXCEPTION)
    {
        cnt++;
    }

    if (cnt == 1)
    {
        printf("pass\n");
    }
    else
    {
        printf("fail\n");
    }
}

Em main() um grande número é passado para foo_redundant, que é finalmente passado para _alloca(), o que faz com _alloca() que falhe.

Este exemplo é pass gerado quando compilado sem ASAN (ou seja, sem -fsanitize=address opção), mas é gerado fail quando compilado com o ASAN ativado (ou seja, com o -fsanitize=address comutador). Isso ocorre porque, sem ASAN, o código de exceção corresponde RET_STACK_EXCEPTION , portanto cnt , é definido como 1. Ele se comporta de forma diferente quando compilado com ASAN ativado porque a exceção gerada é um erro AddressSanitizer em vez disso: dynamic-stack-buffer-overflow. Isso significa que o código retorna RET_OTHER_EXCEPTION em vez de RET_STACK_EXCEPTION so cnt não está definido como 1.

Outros benefícios

Com o novo runtime ASAN, nenhum binário extra precisa ser implantado com seu aplicativo. Isso facilita ainda mais o uso do ASAN com seu equipamento de teste normal, pois você não precisa gerenciar binários extras.

Confira também

Postagem no blog AddressSanitizer Continuar com erro
Exemplo de erros de segurança de memória
Sinalizador do compilador -Zi
-fsanitize=sinalizador do compilador de endereços
Os 25 pontos fracos de software mais perigosos