FIO34-C. ファイルから読み込んだ文字と EOF や WEOF を区別する
EOF マクロは、ファイルからデータを読み込んでいるときに、ファイルの終わりに到達したことを示す負の値として使われる。EOF は in-band error indicator の一例である。in-band error indicator の使用は危険であり、「ERR02-C. 正常終了時の値とエラーの値は別の手段で通知する」ではその使用を避けるように勧めている。
入出力関数 fgetc(), getc(), getchar() は、いずれもストリームから文字を読み取り、それを int 型で返す(「STR00-C. 文字の表現には適切な型を使用する」を参照)。ストリームがファイルの終わりに達すると、ストリームのファイル終了フラグが設定され、関数は EOF を返す。読み取りエラーが発生すると、ストリームのエラーフラグが設定され、関数は EOF を返す。EOF は負の値なので、符号無しの型である文字の値と一致することはない。ただし、これは int 型のビット幅が char 型のビット幅よりも大きい場合にだけ成り立つ話である。int 型と char 型のビット幅が同じ場合、文字を読み込む関数は EOF と全く同じビットパターンの文字を返す可能性がある。これは例えば、攻撃者がファイルやデータストリームに EOF に見える値を紛れ込ませた場合に発生する。C 言語規格で要請されているのは、int 型が +32767 を表現できること、char 型のビット幅は int 型を超えないこと、だけだ。あまり一般的ではないが、sizeof(int) == sizeof(char) となるような環境では (int)(unsigned char)65535 == -1 となり、EOF は通常の文字と見分けがつかない。したがってこのような環境では、ファイルの終わりやエラーの発生をチェックする feof() と ferror() 関数を使わなければ、EOF の扱いを誤ってしまう可能性がある。
ワイド文字の読込みでは、より多くの環境でこの問題が発生する。fgetwc()、getwc()、getwchar() は wint_t 型の値を返す。wint_t 型の値は読み込んだワイド文字の値を表現する型であるとともに、ワイド文字のストリームの終わりを示す WEOF も表現できる型である。多くの実装では wchar_t 型と wint_t 型は同じ大きさであり、これらの関数の返り値が通常の文字なのか、ストリームの終わりを示す WEOF なのか区別できない状況が発生する可能性がある。
UTF-16 符号化形式では、0xFFFF は文字ではないことが保証されているため、WEOF を -1 で表すことが可能である。同様に UTF-32 符号化形式では、全ての文字コードは符号付き32ビット整数として正の値を持つ。現在広く使われている文字集合(符号化形式)では、文字ではないコードが少なくとも1つあるようにデザインされている。したがって、ワイド文字あるいは int 型と同じ大きさの通常の(ワイド文字でない)文字で、この問題が発生することはまずないと言っていいだろう。
C 標準ライブラリの feof() 関数と ferror() 関数は文字型と整数型の大きさの問題には影響されないので、ファイルの終わりやファイルエラーをチェックするときはこれらの関数を使うべきである[Kettlewell 2002]。ただし、繰り返し処理のなかで毎回2つの関数を呼ぶとオーバーヘッドがかかる。繰り返し処理のなかでは EOF や WEOF はファイルの終わりやエラーを示しているものとして扱い、繰り返し処理の後で feof() と ferror() を使い、正しい処理を行っていたかを確認する、という方法が良いだろう。
違反コード
次の違反コード例では、文字 c が EOF になるまで処理を繰り返している。
#include <stdio.h>
void func(void) {
int c;
do {
c = getchar();
} while (c != EOF);
}
EOF は負の値であり、他の正の値を持つ文字と区別できることは保証されているが、入力した文字データを int に変換して比較する場合、正しく EOF と区別できるとは限らない。int 型と char 型が同じ大きさの環境では、繰り返し処理が実際の入力データの終わりまで処理を行わずに終了してしまう可能性がある。
適合コード (ポータブルなコード)
次の適合コードでは feof() を使ってファイルの終わりをチェックし、ferror() を使ってエラーが発生していないかチェックしている。
#include <stdio.h>
void func(void) {
int c;
do {
c = getchar();
} while (c != EOF);
if (feof(stdin)) {
/* ファイルの終わりだった場合の処理 */
} else if (ferror(stdin)) {
/* ファイルに関するエラーだった場合の処理 */
} else {
/* EOF を入力として受け取っていた場合の処理 */
}
}
違反コード (ポータブルでないコード)
次に示す違反コード例ではアサーションを使って、int 型が char より大きい環境でのみ実行が行われるようにしている。また、EOF は有効な文字の値とは異なることが保証されている。しかし、このコード例では、変数 c が int 型ではなく char 型と宣言されている。単なる char 型が符号付きの型として扱われる環境の場合、int 型への変換において符号拡張が行われるため、入力された文字の値を EOF と比較したときに一致してしまう可能性がある。
#include <assert.h>
#include <limits.h>
#include <stdio.h>
void func(void) {
char c;
static_assert(UCHAR_MAX < UINT_MAX, "FIO34-C violation");
do {
c = getchar();
} while (c != EOF);
}
char が符号付き8ビット幅の型、int が32ビット幅の型である場合、getchar() が文字コード 0xFF (10進で255) の文字を返したとすると、これは EOF と解釈されてしまう。なぜならこの値は比較演算の際に 0xFFFFFFFF (EOF の値) に符号拡張されるからである(「STR34-C. 文字データをより大きなサイズの整数型に変換するときは事前に unsigned char 型に変換する」を参照のこと)。
適合コード (ポータブルでないコード)
次に示す適合コードでは、変数 c を int 型と宣言している。これによって、繰り返し処理は、ファイルの終わりまで行われるようになる。
#include <assert.h>
#include <stdio.h>
#include <limits.h>
void func(void) {
int c;
static_assert(UCHAR_MAX < UINT_MAX, "FIO34-C violation");
do {
c = getchar();
} while (c != EOF);
}
違反コード (ワイド文字)
次の違反コード例では、C 標準ライブラリ関数 getwc() の呼出しの結果が wchar_t 型の変数に格納され、WEOF と比較されている。
#include <stddef.h>
#include <stdio.h>
#include <wchar.h>
enum { BUFFER_SIZE = 32 };
void g(void) {
wchar_t buf[BUFFER_SIZE];
wchar_t wc;
size_t i = 0;
while ((wc = getwc(stdin)) != L'\n' && wc != WEOF) {
if (i < (BUFFER_SIZE - 1)) {
buf[i++] = wc;
}
}
buf[i] = L'\0';
}
このコードには2つ問題がある。1つめは、getwc() の返り値が、WEOF との比較演算の前に wchar_t に変換されていることである。2つめは、wint_t が wchar_t より大きい型であることを確認していないことである。これらの問題により、ファイル中に WEOF と一致する値となるデータを入れておくことで、ファイルの途中で処理を終了させることが可能になってしまう。
適合コード (ポータブルなコード)
次に示す適合コードでは、変数 c を wint_t 型と宣言し、getwc() の返り値の整数型に合わせている。さらに、ファイルの終わりを判別するために、WEOF の識別以外の方法も使っている。
#include <stddef.h>
#include <stdio.h>
#include <wchar.h>
enum {BUFFER_SIZE = 32 }
void g(void) {
wchar_t buf[BUFFER_SIZE];
wint_t wc;
size_t i = 0;
while ((wc = getwc(stdin)) != L'\n' && wc != WEOF) {
if (i < BUFFER_SIZE - 1) {
buf[i++] = wc;
}
}
if (feof(stdin) || ferror(stdin)) {
buf[i] = L'\0';
} else {
/* WEOF を入力として受け取っていた場合の処理 */
}
}
例外
FIO34-EX1: 正常に処理が行われた結果として文字列を返し、エラー終了時には EOF を返す C の関数はいくつもある。これらには次のような関数も含まれる。fclose()、fflush()、fputs()、fscanf()、puts()、scanf()、sscanf()、vfscanf()、vscanf()。これらの関数呼出しにおけるエラー判定は、返り値を EOF と比較すればよい。
リスク評価
ファイルに含まれる文字データが EOF や WEOF と区別できるという誤解から様々な脆弱性が発生した。コマンドインジェクション攻撃などはその一例である(CA-1996-22 を参照)。
|
ルール |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
|---|---|---|---|---|---|
|
FIO34-C |
高 |
中 |
中 |
P12 |
L1 |
自動検出(最新の情報はこちら)
|
ツール |
バージョン |
チェッカー |
説明 |
|---|---|---|---|
| CodeSonar | 4.0 | LANG.CAST.COERCE | Coercion Alters Value |
|
|
|
|
|
| 6.5 |
CHAR_IO |
|
|
| 1.2 |
CC2.FIO34 |
一部実装済み |
|
|
5.0 |
|
CERT C Rule Pack を使ってこのルールの違反を検出できる。 |
|
| 3.1.1 |
|
|
関連するガイドライン
参考資料
| [Kettlewell 2002] | Section 1.2, "<stdio.h> and Character Types" |
| [NIST 2006] | SAMATE Reference Dataset Test Case ID 000-000-088 |
| [Summit 2005] | Question 12.2 |
翻訳元
これは以下のページを翻訳したものです。
FIO34-C. Distinguish between characters read from a file and EOF or WEOF (revision 17)



