C

ヤバすぎるC言語のサンプルのヤバさを分かりやすくしてみる

侍エンジニア塾のC言語のサンプルがヤバすぎる。
https://www.sejuku.net/blog/25002

ヤバすぎると言われているソースです。

yabai.c
#include <stdio.h>
#include <stdlib.h>

// 構造体の宣言
typedef struct {
  int num;
  char *str;
} strct;

int main(void) {
  // 実体を生成
  strct *entity;

  // 動的メモリの確保。確保したメモリをstrct型ポインタにキャスト。
  entity = (strct*)malloc(sizeof(strct));

  // メンバの初期化
  entity->num = 0;
  entity->str = (char*)malloc(sizeof(32));

  // メモリに文字列を代入
  sprintf(entity->str, "%s %s!", "Hello", "World");
  printf("%s\n", entity->str);

  // メモリの解放
  free(entity->str);
  free(entity);

  return 0;
}

ためしに動かしてみましょう。
(macOS High Sierraで試していますが、実行環境によって結果が変わるかもです)

gcc yabai.c -o yabai
./yabai
Hello World!

普通に結果が表示されています。この結果だけ見ればヤバくないです。

さて、このソースは32バイトまでentry->strに文字列を書き込むことができるように作ったのだと思います。ためしに、文字列を増やしてみましょう。

yabai.c
#include <stdio.h>
#include <stdlib.h>

// 構造体の宣言
typedef struct {
  int num;
  char *str;
} strct;

int main(void) {
  // 実体を生成
  strct *entity;

  // 動的メモリの確保。確保したメモリをstrct型ポインタにキャスト。
  entity = (strct*)malloc(sizeof(strct));

  // メンバの初期化
  entity->num = 0;
  entity->str = (char*)malloc(sizeof(32));

  // メモリに文字列を代入 !を増やしてみた
  sprintf(entity->str, "%s %s!!!!!!", "Hello", "World");
  printf("%s\n", entity->str);

  // メモリの解放
  free(entity->str);
  free(entity);

  return 0;
}

動かしてみます。

gcc yabai.c -o yabai
./yabai
Hello World!!!!!!
yabai(37341,0x7fffac7bd380) malloc: *** error for object 0x7fbb6e4028b0: incorrect checksum forfreed object - object was probably modified after being freed.
*** set a breakpoint in malloc_error_break to debug
Abort trap: 6

アボートしてしまいました。ヤバいですね。
もう少しヤバさが分かるようにしてみましょう。

yabai.c
#include <stdio.h>
#include <stdlib.h>

// 構造体の宣言
typedef struct
{
    int num;
    char *str;
} strct;

int main(void)
{
    // 実体を生成
    strct *entity;

    // 動的メモリの確保。確保したメモリをstrct型ポインタにキャスト。
    entity = (strct *)malloc(sizeof(strct));

    // メンバの初期化
    entity->num = 0;
    entity->str = (char *)malloc(sizeof(32));

    // mallocを増やしてみた
    char *str = (char *)malloc(32);
    sprintf(str, "\(^o^)/");

    // メモリに文字列を代入 ※printfに変数を追加してみた
    sprintf(entity->str, "%s %s!!!!!!", "Hello", "World");
    printf("%s %s\n", entity->str, str);

    // メモリの解放
    free(entity->str);
    free(entity);
    free(str);

    return 0;
}

実行してみます。

gcc yabai.c -o yabai
./yabai
Hello World!!!!!! !

落ちなくなりました。しかし、顔文字が表示されないです。代わりに「!」に置き換わっています。
何が起きているか調べるためポインタを表示してみます。

printf("%s %s %p %p\n", entity->str, str, entity->str, str);
gcc yabai.c -o yabai
./yabai
Hello World!!!!!! ! 0x7fa670c028a0 0x7fa670c028b0

2つのポインタから、「entity->str」の先頭から16バイト後ろに「str」が確保されているようです。そのため、以下のように「entity->str」に設定した文字列が溢れて「str」まで侵食しています。

アドレス 文字
0x7fa670c028a0 H
0x7fa670c028a1 e
0x7fa670c028a2 l
0x7fa670c028a3 l
0x7fa670c028a4 o
〜〜〜
0x7fa670c028ae !
0x7fa670c028af !
0x7fa670c028b0 !(ここから「str」の領域)

これば本番環境のソースで短い文字列でしかテストしてなかったとしたら、運用時に長い文字列が入れられて落ちる、もしくは結果が正しく表示されないという結果になってしまいます。超ヤバいですね。

mallocで指定するサイズ、sizeofで取得する値が正しいか、常に注意して使いましょう。
よくあるのが、構造体や配列のsizeofではなく、ポインタのsizeofを取ってしまう間違いです。

    strct *entity;
    char str[32];
    char *ptr_str = str;

    printf("sizeof(struct)=%lu\n", sizeof(strct));
    printf("sizeof(entity)=%lu\n", sizeof(entity));
    printf("sizeof(32)=%lu\n", sizeof(32));
    printf("sizeof(str)=%lu\n", sizeof(str));
    printf("sizeof(ptr_str)=%lu\n", sizeof(ptr_str));
    printf("sizeof(*ptr_str)=%lu\n", sizeof(*ptr_str));
sizeof(struct)=16 # 本来の構造体のサイズ
sizeof(entity)=8 # ポインタのサイズなので構造体のサイズと違う
sizeof(32)=4 # intのサイズ
sizeof(str)=32 # 配列のポインタのサイズ
sizeof(ptr_str)=8 # ポインタのサイズ strを代入していても関係ない
sizeof(*ptr_str)=1 # charのサイズ 実体化しても配列のサイズにはならない

C言語で大規模開発をしている所はまだまだあります。
そして悲しいことに、同じようなバグがあるのを発見し、頭を抱えたことがある人は実際にいるんじゃないでしょうか。
(もちろん私はありました・・・)

不幸を繰り返さないためにも、C言語を理解せずに使っている人に、少しでもヤバさが伝われば幸いです。