トラック9・🔤文字列

安全な文字列操作——バッファサイズの意識

読む目安 約14分・ゴール:バッファオーバーフローの危険を理解し、snprintfやfgetsで安全に書ける

アプリでこのトピックを学ぶ(無料・登録不要)

このトピックの要点

文字列を扱う配列のことを、バッファ(buffer)と呼びます。そしてCの歴史上、最も多くの被害を出してきたバグが、このバッファから溢れる事故です。

char name[5];
strcpy(name, "Alexander");    // 9文字+'\0' で10バイト必要なのに、箱は5つ

コンパイルは通ります。実行すると、strcpy は行き先の大きさを知らないまま10バイトを書き込み、配列の外へはみ出します。隣の変数が壊れる、関数の戻り先が壊れる、あるいは何事もなく動いてしまう——境界外アクセスそのものです。

これがバッファオーバーフローで、攻撃者が意図的に長いデータを送り込んでプログラムを乗っ取る手口としても知られています。

書き込む前に、入る大きさを渡す

対策の原則はひとつ。行き先の大きさを関数に教えることです。いちばん使いやすいのが snprintf です。

#include <stdio.h>

int main(void)
{
    char name[5];

    snprintf(name, sizeof(name), "%s", "Alexander");
    printf("%s\n", name);       // Alex(4文字+'\0'。溢れずに切り詰められる)
    return 0;
}

snprintf(行き先, 行き先の大きさ, 書式, …) の形です。指定した大きさを超えることはなく、末尾には必ず '\0' が付きます。書式指定子が使えるので、数値と文字列を混ぜて組み立てることもできます。

strncpy の落とし穴

「n付き」の strncpy もありますが、こちらは注意が必要です。コピー元が長いと、'\0' が付かないまま打ち切られます。

char dst[5];
strncpy(dst, "Alexander", sizeof(dst));   // "Alex" までコピー、'\0' なし!
dst[sizeof(dst) - 1] = '\0';              // 自分で終端する必要がある

安全そうな名前なのに一手間が要る、というのがCらしいところです。迷ったら snprintf を選ぶのが無難です。

入力も同じ考え方

キーボードからの入力にも同じ危険があります。scanf の %s は入力の長さを制限しないため、長い入力で簡単に溢れます。文字列の入力には fgets を使いましょう。

#include <stdio.h>

int main(void)
{
    char line[64];

    if (fgets(line, sizeof(line), stdin) != NULL) {
        printf("入力: %s", line);
    }
    return 0;
}

fgets(行き先, 大きさ, stdin) も大きさを渡す形です。なお fgets は改行文字も一緒に取り込むので、必要なら取り除きます(t09-08で扱います)。かつて存在した gets という関数は、大きさを指定できず危険すぎたため、標準から削除されました。

罠・注意

師範器の大きさを知らぬ者に、水を注がせてはならん

まとめ: 文字列を書き込む関数には、必ず行き先の大きさを渡す。strcpy より snprintf、scanf("%s") より fgets。

アプリでこのトピックを学ぶ(無料・登録不要)

確認問題

確認問題 1

char buf[8]; に長さの分からない文字列を入れるとき、最も安全な書き方はどれですか。

  1. strcpy(buf, src);
  2. snprintf(buf, sizeof(buf), "%s", src);
  3. scanf("%s", buf);
  4. gets(buf);
答えを見る

正解:B(snprintf(buf, sizeof(buf), "%s", src);)

snprintf は行き先の大きさを受け取り、超えるぶんを切り詰めたうえで必ず '\0' を付けます。strcpy と scanf("%s") は大きさを見ないため溢れる危険があり、gets は危険すぎて標準から削除されました。

確認問題 2

「名前を配列に入れて表示して」と頼んだら、アイがこう書きました。アイの答案にバグはありますか。

1#include <stdio.h>
2#include <string.h>
3
4int main(void)
5{
6    char name[5];
7
8    strcpy(name, "Alexander");
9    printf("%s\n", name);
10    return 0;
11}
答えを見る

判定:バグあり(8行目)

8行目が原因です。"Alexander" は9文字+'\0' で10バイト必要ですが、name は5バイトしかありません。strcpy は行き先の大きさを確認しないので、配列の外へ5バイトぶん書き込んでしまいます。これがバッファオーバーフローです。snprintf(name, sizeof(name), "%s", "Alexander"); にするか、name を十分な大きさで宣言します。コンパイルも実行も通ってしまうことがあるのが、この事故の怖いところです。

「文字列」の目次

  1. 文字列とはchar配列である
  2. ヌル終端 '\0' の掟
  3. 文字列リテラルと初期化
  4. string.h の主要関数
  5. 安全な文字列操作——バッファサイズの意識
  6. 文字の判定と変換(ctype.h)
  7. 文字コードの基礎——ASCIIとUTF-8
  8. 文字列処理の定石パターン

もっと先へ:ポインタ・メモリ・ファイル入出力・セキュアコーディングを含む全26トラックと、段位検定・模試のフルセットは完全版に収録しています。

完全版の販売ページは準備中です。

ほかのトラック

📚 姉妹教材:手を動かして覚える Linux 教材 — Linuxとインフラの仕組みを地図で学ぶ

Web版