Ponteiro pendente - Dangling pointer

Ponteiro pendurado

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 &num;
}

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 numoutros valores e o ponteiro não funcionaria mais corretamente. Se um ponteiro para numdeve ser retornado, numdeve ter escopo além da função - ele pode ser declarado como static.

Desalocação manual sem referência pendente

Antoni Kreczmar  [ pl ] (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.
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 #definediretivas 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, 0xCDou 0xDDdependendo 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.

Veja também

Referências