Quão perigoso é acessar um array fora dos limites?

Quão perigoso é acessar um array fora de seus limites (em C)? Às vezes, pode acontecer de eu ler de fora da matriz (agora entendo que, em seguida, access a memory usada por algumas outras partes do meu programa ou até mesmo além disso) ou estou tentando definir um valor para um índice fora da matriz. O programa às vezes falha, mas às vezes é executado apenas dando resultados inesperados.

Agora, o que eu gostaria de saber é, quão perigoso é isso realmente? Se danificar o meu programa, não é tão ruim. Se por outro lado quebra algo fora do meu programa, porque de alguma forma consegui acessar alguma memory totalmente não relacionada, então é muito ruim, imagino. Eu li um monte de “qualquer coisa pode acontecer”, “segmentação pode ser o problema menos ruim” , “seu disco rígido pode ficar rosa e unicórnios podem estar cantando sob sua janela”, o que é legal, mas qual é o perigo?

Minhas perguntas:

  1. A leitura de valores de fora da matriz pode prejudicar qualquer coisa além do meu programa? Eu imagino que apenas olhando para as coisas não muda nada, ou mudaria, por exemplo, o atributo ‘última vez aberta’ de um arquivo que eu acessei?
  2. A definição de valores fora do array pode danificar qualquer coisa além do meu programa? A partir desta questão do Stack Overflow, percebo que é possível acessar qualquer local da memory, que não há garantia de segurança.
  3. Agora eu corro meus pequenos programas de dentro do XCode. Isso fornece alguma proteção extra em torno do meu programa, onde ele não pode alcançar fora de sua própria memory? Pode prejudicar o XCode?
  4. Alguma recomendação sobre como executar meu código com bugs inerentemente com segurança?

Eu uso o OSX 10.7, Xcode 4.6.

No que diz respeito ao padrão ISO C (a definição oficial da linguagem), o access a um array fora de seus limites tem ” comportamento indefinido “. O significado literal disso é:

comportamento, mediante a utilização de uma construção de programa não portável ou errónea ou de dados errados, para os quais esta Norma não impõe requisitos

Uma nota não normativa se expande sobre isso:

Possível comportamento indefinido varia de ignorar a situação completamente com resultados imprevisíveis, comportar-se durante a tradução ou execução do programa de uma maneira documentada característica do ambiente (com ou sem emissão de uma mensagem de diagnóstico), para terminar uma tradução ou execução (com a emissão de uma mensagem de diagnóstico).

Então essa é a teoria. Qual é a realidade?

No caso “melhor”, você acessará uma parte da memory que pertence ao seu programa atualmente em execução (o que pode fazer com que seu programa se comporte mal) ou que não seja de propriedade do programa em execução no momento (o que provavelmente fará com que seu programa seja crash com algo como uma falha de segmentação). Ou você pode tentar gravar na memory que seu programa possui, mas isso é marcado como somente leitura; Isso provavelmente também fará com que seu programa trave.

Isso presumindo que seu programa está sendo executado em um sistema operacional que tenta proteger os processos em execução simultaneamente uns dos outros. Se o seu código está sendo executado no “bare metal”, digamos que seja parte de um kernel do sistema operacional ou de um sistema embarcado, não há essa proteção; o seu código mal comportado é o que deveria fornecer essa proteção. Nesse caso, as possibilidades de danos são consideravelmente maiores, incluindo, em alguns casos, danos físicos ao hardware (ou a coisas ou pessoas próximas).

Mesmo em um ambiente de sistema operacional protegido, as proteções nem sempre são 100%. Existem bugs no sistema operacional que permitem que programas sem privilégios obtenham access root (administrativo), por exemplo. Mesmo com privilégios comuns de usuário, um programa com defeito pode consumir resources excessivos (CPU, memory, disco), possivelmente derrubando todo o sistema. Um monte de malware (vírus, etc.) explora saturações de buffer para obter access não autorizado ao sistema.

(Um exemplo histórico: ouvi dizer que em alguns sistemas antigos com memory principal , o access repetido a um único local de memory em um loop apertado poderia literalmente fazer com que esse fragment de memory derreta. Outras possibilidades incluem destruir um monitor CRT e mover a leitura / escreva a cabeça de uma unidade de disco com a frequência harmônica do gabinete da unidade, fazendo com que ela caminhe sobre uma mesa e caia no chão.)

E há sempre a Skynet para se preocupar.

A linha inferior é esta: se você pudesse escrever um programa para fazer algo mal intencionalmente , é pelo menos teoricamente possível que um programa buggy pudesse fazer a mesma coisa acidentalmente .

Na prática, é muito improvável que o seu programa de bugs que está sendo executado em um sistema MacOS X faça algo mais sério do que uma falha. Mas não é possível impedir completamente que o código de bugs faça coisas realmente ruins.

Em geral, os sistemas operacionais de hoje (os mais populares de qualquer maneira) executam todos os aplicativos em regiões de memory protegida usando um gerenciador de memory virtual. Acontece que não é terrivelmente fácil (por exemplo) simplesmente ler ou escrever em um local que existe no espaço REAL fora da (s) região (ões) que foram atribuídas / alocadas para o seu processo.

Respostas diretas:

1) A leitura quase nunca danificará diretamente outro processo, mas pode danificar indiretamente um processo se você ler um valor de KEY usado para criptografar, descriptografar ou validar um programa / processo. A leitura fora dos limites pode ter efeitos adversos / inesperados no seu código se você estiver tomando decisões com base nos dados que está lendo

2) A única maneira pela qual você pode realmente DANIFICAR alguma coisa escrevendo para um loaction acessível por um endereço de memory é se o endereço de memory que você está gravando é realmente um registro de hardware (um local que não é para armazenamento de dados, mas para controlar alguma parte de hardware) não é um local de RAM. De fato, você normalmente não danificará algo a menos que esteja escrevendo algum local programável que não seja regravável (ou algo dessa natureza).

3) Geralmente, a execução de dentro do depurador executa o código no modo de debugging. A execução no modo de debugging faz com que o TEND pare (mas nem sempre) seu código mais rápido quando você faz algo considerado fora de prática ou totalmente ilegal.

4) Nunca use macros, use estruturas de dados que já possuam verificação de limites de índices de array embutidos, etc ….

ADICIONAL Devo acrescentar que as informações acima são realmente apenas para sistemas que usam um sistema operacional com janelas de proteção de memory. Se escrever código para um sistema embarcado ou mesmo um sistema que utiliza um sistema operacional (em tempo real ou outro) que não tenha janelas de proteção de memory (ou janelas virtuais endereçadas), deve-se praticar muito mais cuidado ao ler e gravar na memory. Também nesses casos as práticas de codificação SEGURA e SEGURA devem sempre ser empregadas para evitar problemas de segurança.

Não verificar limites pode levar a efeitos colaterais desagradáveis, incluindo falhas de segurança. Um dos mais feios é a execução de código arbitrário . No exemplo clássico: se você tem uma matriz de tamanho fixo e usa strcpy() para colocar uma string fornecida pelo usuário, o usuário pode fornecer uma string que sobrecarrega o buffer e sobrescreve outros locais de memory, incluindo o endereço de código onde a CPU deve retornar quando sua function terminar.

O que significa que seu usuário pode enviar uma string que fará com que seu programa chame essencialmente exec("/bin/sh") , que irá transformá-lo em shell, executando qualquer coisa que ele queira em seu sistema, incluindo a coleta de todos os seus dados máquina no nó da botnet.

Veja Smashing The Stack For Fun e Profit para detalhes sobre como isso pode ser feito.

Você escreve:

Eu li um monte de “qualquer coisa pode acontecer”, “segmentação pode ser o problema menos ruim”, “seu disco rígido pode ficar rosa e unicórnios podem estar cantando embaixo da sua janela”, o que é legal, mas qual é o perigo?

Vamos colocar dessa forma: carregue uma arma. Aponte para fora da janela sem qualquer objective e fogo. Qual é o perigo?

A questão é que você não sabe. Se o seu código sobrescreve algo que trava o seu programa, você está bem porque ele irá pará-lo em um estado definido. No entanto, se não falhar, os problemas começam a surgir. Quais resources estão sob controle do seu programa e o que isso pode fazer com eles? Quais resources podem ficar sob controle do seu programa e o que isso pode fazer com eles? Eu sei, pelo menos, um grande problema que foi causado por tal estouro. O problema estava em uma function estatística aparentemente sem sentido que atrapalhou algumas tabelas de conversão não relacionadas para um database de produção. O resultado foi uma limpeza muito cara depois. Na verdade, teria sido muito mais barato e mais fácil de lidar se esse problema tivesse formatado os discos rígidos … com outras palavras: os unicórnios cor de rosa podem ser o seu menor problema.

A ideia de que seu sistema operacional irá protegê-lo é otimista. Se possível, tente evitar escrever fora dos limites.

Não rodar o seu programa como root ou qualquer outro usuário privilegiado não prejudicará o seu sistema, então geralmente isso pode ser uma boa idéia.

Ao gravar dados em algum local de memory aleatória, você não “danificará” diretamente qualquer outro programa em execução no seu computador enquanto cada processo é executado em seu próprio espaço de memory.

Se você tentar acessar qualquer memory não alocada para seu processo, o sistema operacional impedirá que seu programa seja executado com uma falha de segmentação.

Portanto, diretamente (sem executar como root e acessar diretamente arquivos como / dev / mem), não há perigo de que seu programa interfira em qualquer outro programa em execução no sistema operacional.

No entanto – e provavelmente é isso que você ouviu falar em termos de perigo – escrevendo cegamente dados randoms em locais de memory aleatória por acidente, com certeza você pode danificar qualquer coisa que você possa danificar.

Por exemplo, seu programa pode querer excluir um arquivo específico dado por um nome de arquivo armazenado em algum lugar em seu programa. Se por acidente você sobrescreve o local onde o nome do arquivo está armazenado, você pode excluir um arquivo muito diferente.

NSArray s no Objective-C recebem um bloco específico de memory. Ultrapassar os limites da matriz significa que você estaria acessando a memory que não está atribuída à matriz. Isso significa:

  1. Essa memory pode ter qualquer valor. Não há como saber se os dados são válidos com base no seu tipo de dados.
  2. Essa memory pode conter informações confidenciais, como chaves particulares ou outras credenciais do usuário.
  3. O endereço de memory pode ser inválido ou protegido.
  4. A memory pode ter um valor de alteração porque está sendo acessado por outro programa ou thread.
  5. Outras coisas usam o espaço de endereço de memory, como portas mapeadas pela memory.
  6. A gravação de dados em endereços de memory desconhecidos pode travar seu programa, replace o espaço de memory do SO e geralmente causar a implosão do sol.

Do aspecto do seu programa, você sempre quer saber quando seu código está excedendo os limites de um array. Isso pode levar a que valores desconhecidos sejam retornados, fazendo com que seu aplicativo trave ou forneça dados inválidos.

Além do seu próprio programa, eu não acho que você irá quebrar nada, no pior dos casos você tentará ler ou escrever de um endereço de memory que corresponde a uma página que o kernel não atribuiu aos seus processos, gerando a exceção apropriada e ser morto (quero dizer, seu processo).

Você pode tentar usar a ferramenta memcheck no Valgrind quando testar seu código – ele não detectará violações de limites de matriz dentro de um quadro de pilha, mas ele deve capturar muitos outros tipos de problemas de memory, incluindo aqueles que causariam problemas sutis, problemas mais amplos fora do escopo de uma única function.

Do manual:

Memcheck é um detector de erros de memory. Ele pode detectar os seguintes problemas que são comuns em programas C e C ++.

  • Acessando a memory que você não deveria, por exemplo, ultrapassando e subindo os blocos de heap, ultrapassando o topo da pilha e acessando a memory depois que ela foi liberada.
  • Usando valores indefinidos, ou seja, valores que não foram inicializados ou que foram derivados de outros valores indefinidos.
  • Liberação incorreta de memory heap, como blocos de heap de liberação dupla ou uso incompatível de malloc / new / new [] versus free / delete / delete []
  • Sobreponha os pointers src e dst em memcpy e funções relacionadas.
  • Perda de memory.

ETA: Embora, como a resposta de Kaz diz, não é uma panacéia, e nem sempre dá a saída mais útil, especialmente quando você está usando padrões de access empolgantes .

Eu estou trabalhando com um compilador para um chip DSP que deliberadamente gera código que acessa um após o final de uma matriz de código C que não o faz!

Isso ocorre porque os loops são estruturados de forma que o final de uma iteração pré-busca alguns dados para a próxima iteração. Portanto, o dado pré-buscado no final da última iteração nunca é realmente usado.

Escrever um código C como esse invoca um comportamento indefinido, mas isso é apenas uma formalidade de um documento de padrões que se preocupa com a portabilidade máxima.

Mais frequentemente, um programa que acessa fora dos limites não é inteligentemente otimizado. É simplesmente um buggy. O código busca algum valor de lixo e, ao contrário dos loops otimizados do compilador mencionado, o código usa o valor em cálculos subseqüentes, corrompendo-os.

Vale a pena capturar bugs como esse, e assim vale a pena tornar o comportamento indefinido apenas pelo mesmo motivo: para que o tempo de execução possa produzir uma mensagem de diagnóstico como “array overrun in line 42 of main.c”.

Em sistemas com memory virtual, um array pode ser alocado de tal forma que o endereço que segue está em uma área não mapeada da memory virtual. O access irá então bombardear o programa.

Como um aparte, note que em C temos permissão para criar um ponteiro que é passado após o final de um array. E este ponteiro tem que comparar maior que qualquer ponteiro para o interior de um array. Isso significa que uma implementação de C não pode colocar um array no final da memory, onde o endereço de um mais se envolveria e pareceria menor do que outros endereços no array.

No entanto, o access a valores não inicializados ou fora dos limites às vezes é uma técnica de otimização válida, mesmo que não seja portável ao máximo. Por exemplo, a ferramenta Valgrind não relata accesss a dados não inicializados quando esses accesss acontecem, mas somente quando o valor é usado posteriormente de alguma forma que poderia afetar o resultado do programa. Você obtém um diagnóstico como “ramificação condicional em xxx: nnn depende do valor não inicializado” e às vezes pode ser difícil rastrear o local de origem. Se todos esses accesss fossem capturados imediatamente, haveria muitos falsos positivos resultantes do código otimizado do compilador, bem como código corretamente otimizado manualmente.

Falando nisso, eu estava trabalhando com algum codec de um fornecedor que estava emitindo esses erros quando portado para o Linux e rodando sob Valgrind. Mas o fornecedor me convenceu de que apenas alguns bits do valor que estava sendo usado vinham da memory não inicializada, e esses bits foram cuidadosamente evitados pela lógica. Apenas os bits bons do valor estavam sendo usados ​​e Valgrind não tem a capacidade de rastrear até o bit individual. O material não inicializado veio da leitura de uma palavra após o final de um stream de bits de dados codificados, mas o código sabe quantos bits estão no stream e não usará mais bits do que realmente existem. Como o access além do final da matriz de stream de bits não causa nenhum dano à arquitetura DSP (não há memory virtual após a matriz, nenhuma porta mapeada na memory e o endereço não é quebrado), é uma técnica de otimização válida.

“Comportamento indefinido” realmente não significa muito, porque de acordo com a ISO C, simplesmente incluindo um header que não está definido no padrão C, ou chamando uma function que não está definida no próprio programa ou no padrão C, são exemplos de indefinidos comportamento. Comportamento indefinido não significa “não definido por ninguém no planeta” apenas “não definido pelo padrão ISO C”. Mas, é claro, às vezes o comportamento indefinido não é absolutamente definido por ninguém.

Se você já fez programação em nível de sistemas ou programação de sistemas embarcados, coisas muito ruins podem acontecer se você gravar em locais de memory randoms. Sistemas mais antigos e muitos microcontroladores usam E / S de mapeamento de memory, portanto, gravar em um local de memory mapeado para um registro periférico pode causar estragos, especialmente se for feito de forma assíncrona.

Um exemplo é programar a memory flash. O modo de programação nos chips de memory é ativado escrevendo-se uma seqüência específica de valores em locais específicos dentro do intervalo de endereços do chip. Se outro processo fosse gravar em qualquer outro local no chip enquanto estava ocorrendo, isso causaria falha no ciclo de programação.

Em alguns casos, o hardware envolve os endereços (os bits / bytes mais significativos do endereço são ignorados), portanto, gravar em um endereço além do final do espaço de endereço físico resultará na gravação de dados no meio das coisas.

E, finalmente, CPUs mais antigas, como o MC68000, podem ser travadas até o ponto em que somente uma reboot de hardware possa fazer com que elas funcionem novamente. Não trabalhei nelas por algumas décadas, mas acredito que foi quando encontrou um erro de barramento (memory inexistente) ao tentar lidar com uma exceção, que simplesmente pararia até que a redefinição de hardware fosse declarada.

Minha maior recomendação é um plug flagrante para um produto, mas não tenho nenhum interesse pessoal nele e não sou afiliado a eles de forma alguma – mas com base em algumas décadas de programação C e sistemas embarcados nos quais a confiabilidade era crítica, o PC da Gimpel O Lint não apenas detectará esse tipo de erro, ele fará de você um programador C / C ++ melhor, insistindo constantemente em você sobre os maus hábitos.

Eu também recomendo ler o padrão de codificação MISRA C, se você conseguir uma cópia de alguém. Eu não vi nenhum recente, mas em alguns dias eles deram uma boa explicação de por que você deveria / não deveria fazer as coisas que eles cobrem.

Não sei sobre você, mas pela segunda ou terceira vez eu recebo um coredump ou hangup de qualquer aplicação, minha opinião sobre qualquer empresa que produza, cai pela metade. A 4ª ou 5ª vez e qualquer que seja o pacote se torna shelfware e eu dirijo uma estaca de madeira através do centro do pacote / disco que veio apenas para ter certeza que ele nunca volta para me assombrar.

Se é um programa de espaço do usuário e rodando em um SO protegido como o Linux, o pior que você veria é a falha de segmentação.