VOID. FIO33-C. 未定義の動作となる入出力エラーを検出して処理する
エラー発生時に変数が適切に初期化されないままになるような入出力関数については、その返り値を必ず検査することが重要である。入出力エラーの検出と処理を適切に行わないと、プログラムは未定義の動作を引き起こす可能性がある。
以下に引用する Apple の『Secure Coding Guide』はエラー処理の重要性を解説している[Apple 2006]。
Apple セキュリティチームが発見したファイル関連の脆弱性の大半は、プログラムの開発者が終了コードを検査していれば避けられたものである。たとえば、誰かが
chflags
ユーティリティを呼び出してファイルに変更不可(immutable)フラグを設定した場合、chmod
ユーティリティを使ってファイルのモードを変更したりそのファイルのアクセス制御リストに変更を加えようとすると、たとえroot
権限で実行したとしてもchmod
は失敗する。予期せずに失敗する可能性のある呼び出し例としては、他にもrm
を呼び出してファイルを削除する場合が考えられる。ディレクトリが空だと思ってそのディレクトリを削除しようとrm
を呼び出したとき、別の誰かがそのディレクトリにファイルやサブディレクトリを置いていると、rm
の呼び出しは失敗する。
C 言語仕様のセクション 7.21 および Annex K で定義されている入出力関数は、失敗したか成功したかが明確に分かる仕組みを提供している。標準ライブラリ関数のエラー発生を検出する方法については、「ERR33-C. 標準ライブラリ関数のエラーを検出し対処する」で説明している。
違反コード (fgets()
)
fgets()
は失敗すると null ポインタを返す。以下のコード例では fgets()
の返り値がエラーかどうかをテストしていない。
#include <stdio.h> #include <string.h> void func(void) { enum { BUFFERSIZE = 32 }; char buf[BUFFERSIZE]; fgets(buf, sizeof(buf), stdin); buf[strlen(buf) - 1] = '\0'; /* 改行を上書き */ }
fgets()
関数自身はファイルの終端と読み取りエラーを区別しないため、関数の呼び出し元は feof()
や ferror()
を使ってどちらが起きたかを調べねばならない。fgets()
が失敗したとき、そのエラーの原因により、配列の内容は変更されていないかもしれないし不定であるかもしれない。C 言語仕様のセクション 7.21.7.2 には次のように記載されている [ISO/IEC 9899:2011]。
fgets
関数は、成功するとs
を返す。ファイルの終わりを検出し、かつ配列に1文字も読み取っていなかった場合、配列の内容を変化させずに残し、空ポインタを返す。読取りエラーが発生した場合も空ポインタを返すが、この場合の配列の内容は、不定とする。
buf[0]
に null 文字が置かれ strlen(buf)
が 0
を返す可能性がある。その場合、改行文字を上書きすることを意図した代入文は、配列境界外書き込みエラーを引き起こすことになる。
適合コード (fgets()
)
以下の適合コードでは、入力バッファの処理を行う前に fgets()
の返り値をチェックしている。
#include <stdio.h> #include <string.h> void func(void) { enum { BUFFERSIZE = 32 }; char buf[BUFFERSIZE]; char *p; if (fgets(buf, sizeof(buf), stdin)) { /* fgets が成功したので、改行文字をスキャンする */ p = strchr(buf, '\n'); if (p) { *p = '\0'; } else { /* 改行文字が見つからなかったので行末まで flush */ while ((getchar() != '\n') && !feof(stdin) && !ferror(stdin)) ; } } else { /* fgets 失敗、エラー処理 */ } }
このコードでは、fgets()
がエラーになっていないかを検査し、アプリケーション固有のエラー処理が行えるようにしている。fgets()
が成功していれば、改行文字があるかどうかバッファを走査し、もしあればそれが null 文字に置き換えられる。改行文字が見つからなければ、gets()
の動作をシミュレートするために stdin
を行末まで読み飛ばす。「FIO37-C. fgets() や fgetws() が読み取り成功時に空でない文字列を返すと想定しない」を参照。
通常は while
ループで次のようにチェックしていれば十分である。
int c; while ((c = getchar()) != '\n' && !feof(stdin) && !ferror(stdin))
詳細は、「FIO34-C. ファイルから読み込んだ文字と EOF や WEOF を区別する」を参照のこと。
違反コード (fopen()
)
このコードでは、fopen()
関数を使用して file_name
が示すファイルをオープンしている。fopen()
の実行が失敗すると、fptr
の値は NULL
になる。その状態で fptr
を引数にして fclose()
を呼び出すと、プログラムがクラッシュしたり意図せぬ動作をしたりする可能性がある。
#include <stdio.h> int truncate_file(const char *file_name) { FILE *fptr = fopen(file_name, "w"); return fclose(fptr); }
適合コード (fopen()
)
fopen()
関数は、null ポインタを返してエラーが発生したことを示す。以下の適合コードで示すように、ファイルを処理する前にエラーチェックを行えば、null ポインタ参照による未定義動作を排除できる。ファイルに対する処理を行う前には常に返り値をチェックし、エラーが発生していないことを確認すること。エラーが発生した場合は適切に処理すること。
#include <stdio.h> int truncate_file(const char *file_name) { FILE *fptr = fopen(file_name, "w"); if (NULL == fptr) { return -1; } return fclose(fptr); }
違反コード (snprintf()
)
snprintf()
および関連する関数の返り値は必ずチェックすること。snprintf()
関数は、表現形式エラーが発生した場合 −1
を返し、結果が完全にバッファに収まらない場合はバッファサイズ以上の値を返す。
このコード例では、値が 0 である変数 j
がさらにデクリメントされる可能性があり、ほとんど常に予期せぬ結果を招く。このエラーが脆弱性に結びつくことは通常あまりないが、プログラムの異常終了は容易に発生しうる。
snprintf()
の返り値がバッファサイズより大きいか等しい場合には、次の snprintf()
呼び出しに渡されるサイズはより小さい値になるが、文字列の切り捨ては検知されない。
#include <stdio.h> int fmt_data(char *buffer, size_t bufsize, char *s, char c, int i, float fp) { int j; j = snprintf(buffer, bufsize, " String: %s\n", s); j += snprintf(buffer + j, bufsize - j, " Character: %c\n", (unsigned char)c); j += snprintf(buffer + j, bufsize - j, " Integer: %d\n", i); j += snprintf(buffer + j, bufsize - j, " Real: %f\n", fp); return j; }
char
型のオブジェクトを snprintf()
などの関数に渡すときは、符号拡張されないように、呼び出し側でオブジェクトを unsigned char
にキャストする必要があることに注意(「STR37-C. 文字処理関数への引数は unsigned char として表現できなければならない」の説明を参照)。
適合コード (snprintf()
)
以下の適合コードでは、rc
に格納された snprintf()
から返される値を検査した上で、nbytes
に書き込まれ格納される文字数に加算している。関数の返り値の型も int
から size_t
に変更されデータが切り捨てられないようにしている。(「INT31-C. 整数変換によってデータの消失や解釈間違いが発生しないことを保証する」を参照。)
#include <stdio.h> size_t fmt_data(char *buffer, size_t bufsize, char *s, char c, int i, float fp) { int rc; size_t nbytes = 0; rc = snprintf(buffer, bufsize, " String: %s\n", s); if (rc < 0) { /* エラー処理 */ } else if (rc >= bufsize) { /* 切り捨てられた出力の処理 */ } else { nbytes += rc; } rc = snprintf(buffer + nbytes, bufsize - nbytes, " Character: %c\n", (unsigned char)c); if (rc < 0) { /* エラー処理 */ } else if (rc >= bufsize - nbytes) { /* 切り捨てられた出力の処理 */ } else { nbytes += rc; } rc = snprintf(buffer + nbytes, bufsize - nbytes, " Integer: %d\n", i); if (rc < 0) { /* エラー処理 */ } else if (rc >= bufsize - nbytes) { /* 切り捨てられた出力の処理 */ } else { nbytes += rc; } rc = snprintf(buffer + nbytes, bufsize - nbytes, " Real: %f\n", fp); if (rc < 0) { /* エラー処理 */ } else if (rc >= bufsize - nbytes) { /* 切り捨てられた出力の処理 */ } else { nbytes += rc; } return nbytes; }
違反コード (snprintf()
)
以下の違反コード例では、snprintf()
関数が 2 回呼び出されている。最初の呼出しでは null バッファとサイズ 0 を指定して文字列を格納するのに十分なバッファのサイズを調べ、そのサイズで動的メモリを確保し、2 回目の呼出しで実際に文字列を格納している。もし snprintf()
の最初の呼び出しが失敗して負の値を返すと(たとえば、GNU libc のバグ 441945 にあるようなメモリ不足が原因の場合など)、malloc()
が負の値を引数として呼び出されてメモリ割り当てに失敗し、NULL
を返す可能性がある。その結果、以降の snprintf()
の呼び出しで未定義動作が発生する。また、snprintf()
の最初の呼び出しと malloc()
の呼び出しがともに成功し、その次の 2 回目の snprintf()
呼出しが失敗する場合、fmtint()
関数は初期化されていないメモリへのポインタを呼び出し側に返すこととなり、そのポインタを通じたメモリアクセスによって未定義動作が発生する可能性がある。
#include <stdio.h> #include <stdlib.h> char *fmtint(int i, int width, int prec) { int n; char *buf; static const char fmt[] = "%*.*d"; /* 必要なバッファのサイズを判定 */ n = snprintf(NULL, 0, fmt, width, prec, i); /* snprintf() からの正常な返り値を想定している */ buf = (char *)malloc(n + 1); /* malloc() からの正常な返り値を想定している */ snprintf(buf, n + 1, fmt, width, prec, i); return buf; }
適合コード (snprintf()
)
以下の適合コードでは、snprintf()
の返り値を検査し、未定義動作につながる可能性がある値の使用を避けている。
#include <stdio.h> #include <stdlib.h> char *fmtint(int i, int width, int prec) { int n; char *buf; static const char fmt[] = "%*.*d"; /* 必要なバッファのサイズを判定 */ n = snprintf(NULL, 0, fmt, width, prec, i); /* snprintf() の実行が失敗したかどうかを検査 */ if (n < 0) { return NULL; } /* malloc() の実行が失敗したかどうかを検査 */ buf = (char *)malloc(n + 1); if (NULL == buf) { return NULL; } /* ここも snprintf() の実行が失敗したかどうかを検査している */ n = snprintf(buf, n + 1, fmt, width, prec, i); if (n < 0) { free(buf); buf = NULL; } return buf; }
リスク評価
入出力エラーを適切に処理しないと、未定義の動作を引き起こしたりバッファオーバーフロー脆弱性につながる恐れがある。
ルール |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
---|---|---|---|---|---|
FIO33-C |
高 |
中 |
中 |
P12 |
L1 |
自動検出
ツール |
バージョン |
チェッカー |
説明 |
---|---|---|---|
|
|
「EXP12-C. 関数の返り値を無視しない」に対する違反がないか検査することによって、このルールの違反を検出できる。 |
|
Coverity |
7.5.0 |
CHECKED_RETURN |
関数呼び出しから返される値の処理方法の不一致を検出する。Coverity Prevent は、このルールの違反をすべて検出できるわけではないため、さらなる検証が必要である。 |
5.0 |
|
CERT C Rule Pack を使ってこのルールの違反を検出できる |
関連するガイドライン
CERT C コーディングスタンダード |
STR35-C. 長さに制限のないデータを固定長配列へコピーしない |
SEI CERT C++ Coding Standard | VOID FIO33-CPP. Detect and handle input output errors resulting in undefined behavior |
MITRE CWE | CWE-391, Unchecked error condition |
参考資料
[Apple 2006] | "Secure File Operations" |
[Haddad 2005] | |
[ISO/IEC 9899:2011] | Subclause 7.21.7.2, "The fgets Function" |
[Kettlewell 2002] | Section 6, "I/O Error Checking" |
[Seacord 2013] | Chapter 6, "Formatted Output" |
翻訳元
これは以下のページを翻訳したものです。
void FIO33-C. Detect and handle input output errors resulting in undefined behavior (revision 115)