Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
Neste passo a passo, crie compilações verificadas que localizem e relatem 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 de 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) é uma tecnologia de compilador e tempo de execução que expõe esses tipos de bugs difíceis de encontrar e faz isso com zero falsos positivos. Para obter uma visão geral do ASAN, consulte AddressSanitizer.
Continue On Error (COE) é um novo recurso ASAN que diagnostica e relata automaticamente erros de segurança de memória à medida que seu aplicativo é executado. Quando o programa é encerrado, um resumo dos erros de segurança de memória exclusivos é 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=address, as chamadas para alocatadores, deallocators como free, memcpy, memsete assim por diante, são encaminhadas para o tempo de execução ASAN. O tempo de execução 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, com zero falsos positivos, à medida que seu aplicativo é executado.
Uma vantagem significativa do COE é que, ao contrário do comportamento ASAN anterior, 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 seu aplicativo é encerrado, um resumo de todos os problemas de memória é a saída.
É uma boa prática criar uma compilação verificada do seu aplicativo C ou C++ com o ASAN ativado e, em seguida, executar seu aplicativo em seu conjunto de teste. À medida que seus testes exercitam os caminhos de código em seu aplicativo procurando 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 a compilação verificada por dias para exercer 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 envio. Se todos os testes existentes passarem, 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 o COE habilitado em produção! O COE destina-se a ser usado apenas em ambientes de teste e desenvolvimento. Você não deve usar uma compilação habilitada para ASAN em produção devido ao impacto no desempenho da instrumentação adicionada para detetar 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 limpador de endereço para stdout ver os erros de segurança de memória que o ASAN relata.
Pré-requisitos
Para concluir este passo a passo, você precisa do Visual Studio 2022 17.6 ou posterior com o de trabalho de desenvolvimento do
Exemplo duplo gratuito
Neste exemplo, você cria uma compilação com ASAN habilitado para testar o que acontece quando a memória é liberada duplamente. O ASAN deteta esse erro e o relata. Neste exemplo, o programa continua a ser executado depois que o erro é detetado, o que leva a um segundo erro - usando a memória que foi liberada. Um resumo dos erros é a saída para stdout quando o programa é encerrado.
Crie o exemplo:
Abra um prompt de comando do desenvolvedor: abra o menu Iniciar, digite Developere selecione o prompt de comando mais recente, como Prompt de Comando do Desenvolvedor para VS 2022 na lista de correspondências.
Crie um diretório em sua máquina para executar este exemplo. Por exemplo,
%USERPROFILE%\Desktop\COE.Nesse diretório, crie um arquivo de origem vazio. Por exemplo,
doublefree.cppCole 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 inventado, mas liberações duplas são um erro fácil de cometer em códigos C++ mais complexos.
Crie uma compilação do código anterior com o COE ativado com as seguintes etapas:
- Compile o código no prompt de comando do desenvolvedor que você abriu anteriormente:
cl -fsanitize=address -Zi doublefree.cpp. O-fsanitize=addressswitch ativa o ASAN e-Zicria um arquivo PDB separado que o AddressSanitizer usa para exibir informações de local de erro de memória. - Envie a saída ASAN para
stdoutdefinindo a variável de ambienteASAN_OPTIONSno prompt de comando do desenvolvedor da seguinte maneira:set ASAN_OPTIONS=continue_on_error=1 - Execute o código de teste com:
doublefree.exe
A saída mostra que houve um erro livre duplo e a pilha de chamadas onde aconteceu. O relatório começa com uma pilha de chamadas que mostra que o erro aconteceu 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 de *pointer na chamada printf() porque a memória a que pointer se refere foi liberada anteriormente. A pilha de chamadas onde o erro ocorre é listada, assim como as pilhas de chamadas onde 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 de buffer. Para obter mais informações sobre bytes de sombra, consulte AddressSanitizer shadow bytes.
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 detetou o erro:
******* Pointer value: xxx
Em seguida, há um resumo dos arquivos de origem onde o erro de memória aconteceu. 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 onde o erro ocorreu.
Essa classificação prioriza 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 é potencialmente mais preocupante do que um erro que atinge muitas vezes. O resumo tem a seguinte aparência:
=== 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
Finalmente, 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 uma compilação com ASAN habilitado para testar o que acontece quando um aplicativo acessa a memória fora dos limites. ASAN deteta esse erro e relata um resumo dos erros para stdout quando o programa é encerrado.
Crie o exemplo:
Abra um prompt de comando do desenvolvedor: abra o menu Iniciar, digite Developere selecione o prompt de comando mais recente, como Prompt de Comando do Desenvolvedor para VS 2022 na lista de correspondências.
Crie um diretório em sua máquina para executar este exemplo. Por exemplo,
%USERPROFILE%\Desktop\COE.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. Existem dois erros de segurança de memória:
- uma carga fora dos limites de
bufno loop defor - um armazenamento fora dos limites para
localno loopfor
O estouro do buffer é devido ao teste de saída de loop <=sz. Quando este exemplo é executado, ele é seguro por coincidência. Isso ocorre devido ao excesso de alocação e alinhamento feito pela maioria das implementações de tempo de execução C++. Quando sz % 16 == 0, a gravação final em local[ii] corrompe a memória. Outros casos só leem/gravam no "malloc slop", que é memória extra alocada devido à maneira como o C Runtime (CRT) aloca para um limite 0 mod 16.
Os erros só são observáveis se a página seguinte à alocação não estiver mapeada ou após o uso de dados corrompidos. Todos os outros casos são omissos neste exemplo. Com Continue On Error, os erros são tornados visíveis no resumo após a execução do programa até a conclusão.
Crie uma compilação do código anterior com o COE ativado:
- Compile o código com
cl -fsanitize=address -Zi coe.cpp. O-fsanitize=addressswitch ativa o ASAN e-Zicria um arquivo PDB separado que o AddressSanitizer usa para exibir informações de local de erro de memória. - Envie a saída ASAN para
stdoutdefinindo a variável de ambienteASAN_OPTIONSno prompt de comando do desenvolvedor da seguinte maneira:set ASAN_OPTIONS=continue_on_error=1 - 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 aconteceram. 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 de buffer. Para obter mais informações sobre bytes de sombra, consulte AddressSanitizer shadow bytes.
Após o relatório de bytes de sombra, há um resumo dos arquivos de origem onde os erros de memória aconteceram. 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 onde o erro ocorreu.
Essa classificação prioriza 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 é potencialmente mais preocupante do que um erro que atinge muitas vezes.
O resumo tem a seguinte aparência:
=== Files in priority order ===
File: C:\Users\xxx\Desktop\COE\coe.cpp Unique call stacks: 2
Finalmente, o relatório contém um resumo de onde ocorreram os erros de memória. Continue On Error relata dois erros distintos que ocorrem na mesma linha de origem. O primeiro erro lê a memória em um endereço global na seção .data e o outro grava na memória alocada a partir da pilha.
O relatório tem a seguinte aparência:
=== 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 padrão de tempo de execução AddressSanitizer 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 tempo de execução do AddressSanitizer diagnostica e relata erros, mas executa instruções subsequentes.
O COE tenta retornar automaticamente o controle ao aplicativo depois de relatar cada erro de segurança de memória. Há situações em que não é possível, como quando há uma violação de acesso à memória (AV) 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ções estruturadas do programa não deteta. Se o COE não puder retornar a execução para o aplicativo, uma mensagem de CONTINUE CANCELLED - Deadly Signal. Shutting down. será exibida.
Selecione para onde enviar a saída ASAN
Use a variável de ambiente ASAN_OPTIONS para determinar para onde enviar a 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
Lidando com comportamento indefinido
O tempo de execução 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 tempo de execução 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 acaba sendo passado para _alloca(), o que faz com que _alloca() falhe.
Este exemplo produz pass quando compilado sem ASAN (ou seja, sem -fsanitize=address switch), mas as saídas fail quando compiladas com ASAN ativado (ou seja, com a opção -fsanitize=address). Isso porque, sem ASAN, o código de exceção corresponde a RET_STACK_EXCEPTION portanto, cnt é definido como 1. Ele se comporta de forma diferente quando compilado com ASAN on porque a exceção lançada é um erro AddressSanitizer: dynamic-stack-buffer-overflow. Isso significa que o código retorna RET_OTHER_EXCEPTION em vez de RET_STACK_EXCEPTION para que cnt não esteja definido como 1.
Outros benefícios
Com o novo tempo de execução ASAN, nenhum binário extra precisa ser implantado com seu aplicativo. Isso torna ainda mais fácil usar o ASAN com seu chicote de teste normal, porque você não precisa gerenciar binários extras.
Ver também
AddressSanitizer Continue na postagem do blog Error
Exemplo de erros de segurança de memória
-Zi sinalizador do compilador
-fsanitize=address sinalizador do compilador
As 25 fraquezas de software mais perigosas