危険な関数と安全な代替を並べて覚える。バッファオーバーフローは過去数十年のセキュリティ脆弱性の上位。
#include <string.h> int main(void) { char buf[8]; // 8バイトしかない strcpy(buf, "This is too long!"); // 18バイト書き込み → オーバーフロー return 0; }
sizeof / 定数)char dst[8]; strcpy(dst, src); // src の長さを dst がチェックしない // src が8バイト以上ならオーバーフロー
char dst[8]; strncpy(dst, src, sizeof(dst) - 1); dst[sizeof(dst) - 1] = '\0'; // 最後に必ず'\0'を入れる
'\0' を付けない。必ず末尾に dst[size-1] = '\0'; を明示的に入れること。char dst[8]; int n = snprintf(dst, sizeof(dst), "%s", src); // snprintf は常に'\0'終端する (size>0 のとき) // 戻り値 n は「書きたかったバイト数」。n >= sizeof(dst) なら切り詰め発生 if (n >= (int)sizeof(dst)) { fprintf(stderr, "warning: truncated\n"); }
char buf[32]; sprintf(buf, "Hello, %s!", name); // name が長いと buf を越える
char buf[32]; snprintf(buf, sizeof(buf), "Hello, %s!", name); // buf のサイズで切り詰める
int n = snprintf(buf, sizeof(buf), fmt, ...); if (n < 0) { /* エンコードエラー */ } else if (n >= sizeof(buf)) { /* 切り詰め発生 */ } else { /* OK: n バイト書き込んだ */ }
gets は長さを指定できず、攻撃対象になりやすいため C11 で廃止されました。代わりに fgets を使います。char line[64]; gets(line); // NG: 長さ制限なし
char line[64]; if (fgets(line, sizeof(line), stdin) == NULL) { // EOF またはエラー }
fgets は改行 '\n' もコピーするので、必要なら取り除きます。char line[64]; if (fgets(line, sizeof(line), stdin)) { size_t len = strlen(line); if (len > 0 && line[len - 1] == '\n') { line[len - 1] = '\0'; } }
line[len-1] != '\n' になるので、ループで残りを読むか、エラー処理するか選ぶ。atoi はエラー検出ができず、オーバーフロー時の挙動も不定です。strtol を使えば「変換失敗」「範囲外」を正しく判定できます。int n = atoi(s); // s="abc" でも 0 を返す → エラーと区別できない // s="99999999999" でも何かを返す(未定義)
#include <stdlib.h> #include <errno.h> errno = 0; char *end; long v = strtol(s, &end, 10); if (end == s) { /* 変換できる文字がなかった */ } else if (*end != '\0') { /* 途中に変な文字 */ } else if (errno == ERANGE) { /* long の範囲外 */ } else { /* 成功: 値は v */ }
#include <errno.h> #include <limits.h> #include <stdlib.h> // s を int に変換できれば *out にセットし 0 を返す。失敗なら -1。 int parse_int(const char *s, int *out) { errno = 0; char *end; long v = strtol(s, &end, 10); if (end == s || *end != '\0') return -1; if (errno == ERANGE || v < INT_MIN || v > INT_MAX) return -1; *out = (int)v; return 0; }
| 避けるべき | 代わりに | ポイント |
|---|---|---|
strcpy | snprintf / strncpy+'\0' | サイズ制限なし |
strcat | strncat / snprintf | 同上 |
sprintf | snprintf | 戻り値で切り詰め検知 |
gets | fgets | gets はC11で廃止 |
atoi / atof | strtol / strtod | エラー検出可能 |
scanf("%s", ...) | fgets+sscanf または %31s のように幅指定 | 幅なし scanf は危険 |
gcc -Wall -Wextra -Wformat-security -Wstack-protector \
-fstack-protector-strong -D_FORTIFY_SOURCE=2 -O2 prog.c
-D_FORTIFY_SOURCE=2 は glibc が多くの関数にランタイムチェックを入れてくれる。-fstack-protector-strong はスタック破壊を検知して即座に止める。cppcheck、clang-tidy、clang --analyze を使うとさらにバッファ系バグを事前に見つけられる。-fstack-protector-strong オプション付きでコンパイルすると stack smashing detected が出る。parse_int をさらに拡張し、"123abc" のような部分マッチをエラーにする版と、"3.14" を許して整数部だけ取る版の2種類を用意せよ。テストケースも10個ほど書く。char *read_line(FILE *fp, char *buf, size_t size); を実装。入力が size を超えたら残りを捨てて次回以降に影響しないようにする。戻り値は buf か NULL(EOF)。