mallocのダブルフリー問題対策(C言語)

プログラミング

プログラミングをしてmallocして確保したメモリをfreeで開放しているけれど、すでに解放済みのアドレスを再度freeしてしまうと致命的な問題になってしまいます。

どうやって、不具合箇所をみつけたり、現場ではどう対応しているかを紹介します。

mallocのダブルフリーってなに?何が起きるの?

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

int main()
{
    char *a;
    a = malloc(10);
    
    free(a);
    free(a);

    return 0;
}

ハイライトのようにfree済みのmallocを再びfreeしようとしています。これが致命的な問題を引き起こします。

実際の実行結果はこちら、

$main
*** Error in `main': double free or corruption (fasttop): 0x00000000008cf010 ***
======= Backtrace: =========
/lib64/libc.so.6(+0x7c91c)[0x7fbc0c75f91c]
/lib64/libc.so.6(+0x877a9)[0x7fbc0c76a7a9]
/lib64/libc.so.6(cfree+0x16e)[0x7fbc0c77010e]
main[0x400555]
/lib64/libc.so.6(__libc_start_main+0xea)[0x7fbc0c7034da]
main[0x40047a]
======= Memory map: ========
00400000-00401000 r-xp 00000000 08:03 13894053                           /home/cg/root/6054590/main
00600000-00601000 r--p 00000000 08:03 13894053                           /home/cg/root/6054590/main
00601000-00602000 rw-p 00001000 08:03 13894053                           /home/cg/root/6054590/main
008cf000-008f0000 rw-p 00000000 00:00 0                                  [heap]
7fbc08000000-7fbc08021000 rw-p 00000000 00:00 0 
7fbc08021000-7fbc0c000000 ---p 00000000 00:00 0 
7fbc0c4cc000-7fbc0c4e2000 r-xp 00000000 08:11 19138674                   /usr/lib64/libgcc_s-7-20170622.so.1
7fbc0c4e2000-7fbc0c6e1000 ---p 00016000 08:11 19138674                   /usr/lib64/libgcc_s-7-20170622.so.1
7fbc0c6e1000-7fbc0c6e2000 r--p 00015000 08:11 19138674                   /usr/lib64/libgcc_s-7-20170622.so.1
7fbc0c6e2000-7fbc0c6e3000 rw-p 00016000 08:11 19138674                   /usr/lib64/libgcc_s-7-20170622.so.1
7fbc0c6e3000-7fbc0c8aa000 r-xp 00000000 08:11 19138625                   /usr/lib64/libc-2.25.so
7fbc0c8aa000-7fbc0caaa000 ---p 001c7000 08:11 19138625                   /usr/lib64/libc-2.25.so
7fbc0caaa000-7fbc0caae000 r--p 001c7000 08:11 19138625                   /usr/lib64/libc-2.25.so
7fbc0caae000-7fbc0cab0000 rw-p 001cb000 08:11 19138625                   /usr/lib64/libc-2.25.so
7fbc0cab0000-7fbc0cab4000 rw-p 00000000 00:00 0 
7fbc0cab4000-7fbc0cadb000 r-xp 00000000 08:11 19138600                   /usr/lib64/ld-2.25.so
7fbc0ccc0000-7fbc0ccc3000 rw-p 00000000 00:00 0 
7fbc0ccd7000-7fbc0ccda000 rw-p 00000000 00:00 0 
7fbc0ccda000-7fbc0ccdb000 r--p 00026000 08:11 19138600                   /usr/lib64/ld-2.25.so
7fbc0ccdb000-7fbc0ccdd000 rw-p 00027000 08:11 19138600                   /usr/lib64/ld-2.25.so
7ffd69954000-7ffd69975000 rw-p 00000000 00:00 0                          [stack]
7ffd699f4000-7ffd699f6000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
/usr/bin/timeout: the monitored command dumped core
sh: line 1: 151220 Aborted                 /usr/bin/timeout 10s main

きれいにdumpして落ちてますね。コメントに「double free or corruption (fasttop)」とでてますので、ダブルフリーかメモリー破壊じゃない?って示唆してますね。

どこでダブルフリーしているのかを探し出す(ダンプログから)

dumpのバックトレースから、ソースコードのどの行でダンプが起きたのかを知る方法があります。

======= Backtrace: =========
/lib64/libc.so.6(+0x7c91c)[0x7fbc0c75f91c]
/lib64/libc.so.6(+0x877a9)[0x7fbc0c76a7a9]
/lib64/libc.so.6(cfree+0x16e)[0x7fbc0c77010e]
main[0x400555]
/lib64/libc.so.6(__libc_start_main+0xea)[0x7fbc0c7034da]
main[0x40047a]

libgccとかlibcの中で落ちているのが「/lib64/libc.so.6」というところで示されています。これは中を見るのは難しいので今回はやめておきます。

その先をみると、自分で書いたプログラムの「main.c」が出てくるので(赤いマーカーのところ)「これかな?」と推測できます。

この「main[0x400555]」を掘り下げて行きます。

linuxであれば、addr2line をいうコンパイラ付属のコマンドを使えばソースコード中のどこの行か特定できます。

addr2line -e main 0x400555

main.c:10

というふうにmain.cの10行目だと、示されます。

arm系socの組み込みlinuxでもgccやccのカスタムされたコンパイラが提供されていれば****-addr2lineという感じのコマンドが提供されていることが多いのでmakeツールのディレクトリを探してみてください。きっと存在します。

ダンプログから特定できない場合

変更したのは自分のプログラムなのに、全く別のマルチスレッドや別のプロセスが先にダンプしてしまうとか無関係のところが先に落ちることがたまにあります。

または、動作は異常だけどダンプしないで動き続けることもたまにあります。

そういうとき、私はprintfデバッグをよく使います。

mallocとfreeしたタイミングをprintfでログ出力してしまうのです。

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

#define MALLOC(a) malloc_degub(a,__FILE__,__LINE__,__func__)
#define FREE(a) free_debug(a,__FILE__,__LINE__,__func__)

void *malloc_degub(size_t size,const char *file_name, const int line, const char *func_name){
    void *ret;
    ret = malloc(size);
    printf("%s [%d] %s malloc(%d) 0x%08x +\n",file_name,line,func_name,size,ret);
    return ret;
}

void free_debug(void *ptr,const char *file_name, const int line, const char *func_name){
    printf("%s [%d] %s free()     0x%08x -\n",file_name,line,func_name,ptr);
    free(ptr);
}

int main()
{
    char *a,*b,*c;
    a = MALLOC(10);
    b = MALLOC(10);

    FREE(b);
    FREE(a);
    <span class="marker-red">FREE(b);</span>

    return 0;
}

2つのmallocとfreeのデバッグ用関数を作成してそれにすべて置き換えてしまいます。

すると、ハイライトのところでダブルフリーを仕込んでいるのですが、実行結果は

$main
main.c [22] main malloc(10) 0x02307010 +
main.c [23] main malloc(10) 0x02308040 +
main.c [25] main free()     0x02308040 -
main.c [26] main free()     0x02307010 -
main.c [27] main free()     0x02308040 -

実行結果を解析すると、main.c[27]で2回同じアドレスをfreeしようとしていることがわかります。

青マーカーでfree済みのアドレスを赤マーカーのところで再度freeしようとしていることがわかります。なので、main.c [25]か、main.c [27]のどちらかが不具合の直接原因になっているようです。

まずはちゃんと設計を見直し正す

こんな問題は開発中、特にコーディング中に少し動作確認やっていると、すぐによく起きてしまいます。(私などはすぐに動かしたくなるので)

当然、1回目か2回目のどちらかのfree処理を削除すれば、解決になるのですが、そこにfreeを入れたのには理由があったはずです。それを無視するわけには行きませんので設計を振り返るか、調べるかして本当に消しても良いのかどうか検討します。

メモリーの不具合は顕在化しにくく、数年という時間をかけて必ず発火し首を締めてきます。しっかり見直し取り除きましょう。マジで。

不具合でお客様にご迷惑をおかけしたり、その影響が大きい場合は、上司やその上の上司、社長、お客様の上司やその上司に報告するのですが、報告の都度、自分のショボさをアピールさせられるため、本当になさけなくなります。時間も相当取られてしまいます。
昔、派遣でプログラム書いていたときに、めでたくさらし首になりました。
恥ずかしくて頭が真っ白になりました。

フェールセーフを入れる

見つかった不具合が修正できたあとに、念の為、フェールセーフを入れます。

NULLポインタをfreeしても無視されるという仕様を利用します。

free後に使用済みポインタを必ずNULLに上書きするだけです。

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

int main()
{
    char *a;

    a = malloc(10);
    
    free(a);
    a = NULL;

    free(a);
    a = NULL;

    return 0;
}

ちゃんちゃん。使用前にfree出来ているかどうか確認してからmallocするもの良いと思います。

まとめ

  • まず不具合の箇所を特定する
  • ダンプログからaddr2lineを使用して特定
  • printfを仕込んで特定
  • デバッグはしっかり設計から見直す
  • フェールセーフを念の為入れる
  • メモリの不具合は数年かけて首を締めてきますので正しく対応しましょう

コメント

タイトルとURLをコピーしました