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)