【C言語】
二重解放(double free)対策の
よくある間違い

二重解放 double free

warning: double-‘free’ of ‘p’ [CWE-415]

警告:二重解放
[-Wanalyzer-double-free]


■free(ポインタ)でポインタはNULLになる?

#include <stdio.h>
#include <stdlib.h>
int main(void){
    char *ptr = malloc(256);
    if(ptr == NULL)
        return  1;  
    
    free(ptr);//解放
    if(ptr == NULL)
        puts("freeしたらNULLになる");
    else
        puts("freeしてもNULLにはならない");  
}

freeしたら
free関数がポインタをNULLにしてくれると
思っている人は多いですが
NULLにはならず元のままです。

参考:

CFAQ-4.8: 呼んだ側のポインターの値は変わらなかった。


■二重解放バグの例

#include <stdio.h>
#include <stdlib.h>
int main(void){
    char    *p = malloc(1024);
    free(p);
    free(p);//二重に解放している
}

二重解放はメモリが破壊され大きな問題になります。

gcc -fanalyzer
gcc -fsanitize=address
で問題検出可能です。


■単純な対策

#include <stdio.h>
#include <stdlib.h>
int main(void){
    char    *p = malloc(1024);
    free(p);
    p = NULL ;  //free直後にNULLを設定する
    free(p);    //free(NULL)は問題ない二重解放にならない    
    p = NULL ;  //これは無駄 CERT-C MEM01-EX1 参照
}

良く知られた対策に 
free()直後にNULLを設定する手法があります。

参考:CERT-C MEM01-C


■間違った対策(関数化1版)

#include <stdio.h>
#include <stdlib.h>
void    MY_free(void *ptr){
    free(ptr);
    ptr = NULL ;//★NG:無駄意味が無い
}
int main(void){
    char    *p = malloc(1024);
    MY_free(p);
    MY_free(p);
}

毎回NULLを設定するのは面倒なので関数化したくなりますが、
MY_freeは初心者がよく書く間違った関数化です。
5行目でNULLを設定しても、
関数呼び元のpは何も変わりません。


■間違った対策(関数化2版)

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

void MY_free(void **vp){//★NG:C-FAQ 4.9
    free(*vp);
    *vp = NULL;
}
int main(void){
    char    *p = malloc(1024);
    MY_free(&p);
    MY_free(&p);
}

コンパイラに怒られます。

warning: incompatible pointer types passing ‘char **’ to parameter of type ‘void **’


■間違った対策(関数化3版)

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

void MY_free(void *vp){
    void    **vpp = vp;//★NG:C-FAQ 4.9
    free(*vpp);
    *vpp = NULL;
}
int main(void){
    char    *p = malloc(1024);
    MY_free(&p);
    MY_free(&p);
}

コンパイラの警告は消えましたが
void ** は移植性が無いと C-FAQ4.9 に書いてありました。
さらに呼び元の 
MY_free(&p)で&を忘れる可能性が高いです。


■推奨する対策(マクロ版)

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

//二重解放対策マクロ
#define     free(x)     (free(x),(x)=NULL)

int main(void){
    char    *p = malloc(1024);
    free(p);
    free(p);
}

マクロを使うと簡単です。
free呼び元の変更も不要です。
マクロ定義が再帰しそうですが大丈夫!
問題なくコンパイルできます。


■練習問題

次のうちで正しいのはどれでしょう?

(1) free(NULL)で
 プログラムが異常終了する可能性がある
(2)fclose(NULL)で
 プログラムが異常終了する可能性がある
(3)free(ptr)を実行すると
 free関数がptrをNULLにしてくれる
(4)free()関数はメモリを放する
(5)free()関数はメモリを放する

参考:

CWE-415: Double Free

Use After Free