コース目次 / 第3章

文字列とバッファ — C に境界チェックは無い

C文字列がヌル終端されたバイト列にすぎないこと、strcpyが渡された箱の大きさを一切知らないことを確認します。バッファのサイズと、書き込む長さの不一致が、この先の穴の芯だと理解します。

第3章 / 全6章目安 約10分この章のゴール: C文字列の実体を理解し、strcpyが境界を確認しない関数だと言える

C言語には、他の多くの言語にある専用の文字列型がありません。「文字列」とは、char の配列で、末尾に 0(ヌル文字)が置かれたもの——それだけの約束事です。

文字列は、ただのバイト列+終端の目印

#include <stdio.h>

int main(void) {
    char greeting[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
    printf("%s\n", greeting);          // "Hello" と表示される
    printf("配列のサイズ: %zu\n", sizeof(greeting));  // 6(中身は5文字+終端)
    return 0;
}

printf(“%s”, …) は、渡されたアドレスから、\0(値が0のバイト)に出会うまで、ひたすら読み続けます。「文字列の長さ」を別途どこかに持っているわけではありません——終端の目印を見つけるまで、読み続けるだけです。この「どこまで読むかは、目印を見つけるまで分からない」という設計が、次に見る穴の土台になります。

strcpy は、箱の大きさを、一切知らない

C言語の代表的な文字列コピー関数 strcpy(コピー先, コピー元) を見ます。

char buf[8];
strcpy(buf, "こんにちは");  // buf は何バイト? コピー元は何バイト?

strcpy がすることは、たった一つ——コピー元を、\0 に出会うまでコピー先に書き写す。これだけです。buf が何バイト確保されているかは、strcpy にはまったく分かりません。関数の引数には、コピー先の「アドレス」しか渡っておらず、「そこに何バイト書いてよいか」という情報は、どこにも含まれていないからです。

もし、コピー元がコピー先より長かったら?

#include <stdio.h>
#include <string.h>

int main(void) {
    char buf[8];  // 8バイトの箱
    strcpy(buf, "AAAAAAAAAAAAAAAAAAAAAAAA");  // 24文字+終端を、8バイトの箱に
    printf("buf: %s\n", buf);
    return 0;
}

これをコンパイルすると、gcc は親切にも警告を出します。

warning: '__builtin_memcpy' writing 25 bytes into a region of size 8
overflows the destination [-Wstringop-overflow=]

コンパイラは「8バイトの箱に25バイト書き込もうとしている」と、静的解析で気づいて警告してくれます。でも、これは警告であって、エラーではありません——コンパイルは通り、実行ファイルはできてしまいます。gcc の警告は親切な助け船ですが、すべての危険なケースを見抜けるわけではありません(次の章で見るように、動的に決まる長さの入力では、コンパイラは見抜けません)。

バッファのサイズと、書き込む長さの不一致

この章の結論を、一言に畳みます。

「箱の大きさ」と「書き込む長さ」が食い違ったとき、C言語は何も守ってくれません。境界チェックは、プログラマが自分で書かない限り、存在しない。strcpy の代わりに、コピー先のサイズを指定できる strncpy のような関数もありますが、それも正しく使わなければ同じ穴が空きます。
sec-injection で見た「データとコードを混ぜない」と、根っこは似ています——C言語は、あなたが渡したものを、あなたが指示したとおりに実行するだけで、意図と食い違っていないかは確認しません。

持ち帰る一言

C文字列は、終端の目印があるだけのバイト列。strcpyは、箱の大きさを知らない。 コピー先のサイズと、書き込む長さが食い違えば、何の防御もなくあふれます。次の章で、そのあふれが実際に何を壊すのかを、手を動かして観察します。

こうなっていればOK

卒業まであと2章です。

この章はまだ完了していません。