Buffer overrun

Um dos argumentos de pessoas que não gostam muito de C/C++ é a possibilidade de ocorrerem buffer overruns. O termo refere-se ao acesso a arrays ou blocos de memória fora dos limites pré-estabelecidos. Por exemplo:

char a[10], *p = a;
*(p + 10) = 'x';

Acima temos o acesso ao 11º item do array ‘a’ (que não deveria ser possível, já que o array tem 10 chars!). Acontece que o uso de ponteiros pressupõe duas coisas:

  1. Que vocẽ saiba o que está fazendo;
  2. Ponteiros podem, em teoria, apontar para qualquer região da memória alocada para o processo.

Diferente do que você possa pensar, no exemplo acima, não são alocados apenas 18 bytes (10 para o array, 4 para o ponteiro ‘a’ e 4 para o ponteiro ‘p’)… A alocação dos segmentos de memória usados por processos depende muito da arquitetura do computador. No caso, para os processadores intel, um bloco de tamanho de no mínimo 4 kB é alocado. Esse tamanho corresponde a uma página de memória virtual (se você não estiver usando PAE – Page Address Extension)… Assim, em teoria, você poderia fazer:

*(p + 4095) = 'x';

E não obter um segmentation fault na cara.

Buffer overruns são particularmente perigosos quando o buffer é declarado como local a uma função. Lembram-se da pilha? Se acessarmos posições do buffer além da sua fronteira podemos sobrescrever, por exemplo, o endereço de retorno, colocado na pilha pelo chamador, fazendo com que o encerramento da rotina crie um salto para outra função que não a do chamador. Esse é o famoso ataque de buffer overrun que alguns de vocês devem ter ouvido falar. Desconsiderando o aspecto de segurança, buffer overruns podem criar alguns comportamentos “estranhos”. Recentemente topei com um problema com este código:

#include <stdio.h>
#include <memory.h>

typedef struct _node {
  struct _node *next;
  int value;
} node;

node list[10] = { { NULL, 8 } };
node *top = list; 

void fill_and_dump(void)
{
  int i;
  node newnode;
  node *pnode = &newnode;

  /* fill */
  for (i = 1; i < 20; i++)
  {
    pnode->next = top++;
    memcpy(top, pnode, sizeof(node));
    top->value = i;
  }

  /* dump */
  pnode = top;
  for (i = 0; pnode != NULL; pnode = pnode->next, i++)
    printf("node %d, value = %d\n", i, pnode->value);
}

int main(void)
{
  fill_and_dump();
  return 0;
}

Devo dizer que essa não seria a forma como eu escreveria a rotina. Do jeito que está ela apresenta um problema interessante:

$ gcc -O0 test.c -o test
$ ./test
node 0, value = 19
node 1, value = 8
node 2, value = 7
node 3, value = 6
node 4, value = 5
node 5, value = 4
node 6, value = 3
node 7, value = 2
node 8, value = 1
node 9, value = 8

Ué?! Cadê os itens 9 até 18? O problema é que ao copiar o bloco de memória em cima do 11º elemento (que não existe no array) sobrescrevemos o ponteiro top porque ele é declarado depois do array list. Assim, na 10º iteração do loop de preenchimento fazemos next da variável local newnode apontar para o último item da lista, incrementamos top e fazemos:

memcpy(top, pnode, sizeof(node));

Quanto memcpy é chamado o componente next de newnode (apontado por pnode) será gravado no ponteiro top! Fazendo com que ele volte a apontar para o último item do array! Na próxima iteração do loop acontece a mesma coisa, até que as 20 iterações de preenchimento acabem. Por isso o “node 0” ai em cima está “fora de sequência”: Foi o último valor gravado no último item do array.

Exemplo do que acontece na rotina

Doideira, né?

Esse é um dos perigos dos buffer overruns… E só aconteceu porque escrevemos além de um bloco de memória alocada. Existem algumas técnicas que podemos usar para evitar esse tipo de “erro”. Para que você não fique definindo constantes para determinar o tamanho de um array, um macete é definir um macro assim:

#define NUM_LIST_ITEMS (sizeof(list) / sizeof(list[0]))

No nosso exemplo, sizeof(list) é 80 e sizeof(list[0]) é o tamanho do primeiro item do array (que é de 8 bytes). Se você mudar o tamanho do array para 30, NUM_LIST_ITEMS refletirá essa alteração automaticamente. Dai, basta usar essa “constante” para nunca utrapassar o tamanho do array, evitando o buffer overrun:

  /* fill */
  for (i = 1; i < NUM_LIST_ITEMS; i++)
  {
    pnode->next = top++;
    memcpy(top, pnode, sizeof(node));
    top->value = i;
  }

Eu coloquei a palavra “erro” entre aspas porque pode ser que um buffer overrun seja exatamente o que você queira fazer numa rotina. Isso não é um erro, é uma feature (O que?! Só a Microsoft pode falar assim, é?!). Por exemplo… Nas implementações do pascal da Borland é comum toparmos com o tipo String, inexistente em C. No caso do pascal, a borland fez o seguinte: O ponteiro para a string aponta para o inćio do array de chars, mas o início real da string fica a 8 bytes de distância, para trás, do início desse array. A Borland resolveu colocar o “tamanho” da string e um contador de referências ANTES do início da string (o que, em minha opinião, é mais eficiente… A falha de strlen, por exemplo, é que ela varre toda o array, contanto os chars, até achar um ”). Em C a estrutura ficaria mais ou menos assim:

struct String {
  int length;
  int ref_count;
  char data[0]; /* Esse 'data' crescerá dinamicamente!
                   Não quero um "ponteiro" aqui, mas um buffer! */
};

Se uma variável do tipo String, em pascal, é um ponteiro para ‘data’, então como acessar o tamanho das string? A função Length, no Delphi, é mais ou menos codificada assim:

function Length(S: String): Integer;
asm
  mov eax,dword ptr [eax-8]
end;

Note que estamos fazendo um buffer overrun “ao contrário”, ou seja, pegando dados ANTES do começo do buffer (seria um buffer underrun?!). É mais ou menos o equivalente, em C, de usar índices de arrays negativos.

De qualquer forma, ponteiros te dão esse poder. Como Platão e o Homem-Aranha já disseram: “Com grande poder vem grande responsabilidade!”

Anúncios

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s