web-dev-qa-db-ja.com

Cのエラーをチェックすることを忘れない方法

誰もが知っているように、Cは私たちが好きなものを書くことを可能にします。大きな問題が1つあります。標準のライブラリ関数に起因するエラーを処理する責任があるのは私たちだけなので、信頼性の高いコードを作成します。

目標は、信頼性の高いコードを簡単に作成できるようにすることです。

そして問題は、標準ライブラリ関数から生じるエラーについてです。

エラーを隠さないように、標準関数をラップするというアイデアがあります。エラーを無視したい場合は、明示的に行う必要があります。

このコードは私の解決策を示しています:

_void
safe_access
        (bool *ok, const char *path, int amode)
{
        // reset error
        *ok = true;
        errno = 0;

        int ret = access(path, amode);

        // indicate error
        if (ret) {
                *ok = false;
        }
}
_

この関数はunistd.hからのaccess(3)をラップします。次の3つの問題を解決します。

  1. これによりerrnoがリセットされるため、古いerrnoを確認できなくなります。
_// this function sets errno
access(...);

// we forget to reset errno and everything seems to be OK
// now, we want to check for errno
// but, unfortunately, we’re prone to check old errno that comes from access()
long a = strtol(...);
if (... && errno) {
}
_
  1. エラーの処理を忘れることはできません。エラーを無視したい場合でも、それを明示的に行う必要があります。
_bool ok;
safe_access(&ok, ...);

// we need something to do with `ok`
_

また、警告を調整して、変数を未使用のままにできないようにすることもできます。

  1. 誰もが同じ方法でエラーをチェックします。これは通常ではありませんが、if-not-zero、if-negative、if-NULLとは異なる方法でエラーをチェックする必要がある場合があります。たとえば、ferror(3)の後にfread(3)を使用する必要があります。私の解決策は、それを行うsafe_fread()を記述することです。

リスクはありますか?

2
David Lehnsherr

セキュリティに誤った印象を与えることは非常に悪い考えです。

それは悪い習慣を取り除きません

今日の同僚が適切なエラーチェックで通話を処理するのに十分な訓練を受けていない場合:

_if (!access ("input.txt", R_OK) || !access ("output.txt", W_OK)) {
    // something went wrong, so I have to check fro errno
    perror ("Required file not accessible"); 
} 
_

また、ブール値を処理しないだけで、エラーを無視し続けます。

_bool ok; 
safe_access (&ok,"input.txt", R_OK); 
safe_access (&ok,"output.txt", W_OK);
// oops forgot to check the first one 
_

しかし、それは通常のイディオムを壊します

関数のシグネチャをかなり過激な方法で変更するため、よく知られているイディオムがいくつか機能しなくなります。

たとえば、最初のエラーで停止するように_||_チェーンでいくつかのエラーチェックを組み合わせる例を考えます。しかし、さらに問題の多い通常のループ:

_while (fgets(buffer, BUFLEN, fpin)) {
    // do something 
}
_

本当に使用しますか?

_safe_fgets (&ok, buffer, BUFLEN, fpin); 
while (ok) {
    // do something 
    safe_fgets (&ok, buffer, BUFLEN, fpin);  // DRY !!!
}
_

そしてそれは新しいリスクをもたらすかもしれません

前の例から、safe_xxx()関数は少なくとも元の関数と同じ戻り値の型を使用する必要があると結論付けることができます。

ただし、式で複数のsafe_xxx()呼び出しを同じ_&ok_と組み合わせた場合、およびこれらの呼び出しの順序が不定である場合、未定義の動作に直面する可能性があります。

パラメータの不一致、またはエラーによってNULLポインタを渡すことは言うまでもありません。

最後に、追加のリスクでは、正当な期待を生み出す可能性があることについても触れておきます。たとえば、関数は安全性をアドバタイズするので、次の呼び出しは安全であると期待します。

_char *path=NULL; 
// do something 
safe_access (&ok, path, R_OK);  
_

したがって、最終的には、関数がすべてのチェックを実行すると想定していないと仮定すると、どこでも安全であることを確認することで、賢明さが低下する可能性があります。

ここでは確信が持てませんが、識別可能なリスクの高い呼び出しが別のライブラリ/コンパイルユニットにラップされているため、一定の伝播を無効にする可能性があるため、静的コードアナライザーがいくつかのリスクを見つけられない可能性はありますか?バッファオーバーフロールールに固有

生産力の喪失

さらに、マニーerrno関連関数はよく知られています。代わりのものを使用することを強制すると、生産性に影響を与える可能性があります。

  • 新しいチームメンバーはそれに慣れる必要があります
  • あなたはとにかく彼らが行動を尊重していることを確認する必要があります
  • 同僚は長い名前を入力します(読みにくくなります)
  • 同僚は既知のイディオムをリファクタリングする必要があります
  • あるプロジェクトから別のプロジェクトへのコードの再利用はそれほど簡単ではないかもしれません
  • サードパーティのライブラリはとにかくあなたのアプローチを使用しません
  • たくさんの定型コード

より良い代替案があります

これはコードレビューとピアプログラミングと呼ばれます:人々がエラーを適切にチェックしていることを確認してください

11
Christophe

ここにいくつかの分析があります

利点:

  1. 手動のAPIエラーチェックは不要
  2. 実装を余儀なくされた場合の回復力を向上

リスク:

  1. ラッパーの使用を強制するには、追加の監視負担が必要です
  2. エラーの範囲(retタイプ)が絞り込まれており、多くの同様の呼び出しに使用できない
  3. 追加のグローバル状態変数bool okアプリの状態が複雑になります
  4. パターンのみに限定(path, mode) -> error他には役に立たない
  5. ラップは常にある程度のパフォーマンスを犠牲にします
1