ERR02-C. 正常終了時の値とエラーの値は別の手段で通知する
インタフェースを設計する際は、正常終了時の値とエラーの値を別の手段で通知するように、設計せよ。Cのライブラリ関数では、正常終了時の値もエラー発生時の値も返り値で表すものが多いが、このような関数の設計は推奨できない。問題を起こしやすい一例として、標準Cの EOF がある(「FIO34-C. 文字入出力関数の返り値の取得には int 型を使用する」および「FIO35-C. sizeof(int) == sizeof(char) の場合、EOF およびファイルエラーの検出には feof() と ferror() を使用する」を参照)。また、「MSC31-C. 関数の返り値は必ず適切な型と比較する」には、問題を起こしやすい別の例として、標準Cの size_t 型と time_t 型に関する使用例が述べられている。
違反コード (sprintf())
以下のコード例は、Linux Kernel Mailing Listからの抜粋だが、同じような例はほかにもよく見られる。
int i;
ssize_t count = 0;
for (i = 0; i < 9; ++i) {
count += sprintf(
buf + count, "%02x ", ((u8 *)&slreg_num)[i]
);
}
count += sprintf(buf + count, "\n");
sprintf() 関数は、配列に書き出された(終端のnull文字を含まない)文字数を返す。この数字は、配列へのインデックスの位置を記憶するために既存のカウンタに追加される。しかし、sprintf() 関数は符号化エラーなどのエラー条件の際には−1を返す。このエラーが最初の呼び出し時に起これば、count 変数の値はデクリメントされ、-1になる。このインデックスが次に使用された場合、範囲外の読み取りや書き込みが生じるだろう。
適合コード (sprintf_m())
以下の適合コードは、APIの設計を変更したCERT Managed Stringライブラリの sprintf() 関数を示している[Burch 2006]。
errno_t sprintf_m( string_m buf, const string_m fmt, int *count, ... );
sprintf_m() は、関数の返り値と書き込まれた文字数の情報を切り離している。この場合、*count には buf に書き込まれた文字数が設定され、返り値は関数の終了状態を示す。関数の返り値として終了状態を示せば、プログラマは関数の終了状態を検査しやすくなる。
前のコード例は次のように修正できる。
int i;
rsize_t count = 0;
errno_t err;
for (i = 0; i < 9; ++i) {
err = sprintf_m(
buf + count, "%02x ", &count, ((u8 *)&slreg_num)[i]
);
if (err != 0) {
/* 出力エラーの処理 */
}
}
err = sprintf_m(
buf + count, "%02x ", &count, ((u8 *)&slreg_num)[i]
);
if (err != 0) {
/* 出力エラーの処理 */
}
違反コード (POSIX、ssize_t)
ssize_t データ型は「size_t の符号付き表現」として設計されている。このため、成功時には符号無しの値を、エラー時には負の値を返す関数の返り値の型として使用されることが多い。たとえば、POSIXの read() 関数のプロトタイプは次のとおりである。
ssize_t read(int fildes, void *buf, size_t nbyte);
read() は、エラーが発生した場合は−1を返し、エラーが発生していない場合は実際に読み取ったバイト数を返す。
この型の使用もやめたほうがよい。ssize_t の値が負である可能性を開発者は無視しがちだからである。
適合コード (POSIX size_t)
read() 関数で考えられる別のプロトタイプを以下に示す。
errno_t read(int fildes, void *buf, size_t nbyte, size_t* rbytes);
rbytes は size_t へのポインタである。エラーが発生していなくて、rbytes が NULL でない場合、値には読み取ったバイトの総数が設定され、read() は0を返す。エラーが発生した場合、read() はエラーを示す0以外の値を返す。
違反コード (TR24731-1)
以下のコード例では、strcpy_s() 関数の返り値は検査されていないまま、エラー処理が正常終了して呼び出し元に戻っている。
constraint_handler_t handle_errors(void) {
constraint_handler_t data;
/* エラー発生時の処理 */
return data;
}
/*...*/
set_constraint_handler(handle_errors);
/*...*/
/* 成功したら 0 を返す */
errno_t function(char *dst1){
char src1[100] = "hello";
strcpy_s(dst1, sizeof(dst1), src1);
/* この時点で strcpy_s がエラーになっても、
handle_errors() が正常復帰してしまう */
/* ... */
return 0;
}
適合コード (TR 24731-1)
以下の解決法では、エラー処理ルーチンがプログラムを終了させており、strcpy_s() が完全に成功しない限り復帰しないことを保証している。
/*
* abort_handler_s() 関数は標準エラーにメッセージを書き込み、
* その後 abort() 関数を呼び出す。
*/
set_constraint_handler(abort_handler_s);
/*...*/
/* 成功したら 0 を返す */
errno_t function(char *dst1){
char src1[100] = "hello";
strcpy_s(dst1, sizeof(dst1), src1);
/* abort_handler_s() は復帰しないため、
処理がここに来るのは strcpy_s() が成功した場合のみ */
/* ... */
return 0;
}
例外
ERR02-EX1: NULLポインタを返すことで関数の呼び出しエラーを表すことがある。NULLポインタの使用はC言語で認められている。C標準[ISO/IEC 9899:2011]セクション6.3.2.3には次のように記載されている。
空ポインタ定数をポインタ型に型変換した場合、その結果のポインタを空のポインタ(null pointer)と呼び、いかなるオブジェクト又は関数へのポインタと比較しても等しくないことを保証する。
ERR02-EX2: 関数内でエラーが発生した場合にプログラムが停止することを保証できるならば、関数の返り値がエラー状態の通知を兼ねるような設計になっていてもよい。たとえば、TR24731-1に定義されている関数は、内部の制約違反へのフックを提供している。エラー発生時に制約違反ハンドラが呼び出し元に戻らないことが確実であれば、これらの関数から返されたエラーを無視しても安全である。たとえば制約違反ハンドラが abort() または longjmp() を呼び出すようにすれば、この処理を実現できるだろう。
TR24731-1で定義されている関数の詳細については、「ERR03-C. TR24731-1 で定義されている関数を呼び出す際は、実行時制約ハンドラを使用する」を参照。
リスク評価
関数の返り値がエラー表示子を兼ねることで生じるリスクは、数値化が難しいため「低」と評価されている。しかし、このような状況で、プログラマが状態コードを検査しなかったり、正しく検査しなかったりすると、深刻な結果をもたらす可能性がある。
|
レコメンデーション |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
|---|---|---|---|---|---|
|
ERR02-C |
低 |
低 |
高 |
P1 |
L3 |
関連するガイドライン
| CERT C++ Secure Coding Standard | ERR02-CPP. Avoid in-band error indicators |
| ISO/IEC TR 24731-1:2007 |
参考資料
| [Burch 2006] | |
| [ISO/IEC 9899:2011] | Section 6.3.2 "Other Operands" |
翻訳元
これは以下のページを翻訳したものです。
ERR02-C. Avoid in-band error indicators (revision 51)
