Ponteiro pendente - Dangling pointer
Ponteiros oscilantes e ponteiros selvagens em programação de computador são ponteiros que não apontam para um objeto válido do tipo apropriado. Esses são casos especiais de violações de segurança de memória . De modo mais geral, referências pendentes e referências selvagens são referências que não resolvem para um destino válido e incluem fenômenos como o apodrecimento de links na Internet.
Ponteiros pendentes surgem durante a destruição do objeto , quando um objeto que tem uma referência de entrada é excluído ou desalocado, sem modificar o valor do ponteiro, de modo que o ponteiro ainda aponte para a localização da memória desalocada. O sistema pode realocar a memória liberada anteriormente e, se o programa desreferenciar o (agora) ponteiro pendente, pode ocorrer um comportamento imprevisível , pois a memória agora pode conter dados completamente diferentes. Se o programa gravar na memória referenciado por um ponteiro pendurado, pode ocorrer uma corrupção silenciosa de dados não relacionados, levando a erros sutis que podem ser extremamente difíceis de encontrar. Se a memória foi realocada para outro processo, tentar cancelar a referência do ponteiro pendente pode causar falhas de segmentação (UNIX, Linux) ou falhas de proteção geral (Windows). Se o programa tiver privilégios suficientes para permitir que ele sobrescreva os dados de contabilidade usados pelo alocador de memória do kernel, a corrupção pode causar instabilidades no sistema. Em linguagens orientadas a objetos com coleta de lixo , as referências pendentes são evitadas destruindo apenas objetos inacessíveis, o que significa que eles não têm nenhum ponteiro de entrada; isso é garantido por rastreamento ou contagem de referência . No entanto, um finalizador pode criar novas referências a um objeto, exigindo a ressurreição do objeto para evitar uma referência pendente.
Wild pointers surgem quando um ponteiro é usado antes da inicialização para algum estado conhecido, o que é possível em algumas linguagens de programação. Eles mostram o mesmo comportamento errático dos ponteiros pendentes, embora sejam menos propensos a não serem detectados porque muitos compiladores irão emitir um aviso em tempo de compilação se as variáveis declaradas forem acessadas antes de serem inicializadas.
Causa de ponteiros pendentes
Em muitas linguagens (por exemplo, a linguagem de programação C ), excluir um objeto da memória explicitamente ou destruir o quadro de pilha no retorno não altera os ponteiros associados. O ponteiro ainda aponta para o mesmo local na memória, embora agora possa ser usado para outros fins.
Um exemplo simples é mostrado abaixo:
{
char *dp = NULL;
/* ... */
{
char c;
dp = &c;
}
/* c falls out of scope */
/* dp is now a dangling pointer */
}
Se o sistema operacional é capaz de detectar referências em tempo de execução para ponteiros nulos , uma solução para o acima é atribuir 0 (nulo) a dp imediatamente antes do bloco interno ser encerrado. Outra solução seria garantir de alguma forma que o dp não seja usado novamente sem uma inicialização posterior.
Outra fonte frequente de ponteiros pendentes é uma combinação confusa de chamadas de biblioteca malloc()
e free()
: um ponteiro fica pendurado quando o bloco de memória para o qual ele aponta é liberado. Como no exemplo anterior, uma maneira de evitar isso é certificar-se de redefinir o ponteiro para nulo depois de liberar sua referência - conforme demonstrado abaixo.
#include <stdlib.h>
void func()
{
char *dp = malloc(A_CONST);
/* ... */
free(dp); /* dp now becomes a dangling pointer */
dp = NULL; /* dp is no longer dangling */
/* ... */
}
Um erro muito comum é retornar endereços de uma variável local alocada na pilha: uma vez que uma função chamada retorna, o espaço para essas variáveis é desalocado e, tecnicamente, elas têm "valores de lixo".
int *func(void)
{
int num = 1234;
/* ... */
return #
}
As tentativas de leitura do ponteiro ainda podem retornar o valor correto (1234) por um tempo após a chamada func
, mas quaisquer funções chamadas depois disso podem sobrescrever o armazenamento da pilha alocado para num
outros valores e o ponteiro não funcionaria mais corretamente. Se um ponteiro para num
deve ser retornado, num
deve ter escopo além da função - ele pode ser declarado como static
.
Desalocação manual sem referência pendente
Antoni Kreczmar (1945-1996) criou um sistema completo de gerenciamento de objetos que está livre de fenômenos de referência pendentes, consulte
- Esquema de axiomas da operação matar
- Sejam x 1 , ..., x n variáveis, n> 0, 1≤i≤n. Cada fórmula do esquema a seguir é um teorema da máquina virtual construída por Kreczmar.
- Sejam x 1 , ..., x n variáveis, n> 0, 1≤i≤n. Cada fórmula do esquema a seguir é um teorema da máquina virtual construída por Kreczmar.
- leia como : se um objeto o é o valor de n variáveis, então após a execução da instrução kill (x i ) o valor comum dessas variáveis é nenhum (isso significa que a partir deste momento o objeto o é inalcançável e, conseqüentemente, a parte do a memória ocupada por ele pode ser pela mesma operação de eliminação reciclada sem nenhum dano).
Consequentemente:
- não há necessidade de repetir a operação kill (x 1 ), kill (x 2 ), ...
- não há fenômeno de referência pendente ,
- qualquer tentativa de acessar o objeto excluído, será detectada e sinalizada como uma exceção “ referência a nenhum ”.
Nota: o custo de matar é constante .
Uma abordagem semelhante foi proposta por Fisher e LeBlanc sob o nome de Locks-and-keys .
Causa de ponteiros selvagens
Os ponteiros selvagens são criados omitindo a inicialização necessária antes do primeiro uso. Assim, estritamente falando, todo ponteiro em linguagens de programação que não obrigam a inicialização começa como um ponteiro selvagem.
Na maioria das vezes, isso ocorre devido ao salto sobre a inicialização, não por omiti-la. A maioria dos compiladores é capaz de alertar sobre isso.
int f(int i)
{
char *dp; /* dp is a wild pointer */
static char *scp; /* scp is not a wild pointer:
* static variables are initialized to 0
* at start and retain their values from
* the last call afterwards.
* Using this feature may be considered bad
* style if not commented */
}
Falhas de segurança envolvendo ponteiros pendentes
Assim como os bugs de estouro de buffer, os bugs pendentes / wild pointer frequentemente se tornam falhas de segurança. Por exemplo, se o ponteiro for usado para fazer uma chamada de função virtual , um endereço diferente (possivelmente apontando para o código de exploração) pode ser chamado devido ao ponteiro vtable ser sobrescrito. Como alternativa, se o ponteiro for usado para gravar na memória, alguma outra estrutura de dados pode estar corrompida. Mesmo se a memória só for lida quando o ponteiro ficar pendente, isso pode levar a vazamentos de informações (se dados interessantes forem colocados na próxima estrutura alocada lá) ou ao aumento de privilégios (se a memória agora inválida for usada em verificações de segurança). Quando um ponteiro pendente é usado após ter sido liberado sem alocar um novo bloco de memória para ele, isso se torna conhecido como uma vulnerabilidade de "uso após liberação". Por exemplo, CVE - 2014-1776 é uma vulnerabilidade use after-free no Microsoft Internet Explorer 6 a 11 sendo usada por ataques de dia zero por uma ameaça persistente avançada .
Evitando erros de ponteiro pendurado
Em C, a técnica mais simples é implementar uma versão alternativa da função free()
(ou semelhante) que garanta o reset do ponteiro. No entanto, essa técnica não limpará outras variáveis de ponteiro que podem conter uma cópia do ponteiro.
#include <assert.h>
#include <stdlib.h>
/* Alternative version for 'free()' */
static void safefree(void **pp)
{
/* in debug mode, abort if pp is NULL */
assert(pp);
/* free(NULL) works properly, so no check is required besides the assert in debug mode */
free(*pp); /* deallocate chunk, note that free(NULL) is valid */
*pp = NULL; /* reset original pointer */
}
int f(int i)
{
char *p = NULL, *p2;
p = malloc(1000); /* get a chunk */
p2 = p; /* copy the pointer */
/* use the chunk here */
safefree((void **)&p); /* safety freeing; does not affect p2 variable */
safefree((void **)&p); /* this second call won't fail as p is reset to NULL */
char c = *p2; /* p2 is still a dangling pointer, so this is undefined behavior. */
return i + c;
}
A versão alternativa pode ser usada até mesmo para garantir a validade de um ponteiro vazio antes de chamar malloc()
:
safefree(&p); /* i'm not sure if chunk has been released */
p = malloc(1000); /* allocate now */
Esses usos podem ser mascarados por meio de #define
diretivas para construir macros úteis (sendo um comum #define XFREE(ptr) safefree((void **)&(ptr))
), criando algo como uma metalinguagem ou podem ser incorporados em uma biblioteca de ferramentas à parte. Em todos os casos, os programadores que usam essa técnica devem usar as versões seguras em todas as instâncias em free()
que seriam usadas; deixar de fazê-lo leva novamente ao problema. Além disso, essa solução é limitada ao escopo de um único programa ou projeto e deve ser devidamente documentada.
Entre as soluções mais estruturadas, uma técnica popular para evitar ponteiros pendentes em C ++ é usar ponteiros inteligentes . Um ponteiro inteligente normalmente usa contagem de referência para recuperar objetos. Algumas outras técnicas incluem o método de marcas para exclusão e o método de bloqueios e chaves .
Outra abordagem é usar o coletor de lixo Boehm , um coletor de lixo conservador que substitui as funções de alocação de memória padrão em C e C ++ por um coletor de lixo. Essa abordagem elimina completamente os erros de ponteiro pendurado desabilitando liberações e recuperando objetos por coleta de lixo.
Em linguagens como Java, ponteiros pendentes não podem ocorrer porque não há mecanismo para desalocar explicitamente a memória. Em vez disso, o coletor de lixo pode desalocar memória, mas apenas quando o objeto não estiver mais acessível a partir de quaisquer referências.
Na linguagem Rust , o sistema de tipos foi estendido para incluir também as variáveis tempos de vida e aquisição de recursos é inicialização . A menos que um desabilite os recursos da linguagem, ponteiros pendentes serão capturados no tempo de compilação e relatados como erros de programação.
Detecção de ponteiro oscilante
Para expor erros de ponteiro pendurado, uma técnica de programação comum é definir ponteiros para o ponteiro nulo ou para um endereço inválido assim que o armazenamento para o qual eles apontam for liberado. Quando o ponteiro nulo é desreferenciado (na maioria das linguagens), o programa termina imediatamente - não há potencial para corrupção de dados ou comportamento imprevisível. Isso torna o erro de programação subjacente mais fácil de localizar e resolver. Essa técnica não ajuda quando há várias cópias do ponteiro.
Alguns depuradores irá dados automaticamente sobrescrever e destruir que tem sido libertados, geralmente com um padrão específico, tais como 0xDEADBEEF
(Visual C da Microsoft / depurador C ++, por exemplo, usa 0xCC
, 0xCD
ou 0xDD
dependendo do que foi libertado). Isso geralmente impede que os dados sejam reutilizados, tornando-os inúteis e também muito proeminentes (o padrão serve para mostrar ao programador que a memória já foi liberada).
Ferramentas como Polyspace , TotalView , Valgrind , Mudflap, AddressSanitizer ou ferramentas baseadas em LLVM também podem ser usadas para detectar o uso de ponteiros pendentes.
Outras ferramentas ( SoftBound , Insure ++ e CheckPointer ) instrumentam o código-fonte para coletar e rastrear valores legítimos para ponteiros ("metadados") e verificar cada acesso de ponteiro em relação aos metadados para validade.
Outra estratégia, ao suspeitar de um pequeno conjunto de classes, é tornar temporariamente todas as suas funções-membro virtuais : depois que a instância da classe foi destruída / liberada, seu ponteiro para a Tabela de Método Virtual é definido como NULL
, e qualquer chamada para uma função-membro travar o programa e ele mostrará o código culpado no depurador.
Outros usos
O termo ponteiro pendurado também pode ser usado em contextos diferentes de programação, especialmente por técnicos. Por exemplo, o número de telefone de uma pessoa que mudou de telefone é um exemplo do mundo real de um ponteiro pendurado. Outro exemplo é uma entrada em uma enciclopédia online que se refere a outra entrada cujo título foi alterado, transformando todas as referências existentes a essa entrada em ponteiros pendentes.