scanfでもバッファオーバーフローは防げるけど、悩みが増える

scanf系の関数の%sあたりでバッファオーバーフローを防ぐ方法は結構メジャーな(つまり世間的にありふれていてネタにもならない)ネタだと思っていたのだけど、例えばこんなページが存在していることから推測するに、そうではないらしい。

どうも世間では「scanf == バッファオーバーフロー == ダメ。ゼッタイ」が定説なようだ。

誤解の無いように書いておくと、先ほど挙げたページでは「その定説は迷信です」と指摘していて、それは正しい。ただ、そのようなページが存在するということはつまり、「迷信」だと指摘する行為が世間の注目を多少なりとも集める程には「scanf == バッファオーバーフロー」説が広まっているのだろう。でなければ誰も指摘しようとは思わないはずだ。

さて、文字列の読み込みに関しては、scanfでもバッファオーバーフローを防げるのは事実で、単にフィールド幅を指定しておくだけだ。その上で、scanfの直感的でない一部仕様で自爆する可能性を除外したとしても、自分がscanfを使うかというと多分Noだと思う。

理由は単純で、バッファサイズを定数化しようとすると色々と辛くなってくるからだ。

例えばこんなケースでは問題ない*1

char buf[50+1];
 ……
scanf("%50s", buf);     /* 最大50byte分まで読み込む */

こうしたい時にどうするか、という話だ。

#define  INPUT_MAX_SIZE  50
 ……
char buf[INPUT_MAX_SIZE+1];
 ……
scanf("%50s", buf);     /* フィールド幅をINPUT_MAX_SIZEにする方法は? */

「"%INPUT_MAX_SIZEs"」のように文字列に埋め込んだところで、プリプロセッサはマクロ置換してくれない。printf系の関数なら'*'が使えるけど、scanfでは代入抑止文字扱いなのでダメだ。

一応、何とかすることは可能だ。例えばこんな方法がある。

#define  STR(s)  #s
#define  XSTR(s)  STR(s)
#define  INPUT_MAX_SIZE  50
 ……
char buf[INPUT_MAX_SIZE+1];
 ……
scanf("%" XSTR(INPUT_MAX_SIZE) "s", buf);

「XSTR(INPUT_MAX_SIZE)」はプリプロセス時にマクロ置換されて「"50"」になり、「"%" "50" "s"」はコンパイル時に連結されて「"%50s"」になる。

ただ、この方法には制限がある。

  • INPUT_MAX_SIZEはマクロ定数であること。enumな定数だと破綻する。
  • INPUT_MAX_SIZEは単なる整数値であること。例えば「#define INPUT_MAX_SIZE (40 + 10)」 だと破綻する。
  • INPUT_MAX_SIZEに数値以外が含まれていてはならない。例えば「#define INPUT_MAX_SIZE (50)」だと破綻する。

私はenumで定数化したり*2マクロに四則演算程度の式を書いたり*3することが好きなのだけど、この場合は厳しい。

sprintfないしsnprintfで書式文字列を生成させる方法もある。これなら前述の制限を回避できる。

#if 1
	enum { INPUT_MAX_SIZE = 50 };
#elif 0
	#define  INPUT_MAX_SIZE  (40 + 10)
#else
	#define  INPUT_MAX_SIZE  (50)
#endif
 ……
/* 例えUINT64_MAXでも10進数表現で最大20桁なので、
 * これくらいあれば暫くの間は十分だろう……
 */
char fmt[64];
char buf[INPUT_MAX_SIZE+1];
 ……
sprintf(fmt, "%%%ds", INPUT_MAX_SIZE);
scanf(fmt, buf);

繰り返し実行する処理の場合、毎回書式文字列を作るのも微妙なので、もう少し工夫することになるかもしれない*4

この方法には別の次元で弱点がある。気が利いたコンパイラや静的解析ツールではprintfやscanfなどの書式文字列と引数の数やデータ型とで整合性がとれているかチェックして、一致しない場合に警告してくれるのだけど、このチェックができなくなる。

元々、可変個数引数の関数では、コンパイル時に引数の数やデータ型をチェックすることは不可能だ。一部のコンパイラや静的解析ツールでは、書式文字列のリテラルを静的に解析することによって、書式の内容と引数の数やデータ型が一致しているかどうか調べられるようにしている。しかしsprintf等で書式文字列を動的に生成している場合、書式文字列はプログラム実行時になって初めて生成される。静的解析の時点では肝心の書式文字列が存在しないのだ。だからチェックできない*5

私は警告レベル最大でコンパイルしたり静的解析ツールを併用したりすることが大好きな人間なので、一部とはいえ型チェックできなくなることに恐れを感じる*6。なので、この方法を積極的に採用することはない。

そんな訳で、私はfgetsを使う。scanfを使うとしたら、何か特別な事情がある場合だろう。

まあ、こんな問題はあってもscanfでも文字列読み込み時のバッファオーバーフローを防げるという事実は変わらないのだけど。

*1:バッファサイズを超える入力の読み捨てについては意図的に無視している。このエントリで書きたい内容の本質から離れてしまいそうだからだ。

*2:デバッグ時にシンボルで表示されるから。但しint型扱いになるので、値の大きさには注意する必要がある。

*3:他の定数を元に計算するとか。例えば「#define INPUT_MAX_SIZE (FOO_MAX + BAR_MAX)」のような場合。

*4:例えばプログラムの初期化時に書式文字列を作っておいて、それを使い回すとか。

*5:頑張って解析すれば何とかなるかもしれないけど、それって結局は動的解析に近い処理になると思う。

*6:printfの引数の型を間違えたのが原因で不具合が発生して嵌った経験があるので特に……。