Por que é gets()
perigoso
O primeiro worm da Internet (o Morris Internet Worm ) escapou cerca de 30 anos atrás (02/11/1988) e usou gets()
um estouro de buffer como um de seus métodos de propagação de um sistema para outro. O problema básico é que a função não sabe qual o tamanho do buffer, portanto continua lendo até encontrar uma nova linha ou encontrar um EOF e pode exceder os limites do buffer que recebeu.
Você deve esquecer que já ouviu falar que gets()
existia.
A norma C11 ISO / IEC 9899: 2011 foi eliminada gets()
como uma função padrão, que é A Good Thing ™ (foi formalmente marcada como 'obsoleta' e 'obsoleta' na ISO / IEC 9899: 1999 / Cor.3: 2007 - Corrigenda técnica 3 para C99 e depois removido em C11). Infelizmente, ele permanecerá nas bibliotecas por muitos anos (significando 'décadas') por razões de compatibilidade com versões anteriores. Se dependesse de mim, a implementação de gets()
se tornaria:
char *gets(char *buffer)
{
assert(buffer != 0);
abort();
return 0;
}
Como seu código falhará de qualquer maneira, mais cedo ou mais tarde, é melhor resolver o problema mais cedo ou mais tarde. Eu estaria preparado para adicionar uma mensagem de erro:
fputs("obsolete and dangerous function gets() called\n", stderr);
As versões modernas do sistema de compilação do Linux geram avisos se você vincular gets()
- e também para algumas outras funções que também apresentam problemas de segurança ( mktemp()
,…).
Alternativas para gets()
fgets ()
Como todo mundo disse, a alternativa canônica gets()
é fgets()
especificar stdin
como o fluxo de arquivos.
char buffer[BUFSIZ];
while (fgets(buffer, sizeof(buffer), stdin) != 0)
{
...process line of data...
}
O que ninguém mais mencionou ainda é que gets()
não inclui a nova linha, mas fgets()
inclui. Portanto, pode ser necessário usar um wrapper fgets()
que exclua a nova linha:
char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp)
{
if (fgets(buffer, buflen, fp) != 0)
{
size_t len = strlen(buffer);
if (len > 0 && buffer[len-1] == '\n')
buffer[len-1] = '\0';
return buffer;
}
return 0;
}
Ou melhor:
char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp)
{
if (fgets(buffer, buflen, fp) != 0)
{
buffer[strcspn(buffer, "\n")] = '\0';
return buffer;
}
return 0;
}
Além disso, como caf aponta em um comentário e paxdiablo mostra em sua resposta, fgets()
você pode ter dados restantes em uma linha. Meu código de wrapper deixa esses dados para serem lidos na próxima vez; você pode modificá-lo prontamente para devorar o restante da linha de dados, se preferir:
if (len > 0 && buffer[len-1] == '\n')
buffer[len-1] = '\0';
else
{
int ch;
while ((ch = getc(fp)) != EOF && ch != '\n')
;
}
O problema residual é como relatar os três estados de resultado diferentes - EOF ou erro, linha lida e não truncada e linha parcial lida, mas os dados foram truncados.
Esse problema não ocorre gets()
porque ele não sabe onde seu buffer termina e atropela alegremente além do fim, causando estragos em seu layout de memória bem cuidado, geralmente atrapalhando a pilha de retorno (um estouro de pilha ) se o buffer estiver alocado em a pilha ou pisar nas informações de controle se o buffer for alocado dinamicamente ou copiar dados sobre outras variáveis globais preciosas (ou módulos) se o buffer for alocado estaticamente. Nada disso é uma boa idéia - eles resumem a frase "comportamento indefinido".
Há também o TR 24731-1 (Relatório Técnico do Comitê Padrão C) que fornece alternativas mais seguras para uma variedade de funções, incluindo gets()
:
§6.5.4.1 A gets_s
função
Sinopse
#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);
Restrições de tempo de execução
s
não deve ser um ponteiro nulo. n
não deve ser igual a zero nem maior que RSIZE_MAX. Um caractere de nova linha, fim de arquivo ou erro de leitura deve ocorrer dentro dos n-1
caracteres de leitura
de stdin
. 25)
3 Se houver uma violação de restrição de tempo de execução, s[0]
for definido como o caractere nulo, e os caracteres serão lidos e descartados stdin
até que um caractere de nova linha seja lido, ou no final do arquivo ou ocorra um erro de leitura.
Descrição
4 A gets_s
função lê no máximo um a menos do que o número de caracteres especificado por n
do fluxo apontado por stdin
, na matriz apontada por s
. Nenhum caractere adicional é lido após um caractere de nova linha (que é descartado) ou após o final do arquivo. O caractere de nova linha descartado não conta para o número de caracteres lidos. Um caractere nulo é gravado imediatamente após o último caractere lido na matriz.
5 Se o final do arquivo for encontrado e nenhum caractere tiver sido lido na matriz ou se ocorrer um erro de leitura durante a operação, ele s[0]
será definido como o caractere nulo e os outros elementos de s
valores não especificados.
Prática recomendada
6 A fgets
função permite que programas escritos corretamente processem com segurança as linhas de entrada por tempo demais para armazenar na matriz de resultados. Em geral, isso exige que os chamadores fgets
prestem atenção à presença ou ausência de um caractere de nova linha na matriz de resultados. Considere usar fgets
(juntamente com qualquer processamento necessário com base em caracteres de nova linha) em vez de
gets_s
.
25) A gets_s
função, diferentemente gets
, torna uma violação de restrição de tempo de execução uma linha de entrada exceder o buffer para armazená-lo. Ao contrário fgets
, gets_s
mantém um relacionamento individual entre as linhas de entrada e as chamadas bem-sucedidas para gets_s
. Os programas que usam gets
esperam esse relacionamento.
Os compiladores do Microsoft Visual Studio implementam uma aproximação ao padrão TR 24731-1, mas há diferenças entre as assinaturas implementadas pela Microsoft e as do TR.
A norma C11, ISO / IEC 9899-2011, inclui TR24731 no anexo K como parte opcional da biblioteca. Infelizmente, raramente é implementado em sistemas similares ao Unix.
getline()
- POSIX
O POSIX 2008 também fornece uma alternativa segura para gets()
chamada getline()
. Como aloca espaço para a linha dinamicamente, você acaba liberando-a. Remove a limitação no comprimento da linha, portanto. Ele também retorna o comprimento dos dados que foram lidos, ou -1
(e não EOF
!), O que significa que bytes nulos na entrada podem ser manipulados de maneira confiável. Há também uma variação de 'escolha seu próprio delimitador de caractere único' chamada getdelim()
; isso pode ser útil se você estiver lidando com a saída de find -print0
onde as extremidades dos nomes de arquivo são marcadas com um caractere ASCII NUL '\0'
, por exemplo.
gets()
Buffer_overflow_attack