ERR05-C. アプリケーション非依存なコードではエラー検知のみ行ない、エラー処理は行わない
アプリケーション非依存なコードとは以下のようなコードである。
- コンパイラまたはオペレーティングシステムに同梱されているコード
- サードパーティ製ライブラリのコード
- 自組織で開発したコード
アプリケーション固有のコードでエラーを検知する場合、次のように即座に特定の処理を行なうことができる。
if (something_really_bad_happens) { take_me_some_place_safe(); }
アプリケーションはエラーを検知するとともにエラー処理の仕組みを提供しなければならない。しかしアプリケーション非依存なコードは、その用途が特定のアプリケーションに限られているわけではないので、エラーを勝手に処理するべきでない。アプリケーションがエラーを処理できるように、エラーを検知して報告することが必要である。
エラーを検知し報告する仕組みを以下に列挙する。
- 返り値(特に
errno_t
型の値) - 参照渡しの引数
- グローバルオブジェクト(例:
errno
) longjmp()
- 上記の組み合わせ
違反コード
以下のコード例は、f()
および g()
というアプリケーション非依存な2つの関数からなる。関数 f()
は外部から参照される API の一部であり、関数 g()
は内部でのみ使われる関数である。
void g(void) { /* ... */ if (something_really_bad_happens) { fprintf(stderr, "Something really bad happened!\n"); abort(); } /* ... */ } void f(void) { g(); /* ... f の残りを実行する ... */ }
g()
の中で something_really_bad_happens
(何か悪いこと)が生じると、g()
はエラーメッセージを stderr
に出力し abort()
を呼び出している。このアプリケーション非依存なコードでは、自分が呼び出されたときの状況は分からないため、勝手にエラーを処理してしまうのは誤りである。
“Smart Libraries,” Practice 23 [Miller 2004]には次のように記載されている。
何らかの異常が原因でライブラリが処理を中断するということは、異常の検知後は処理を続行できないと判断していることを意味する。しかしそれは、呼び出し元に代わってこの判断をしていることになる。異常がライブラリ内部のバグだったとしても、その時点で異常を解消できないのは自明ではあるが、勝手に処理を中断するのはよくない。ライブラリの開発者は、自分が開発したライブラリが使われる状況でどの程度の耐障害性が求められているかを知り得ないのである。ライブラリが異常から回復する術を持っていなかったとしても、ライブラリを呼び出したアプリケーション側ではなんとかできるかもしれない。
単純に g()
から abort()
の呼び出しをなくすだけではだめだ。呼び出し元の関数に対してエラーが生じたことを伝える方法がない。
適合コード (返り値)
呼び出し元の関数にエラーを伝える方法の 1 つは、成功または失敗を示す値を返すことである。以下の適合コードでは、それぞれの関数が必ず errno_t
型の値を返している。0 はエラーが発生しなかったことを示す。
const errno_t ESOMETHINGREALLYBAD = 1; errno_t g(void) { /* ... */ if (something_really_bad_happens) { return ESOMETHINGREALLYBAD; } /* ... */ return 0; } errno_t f(void) { errno_t status = g(); if (status != 0) return status; /* ... f の残りを実行する ... */ return 0; }
f()
の返り値は、成功の場合ゼロ、失敗の場合非ゼロを返し何が悪かったのかを示す。
返り値の型 errno_t
は、関数がステータス表示子を返すことを示す(「DCL09-C. errno エラーコードを返す関数は返り値を errno_t 型として定義する」を参照)。
上記のエラー処理のアプローチは安全だが、以下の不利な点がある。
- ソースとオブジェクトコードのサイズが大幅に増加する。30~40%増加する可能性がある [Saks 2007b]。
- 関数からの返り値をすべて検査しなければならない(「MEM32-C. メモリ割り当てエラーを検出し、対処する」を参照)。
- 関数は、エラー表示子を返す場合には他の値を返すべきではない(「ERR02-C. 正常終了時の値とエラーの値は別の手段で通知する」を参照)。
- リソースの割り当てを行う関数は、エラーが生じたときにそれらのリソースを確実に解放しなければならない。
適合コード(ポインタ引数)
関数の返り値にステータス表示子を組み込む代わりに、エラーを示すためのポインタを引数としてとる方法がある。次の例では、それぞれの関数が errno_t\ *
引数を使用してエラーを報告する。
const errno_t ESOMETHINGREALLYBAD = 1; void g(errno_t * err) { if (err == NULL) { /* null ポインタを処理する */ } /* ... */ if (something_really_bad_happens) { *err = ESOMETHINGREALLYBAD; } else { /* ... */ *err = 0; } } void f(errno_t * err) { if (err == NULL) { /* null ポインタを処理する */ } g(err); if (*err == 0) { /* ... f の残りを実行する ... */ } return 0; }
f()
の引数として errno_t
型のオブジェクトへのポインタが渡されると、正常終了時にはゼロ、失敗すると非ゼロのステータス表示子が返される。
上記の解決策は安全ではあるが、以下の不利な点がある。
- 呼び出し元から渡されたポインタが
errno_t
型のオブジェクトへの有効なポインタの場合にのみステータスを返すことができる。この引数がNULL
の場合にはエラーを返す手立てはない。 - 引数にNULLポインタを受け取る可能性があるため、ソースコードのサイズはさらに大きくなる。
- 関数の呼出し後、エラー表示子を検査する必要がある。
- リソースの割り当てを行う関数は、エラーが生じたときにそれらのリソースを確実に解放しなければならない。
- 返り値の場合とは異なり、静的分析ツールでは一般にポインタ引数を通じて渡されたエラー表示子の検査の有無を診断しない。
適合コード (グローバルなエラー表示子)
関数は、返り値または引数にエラー表示子を組み込む代わりに、グローバル変数に値を代入することでステータスを示すことができる。次の例では、それぞれの関数が my_errno
という static な表示子を使用する。
errno
変数は、このアプローチによる標準Cライブラリにおけるエラー処理の実装である。
errno_t my_errno; /* .h ファイルにも定義されている */ const errno_t ESOMETHINGREALLYBAD = 1; void g(void) { /* ... */ if (something_really_bad_happens) { my_errno = ESOMETHINGREALLYBAD; return; } /* ... */ } void f(void) { my_errno = 0; g(); if (my_errno != 0) { return; } /* ... f の残りを実行する ... */ }
f()
を呼び出すと、成功の場合はゼロ、失敗の場合は非ゼロのステータス表示子が返される。
この解決策は、利点も欠点も含めて errno
と多くの共通の特性がある。
- ソースコードのサイズは大きくなるが、他の方法の場合ほどではない。
- 関数の呼出し後、エラー表示子を検査する必要がある。
- このメカニズムを使用する関数呼び出しを入れ子にすると問題がある。
- リソースの割り当てを行う関数は、エラーが生じたときにそれらのリソースを確実に解放しなければならない。
- 一般に、この仕組みを複数組み合わせて使うのは難しい。たとえば、適合コードを変更して
errno
を使うようにするのは難しくバグも生じやすい。開発者は、errno
に値が設定されるタイミングや値がクリアされるタイミングを正確に知っていなければならず、errno
で新たな値を使おうとする場合には、いままでどのような値を使っていたかを把握しなければならないからである。 - 他のアプリケーション非依存コードから
f()
を呼び出すことについては大きな制限がある。f()
は、my_errno
をゼロに設定するので、別のアプリケーション非依存コードが設定したエラー値を上書きする可能性がある。
上記のような理由から、グローバルなエラー表示子は使用しないほうがよい。
適合コード (setjmp()
および longjmp()
)
C言語では、setjmp()
と longjmp()
を使うことで、制御フローを変えることができる。つまり、エラー時の返り値をチェックせずに、エラー発生時にしかるべき制御フローに切り替えることが可能だ。
次の例は、setjmp()
と longjmp()
を使用して、エラーが生じたときに制御フローを確実に中断する。また、上記の例の my_errno
表示子も使用している。setjmp()
と longjmp()
についての詳細は、「MSC22-C. setjmp()、longjmp() の機能を安全に使用する」を参照すること。
#include <setjmp.h> const errno_t ESOMETHINGREALLYBAD = 1; jmp_buf exception_env; void g(void) { /* ... */ if (something_really_bad_happens) { longjmp(exception_env, ESOMETHINGREALLYBAD); } /* ... */ } void f(void) { g(); /* ... f の残りを実行する ... */ } /* ... */ if (setjmp(exception_env) != 0) { /* ここに到達した場合、エラーが生じている。処理を続行しないこと。 */ } /* ... */ f(); /* ここに到達した場合、エラーは生じていない */ /* ... */
f()
の呼び出しは正常に終了するか、エラーを捕捉するために用意された if
ブロックに制御を引き渡す。
- 関数の仕様はエラーを検知または処理しない場合と同じため、ソースコードのサイズは大幅には大きくならない。
- エラーが生じた場合、割り当てられているリソースは解放されなければならない。
- アプリケーションは、アプリケーション非依存コードを呼び出す前に
setjmp()
を呼び出さなければならない。 - シグナルは、
longjmp()
の呼び出しを経た場合、必ずしも維持されない。 setjmp()
/longjmp()
を使用すると、通常とは異なる動作となる。- リソースの割り当てを行う関数は、エラーが生じたときにそれらのリソースを確実に解放しなければならない。
まとめ
次の表に、エラー報告と検知のメカニズムの特徴をまとめる。
方法 |
コードの増加 |
割り当てリソース管理 |
自動的に適用可能 |
---|---|---|---|
返り値 |
大きい(30–40%) |
NO |
YES |
ポインタ引数 |
大きい |
NO |
NO |
グローバル表示子 |
中 |
NO |
YES |
|
小さい |
NO |
n/a |
リスク評価
エラー検知の仕組みがないと、アプリケーションはエラーによって通常のプログラム動作が中断されたことを知ることができない。
レコメンデーション |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
---|---|---|---|---|---|
ERR05-C |
中 |
中 |
高 |
P4 |
L3 |
自動検出(最新の情報はこちら)
ツール |
バージョン |
チェッカー |
説明 |
---|---|---|---|
Compass/ROSE |
|
|
|
関連するガイドライン
CERT C++ Secure Coding Standard | ERR05-CPP. Application-independent code should provide error detection without dictating error handling |
参考資料
[Miller 2004] |
[Saks 2007b] |
翻訳元
これは以下のページを翻訳したものです。
ERR05-C. Application-independent code should provide error detection without dictating error handling (revision 44)