安全でないコードサンプル

scanfは使ってはいけないと耳にしました。

むちゃくちゃ長い文字列を入力したりすると、クラッシュするそうです。

バッファーオーバーランだそうです。

へぇ~

まず、私がよく眼にするメモリ破壊コードを紹介します。私の知っているメモリを破壊するコードはこんな感じです。

char p[10];  // ①

for(int i = 0; i < 11; i++)  // ②

    p[ i ] = 0x00;

とても慎重にコーディングをしないと、この手のバグの除去は困難です。①と②が別のスコープあるいはモジュール、ソースファイルにあったりするのが普通でしょうから、実地ではもっとヤバゲです。これにマルチスレッドとかマルチタスクがからんでくるんですからね。

さて、scanfでも似たような問題は起こりえます。

#include <stdio.h>

int main()
{
  char p[10];

  scanf( "%s", &p );     // 何文字来るかわからんよイヒヒ
  printf( "—-\n" );
  printf( "%s", p );

  return 0;
}

意気込んで、コンソールから11文字(2文字オーバー)入力しても何も起こりません。

$ ./ts.exe
01234567890
—-
01234567890

もっとたくさん打ち込んだら、皆さんが期待した結果が得られました。

$ ./ts.exe
1111111111111111111111111111111111111111111111111111111111
—-
      6 [main] ts 2780 _cygtls::handle_exceptions: Error while dumping state (pr
obably corrupted stack)
Segmentation fault (core dumped)

現象としては、前者の方が厄介です。後者は、「誰かが悪さしている」というシグナルが発せられているからです。文字通りに!

前者はめぐりめぐってどこかで影響がでます。事前にどこかはわかりませんし、オブジェクトのでき方次第では、現象は日々変化するでしょう。

scanfの%sについては、対策は簡単です。

#include <stdio.h>

int main()
{
  char p[10];

  scanf( "%9s", &p );        // 最大幅を指定すれば多くても安心
  printf( "—-\n" );
  printf( "%s", p );

  return 0;
}

実行結果

$ ./ts.exe
01234567890
—-
012345678

見事に切り捨てられます。libcを書いているような人たち、即ち巨人の肩によるバッファオーバーチェックです。

scanfが危険だとかいう人たちは、きっと、getsの話と勘違いしているだけなのだと思います。

個人的には、どこの馬の骨が書いたかわからない、getc()を使ったパーサもどきルーチンの方がよっぽど怖いです。

ここまで書いても、まだ心配な人がいるようです。次の例です。

#include <stdio.h>

int main()
{
  float f;

  scanf( "%f", &f );
  printf( "—-\n" );
  printf( "%f", f );

  return 0;
}

実行結果

$ ./ts
12.3
—-
12.300000

$ ./ts
12.30000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000
—-
12.300000

$ ./ts
AAAAAAAAAAAAAAAAAAAGGGGGGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAA
—-
0.000000

相当長い文字列入れても大丈夫だと思います。最大長のチェックしていると思うんで。文字列だとゼロになるのは、どうでしょうかね。

(2009.06.15追記・改)

scanfで数値を取り組む場合、オーバーフローについては動作が未定義のようです。特に浮動小数点については丸め処理の例もあるように、処理系/実行環境の差異が多数あるので、意図しない値が入るだけ、という甘い考えでははまることがあるようです。(コメント欄も参照ねがいまっする)

(追記終わり)

巨人の肩にタックルしてみたい人はどうぞご勝手にです。

私が言いたいのは、scanfは正しく使えば、仕様通りには使うことができるということで、正しい使い方はそれほど難しいものではないということです。もちろん、自由自在ではないでしょう。書式指定がしょぼいとか。しかしそれは箸でスープを飲もうとして文句を並べている鶴と同じです。

最初に紹介した例は、そういうレベルの話ではなく、ごく普通にそこらじゅうにあるコードでかつ、危険だということです。しかも、唯一の対策が、

慎重に、注意して、厳重にチェックしないとコロス

という、いかにも情けない旧社会主義的方法論しかないということです。

配列のサイズに#defineマクロconst変数を使ったってだめです。①で指定したシンボルと②で指定するシンボルを間違えたらドボンですから。

コンテナクラスを作ろうが、STLを使おうが同じことです。

所詮はアセンブラのマクロのマクロのマクロなんですから。1バイト間違えれば死が待っている世界なのです。

$ gcc –version
gcc (GCC) 3.4.4 (cygming special) (gdc 0.12, using dmd 0.125)
Copyright (C) 2004 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

りんくる

メモリ保護されていない世界での救済を求めて

(2009.06.07追記)

もっとまじめに解説したページをたまたま見つけました。

[迷信] scanf ではバッファオーバーランを防げない

http://www.kijineko.co.jp/tech/superstitions/buffer-overrun-of-scanf.html

・・・scanf 系の関数は、文字列を読み込むことに限れば、fgets なんかよりずっと柔軟な処理ができます。要は、書式指定を完全に把握する気があるかどうかの問題です。

・・・

バッファオーバーランに関しては、IPA のセキュア・プログラミング講座の記事も参考にしてください。

そうそう。このIPAの記事を見れば道案内にはなります。