Por que o goto é perigoso?
goto
não causa instabilidade por si só. Apesar de cerca de 100.000 goto
s, o kernel do Linux ainda é um modelo de estabilidade.
goto
por si só não deve causar vulnerabilidades de segurança. No entanto, em alguns idiomas, misturá-lo com os blocos de gerenciamento de try
/ catch
exceção pode levar a vulnerabilidades, conforme explicado nesta recomendação do CERT . Os principais compiladores C ++ sinalizam e evitam esses erros, mas infelizmente os compiladores mais antigos ou mais exóticos não.
goto
causa código ilegível e impossível de manter. Isso também é chamado de código de espaguete , porque, como em uma placa de espaguete, é muito difícil acompanhar o fluxo de controle quando há muitos gotos.
Mesmo se você conseguir evitar o código de espaguete e usar apenas alguns gotos, eles ainda facilitarão erros como o vazamento de recursos:
- É fácil seguir códigos usando programação de estrutura, com blocos e loops ou switches aninhados claros; seu fluxo de controle é muito previsível. Portanto, é mais fácil garantir que os invariantes sejam respeitados.
- Com uma
goto
declaração, você quebra esse fluxo direto e quebra as expectativas. Por exemplo, você pode não perceber que ainda precisa liberar recursos.
- Muitos
goto
em lugares diferentes podem enviar você para um único alvo. Portanto, não é óbvio saber com certeza o estado em que você se encontra ao chegar a esse lugar. O risco de fazer suposições erradas / infundadas é, portanto, bastante grande.
Informações e citações adicionais:
C fornece a goto
declaração e os rótulos infinitamente abusivos aos quais ramificar. Formalmente, isso goto
nunca é necessário e, na prática, quase sempre é fácil escrever código sem ele. (...)
No entanto, sugerimos algumas situações em que o goto pode encontrar um lugar. O uso mais comum é abandonar o processamento em algumas estruturas profundamente aninhadas, como interromper dois loops ao mesmo tempo. (...)
Embora não sejamos dogmáticos sobre o assunto, parece que as declarações goto devem ser usadas com moderação, se houver .
Quando o goto pode ser usado?
Como K&R, eu não sou dogmático sobre gotos. Eu admito que há situações em que ir poderia facilitar a vida de alguém.
Normalmente, em C, o goto permite saída de loop multinível ou tratamento de erros que exigem atingir um ponto de saída apropriado que libere / desbloqueie todos os recursos que foram alocados até agora (alocação múltipla em sequência significa vários rótulos). Este artigo quantifica os diferentes usos do goto no kernel do Linux.
Pessoalmente, prefiro evitá-lo e, em 10 anos de C, usei no máximo 10 gotos. Prefiro usar if
s aninhados , que acho mais legíveis. Quando isso levaria a um aninhamento muito profundo, eu optaria por decompor minha função em partes menores ou usar um indicador booleano em cascata. Os compiladores de otimização de hoje são inteligentes o suficiente para gerar quase o mesmo código que o mesmo código goto
.
O uso de goto depende muito do idioma:
No C ++, o uso adequado do RAII faz com que o compilador destrua automaticamente objetos que ficam fora do escopo, de modo que os recursos / bloqueio sejam limpos de qualquer maneira e sem necessidade real de ir mais longe.
Em Java, não há necessidade de Goto (ver citação autor de Java acima e este excelente resposta Stack Overflow ): o coletor de lixo que limpa a bagunça, break
, continue
, e try
/ catch
o tratamento de exceção cobrir todo o caso em que goto
poderia ser útil, mas em um mais seguro e melhor maneira. A popularidade de Java prova que a declaração goto pode ser evitada em uma linguagem moderna.
Dê um zoom na famosa vulnerabilidade SSL para falhar
Isenção de responsabilidade importante: em vista da discussão acirrada nos comentários, quero esclarecer que não pretendo que a declaração goto seja a única causa desse bug. Eu não finjo que sem ir, não haveria bug. Eu só quero mostrar que um goto pode estar envolvido em um bug sério.
Não sei a quantos bugs sérios estão relacionados goto
na história da programação: os detalhes geralmente não são comunicados. No entanto, houve um famoso bug do SSL da Apple que enfraqueceu a segurança do iOS. A afirmação que levou a esse bug foi uma goto
afirmação errada .
Alguns argumentam que a causa raiz do bug não foi a declaração goto em si, mas uma cópia / pasta incorreta, uma indentação enganosa, faltando chaves no bloco condicional ou talvez os hábitos de trabalho do desenvolvedor. Não posso confirmar nenhum deles: todos esses argumentos são prováveis hipóteses e interpretação. Ninguém realmente sabe. ( enquanto isso, a hipótese de uma mesclagem que deu errado, como alguém sugeriu nos comentários, parece ser um candidato muito bom em vista de outras inconsistências de indentação na mesma função ).
O único fato objetivo é que um duplicado goto
levou a sair prematuramente da função. Observando o código, a única outra declaração única que poderia ter causado o mesmo efeito teria sido um retorno.
O erro está em função SSLEncodeSignedServerKeyExchange()
de esse arquivo :
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) != 0)
goto fail;
if ((err =...) !=0)
goto fail;
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail; // <====OUCH: INDENTATION MISLEADS: THIS IS UNCONDITIONDAL!!
if (...)
goto fail;
... // Do some cryptographic operations here
fail:
... // Free resources to process error
De fato, chaves em volta do bloco condicional poderiam ter evitado o erro:
isso levaria a um erro de sintaxe na compilação (e, portanto, a uma correção) ou a um goto redundante e inofensivo. A propósito, o GCC 6 poderá detectar esses erros graças ao aviso opcional para detectar recuo inconsistente.
Mas, em primeiro lugar, todos esses gotos poderiam ter sido evitados com um código mais estruturado. Portanto, o goto é pelo menos indiretamente uma causa desse bug. Existem pelo menos duas maneiras diferentes de evitá-lo:
Abordagem 1: se cláusula ou if
s aninhados
Em vez de testar sequencialmente muitas condições de erro e sempre que enviar para um fail
rótulo em caso de problema, poderia-se optar por executar as operações criptográficas em uma if
declaração que o faria apenas se não houvesse uma condição prévia errada:
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0 &&
(err = ...) == 0 ) &&
(err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0) &&
...
(err = ...) == 0 ) )
{
... // Do some cryptographic operations here
}
... // Free resources
Abordagem 2: use um acumulador de erros
Essa abordagem é baseada no fato de que quase todas as instruções aqui chamam alguma função para definir um err
código de erro e executam o restante do código apenas se err
for 0 (ou seja, função executada sem erro). Uma boa alternativa segura e legível é:
bool ok = true;
ok = ok && (err = ReadyHash(&SSLHashSHA1, &hashCtx))) == 0;
ok = ok && (err = NextFunction(...)) == 0;
...
ok = ok && (err = ...) == 0;
... // Free resources
Aqui, não há um único passo: não há risco de pular rapidamente para o ponto de saída da falha. E visualmente seria fácil identificar uma linha desalinhada ou uma esquecida ok &&
.
Essa construção é mais compacta. É baseado no fato de que em C, a segunda parte de uma lógica e ( &&
) é avaliada apenas se a primeira parte for verdadeira. De fato, o assembler gerado por um compilador de otimização é quase equivalente ao código original com gotos: O otimizador detecta muito bem a cadeia de condições e gera código, que no primeiro valor de retorno não nulo salta até o fim ( prova online ).
Você pode até prever uma verificação de consistência no final da função que durante a fase de teste identifique incompatibilidades entre o sinalizador ok e o código de erro.
assert( (ok==false && err!=0) || (ok==true && err==0) );
Erros como um erro ==0
inadvertidamente substituído por um !=0
ou conector lógico seriam facilmente detectados durante a fase de depuração.
Como dito: não finjo que construções alternativas teriam evitado qualquer bug. Eu só quero dizer que eles poderiam ter tornado o bug mais difícil de ocorrer.