なぜgotoは危険ですか?
goto
それ自体が不安定になることはありません。約100,000 goto
秒にもかかわらず、Linuxカーネルは依然として安定性のモデルです。
goto
単独でセキュリティの脆弱性を引き起こすことはありません。ただし、一部の言語では、これをtry
/ catch
例外管理ブロックと混合すると、このCERT勧告で説明されているように脆弱性が生じる可能性があります。メインストリームC ++コンパイラーは、このようなエラーにフラグを立てて防止しますが、残念ながら、古いコンパイラーやよりエキゾチックなコンパイラーはそうではありません。
goto
判読不能で保守不能なコードを引き起こします。これはスパゲッティコードとも呼ばれます。スパゲッティプレートのように、ゴトが多すぎると制御の流れをたどることが非常に難しいためです。
スパゲッティコードを回避できたとしても、ごく少数のgotoを使用するだけでも、次のようなバグやリソースリークが発生しやすくなります。
- 明確なネストされたブロックとループまたはスイッチを備えた構造プログラミングを使用したコードは、簡単に追跡できます。その制御の流れは非常に予測可能です。したがって、不変条件が尊重されるようにすることは簡単です。
goto
ステートメントを使用すると、その単純なフローを破り、期待を破ります。たとえば、まだリソースを解放する必要があることに気付かない場合があります。
goto
さまざまな場所の多くの人が、1つのgotoターゲットにあなたを送ることができます。そのため、この場所に着いたときの状態を確実に知ることは明らかではありません。したがって、誤った/根拠のない仮定を行うリスクは非常に大きくなります。
追加情報と引用符:
Cは、無限に悪用可能なgoto
ステートメントと、分岐先のラベルを提供します。正式にgoto
は必須ではなく、実際には、それなしでコードを書くことはほとんど常に簡単です。(...)
それにもかかわらず、gotoが場所を見つける可能性のあるいくつかの状況を提案します。最も一般的な使用法は、一度に2つのループから抜け出すなど、いくつかの深くネストされた構造の処理を放棄することです。(...)
私たちはこの問題について独断的ではありませんが、gotoステートメントは、たとえあったとしても、控えめに使用する必要があるようです。
gotoはいつ使用できますか?
K&Rのように、gotoについて独断的ではありません。gotoが人生を楽にしてくれる状況があることは認めます。
通常、Cでは、gotoを使用すると、マルチレベルループ出口、またはこれまでに割り当てられたすべてのリソースを解放/ロック解除する適切な出口点に到達する必要があるエラー処理が可能になります。 この記事では、Linuxカーネルでのgotoのさまざまな使用法を定量化します。
個人的には避けることを好み、Cの10年間で最大10個のgotoを使用しました。私はネストされたif
s を使用することを好みます。これによりネストが深すぎる場合、関数をより小さな部分に分解するか、カスケードでブールインジケータを使用するかを選択します。今日の最適化コンパイラは、と同じコードとほぼ同じコードを生成するのに十分賢いgoto
です。
gotoの使用は、言語に大きく依存します。
C ++では、RAIIを適切に使用すると、コンパイラがスコープ外のオブジェクトを自動的に破棄するため、リソース/ロックがとにかくクリーンになり、gotoの実際の必要がなくなります。
Javaではgotoの必要はありません(上記のJavaの著者の引用とこの優れたStack Overflowの回答を参照):混乱、、、および/ 例外処理をクリーンにするガベージコレクターはbreak
、役立つ可能性のあるすべてのケースをカバーしますが、より安全で優れていますマナー。Javaの人気は、現代の言語ではgotoステートメントを回避できることを証明しています。continue
try
catch
goto
有名なSSL goto失敗の脆弱性を拡大
重要な免責事項:コメント内の激しい議論を考慮して、gotoステートメントがこのバグの唯一の原因であるふりをしていないことを明確にしたいと思います。gotoがなければバグがないとは思わない。gotoが深刻なバグに関与する可能性があることを示したいだけです。
goto
プログラミングの歴史の中で、いくつの深刻なバグが関連しているのかはわかりません。詳細はしばしば伝えられません。しかし、iOSのセキュリティを弱める有名なApple SSLバグがありました。このバグにつながったgoto
声明は間違った声明でした。
バグの根本原因はgoto文そのものではなく、間違ったコピー/貼り付け、誤解を招くようなインデント、条件ブロックの周りの中括弧の欠落、または開発者の作業習慣であると主張する人もいます。私はそれらのいずれも確認することはできません。これらの議論はすべて、おそらく仮説と解釈です。誰も本当に知りません。(一方、コメントで誰かが提案したように間違ったマージの仮説は、同じ関数の他のインデントの不整合を考慮すると非常に良い候補のようです)。
唯一の客観的な事実は、重複goto
が早まって関数を終了することになったことです 。コードを見ると、同じ効果を引き起こす可能性のある他の単一のステートメントは、リターンのみでした。
このファイルのエラーは機能SSLEncodeSignedServerKeyExchange()
しています:
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
実際、条件ブロックを中括弧で囲むことでバグを防ぐことができ
ました。コンパイル時の構文エラー(したがって修正)または冗長な無害なgotoのいずれかを引き起こしていました。ちなみに、GCC 6は、一貫性のないインデントを検出するオプションの警告のおかげで、これらのエラーを見つけることができます。
しかし、そもそもこれらのgotoはすべて、より構造化されたコードで回避できたはずです。したがって、gotoは少なくとも間接的にこのバグの原因です。それを回避できた少なくとも2つの異なる方法があります。
アプローチ1:if節またはネストされたif
s
エラーの多くの条件を順番にテストする代わりにfail
、問題が発生した場合にラベルに送信するたびif
に、間違った前提条件がない場合にのみ実行する-statementで暗号操作を実行することを選択できます。
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0 &&
(err = ...) == 0 ) &&
(err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0) &&
...
(err = ...) == 0 ) )
{
... // Do some cryptographic operations here
}
... // Free resources
アプローチ2:エラーアキュムレーターを使用する
このアプローチは、ここでのほとんどすべてのステートメントがerr
エラーコードを設定するために何らかの関数を呼び出し、err
0の場合にのみ残りのコードを実行するという事実に基づいています(つまり、エラーなしで実行される関数)。安全で読みやすい代替手段は次のとおりです。
bool ok = true;
ok = ok && (err = ReadyHash(&SSLHashSHA1, &hashCtx))) == 0;
ok = ok && (err = NextFunction(...)) == 0;
...
ok = ok && (err = ...) == 0;
... // Free resources
ここには、1つのgotoはありません。障害の出口ポイントにすばやくジャンプするリスクはありません。そして、視覚的には、ずれている線や忘れられてok &&
いる線を見つけるのは簡単でしょう。
この構造はよりコンパクトです。これは、Cでは論理の2番目の部分(&&
)が最初の部分が真である場合にのみ評価されるという事実に基づいています。実際、最適化コンパイラーによって生成されたアセンブラーは、gotoを使用した元のコードとほぼ同等です。オプティマイザーは、一連の条件を非常によく検出し、最初のnull以外の戻り値で最後にジャンプするコードを生成します(オンライン証明)。
テストの段階でokフラグとエラーコードの不一致を識別することができる関数の最後に一貫性チェックを想定することもできます。
assert( (ok==false && err!=0) || (ok==true && err==0) );
==0
誤って、!=0
または論理コネクタエラーで置き換えられたなどの間違いは、デバッグフェーズで簡単に発見されます。
前述のように、代替の構成体がバグを回避したとは思わない。バグの発生をより困難にすることができたと言いたいだけです。