ポインタをタダで差し出してはいけない

2020年1月19日

C は1972年に誕生した言語です。その言語仕様からは、アセンブラと同等の融通性を持たせようという意図が感じられます。当時はアセンブラの時代でしたから、これは当然のことです。

それと同時に、セキュリティのことなどそれほど考えなくてもいい時代だったのでしょう。それは標準関数の野放図ぶりからもうかがえます。そのことへの反省から、2011年に採択された C11 では、いくつかの標準関数が廃止され、より堅牢な新しい関数への置き換えが推奨されています。

ここから一つの教訓が得られます。「配列のポインタは、とにかく配列のサイズを添えて渡せ」。

gets 関数の例

C の標準関数の一つである gets 関数は、標準入力から文字列を取得する関数です。かつては次のような使い方が想定されていました。

#include <stdio.h>

void test()
{
  char str[32];
  gets(str);
  /* 以下 str を使った処理 */
}

プログラマーはここで文字列用に32バイトの配列を用意していますが、標準入力から飛び込んでくる文字列は32バイト以下であるとは限りません。それより長い文字列が標準入力から流し込まれると、バッファオーバーランを起こし、たちまちプログラムの動作は予測不能になります。そればかりでなく、悪意のあるユーザーなら、この穴から任意のプログラムを注入することさえできてしまうのです。

C11 では gets 関数は廃止され、代わりに gets_s 関数が用意されました。これは次のように使います。

#include <stdio.h>

void test()
{
  char str[32];
  gets_s(str, sizeof(str));
  /* 以下 str を使った処理 */
}

gets_s 関数は、ここで受け取った配列のサイズを超えて何かを書くことは絶対にありません。このような守備的な考えをプログラム全体で貫けば、プログラムの堅牢性が向上するようになります。

あなたの製品では悪意のあるユーザーのことなど想定する必要はないかもしれませんが、単なるバッファオーバーランによるバグも、同じぐらい恐ろしいものです。起こしてしまうと、にわかには何が起こったのか理解できない種類のバグだからです。

結論

配列のポインタを引き取る関数があるなら、gets_s 関数の例にならい、その配列のサイズを「次の引数で」引き取るように設計しましょう。ただし const ポインタについてはその限りではありません。

たとえば次のような関数を設計しようとしているなら――

// 文字列をカンマで分割して格納する
void split(const char *src,
           char *dst1,
           char *dst2);

代わりに次のようにしましょう。

// 文字列をカンマで分割して格納する
void split(const char *src,
           char *dst1,
           size_t sizeDst1,
           char *dst2,
           size_t sizeDst2);

こうすれば、sizeof 演算子を適用した式をただ次に置けばいいということが、関数を使う人に伝わります。次のコードはその使用例です。

void test()
{
  char dst1[32];
  char dst2[32];
  split("left,right", dst1, sizeof(dst1), dst2, sizeof(dst2));
}

これはモダンな C における標準的な考え方です。