【C言語】
fclose()を忘れると!?
デバッグ関数で
メモリリークした事例

warning: leak of FILE ‘fp’
[CWE-775]

警告: fopenしたらfcloseしないとメモリリークする
[-Wanalyzer-file-leak]


■この記事の概要

この記事では、C言語のfclose()関数を忘れることによるメモリリークの問題と、その対処法について解説しています。

具体的なコード例を使って、fclose()の多すぎる場合や少なすぎる場合のバグ、goto文を使ったリソース解放の推奨方法が紹介されています。


■ファイルを閉じ忘れた事例

#include <stdio.h>
#include <string.h>
//gcc -fanalyzer

void DEBUG_log(char *str){
    FILE *fp = fopen("DEBUG.log","a");
    if(fp == NULL){
        perror(NULL);
        return  ;
    }
    fprintf(fp,"%s",str);
    //fclose(fp);   忘れた!
}
int main(void){
    for(int i = 0; i< 2000;i++){
        char log[256];
        sprintf(log,"%s:%d id=%d\n",
            __func__,__LINE__,i);
        DEBUG_log(log);
    }
}

単体試験の時は見かけ上問題もなく動作していたのが、

結合試験などで
何度もfopen()を実行して
fclose()していないので
リソース不足でシステム異常を起こし
問題が発覚する場合があります。

参考:CWE-775


warning: double ‘fclose’ of FILE ‘fp’
warning: leak of FILE ‘fp’

警告: fclose多すぎ、少なすぎ
[-Wanalyzer-double-fclose]
[-Wanalyzer-file-leak]


■fcloseが多すぎるバグ例

//gcc -fsanitize=leak   動的解析
//gcc -fanalyzer        静的解析
#include    <stdio.h>
int main(void) {
    FILE    *fp1 = fopen("/usr/include/stdio.h","r");
    if(fp1 == NULL){
        return  1;
    }
    FILE    *fp2 = fopen("テスト用(存在しないファイル)","r");
    if(fp2 == NULL){
        //異常時処理
        perror(NULL);
        fflush(0);
        fclose(fp1);
        //処理続行
    }
    
    //正常時処理
    //異常時と正常時の2回 fclose() してしまった例
    fclose(fp1) ;   
    fclose(fp2) ;
}

同じファイルポインタを2回fclose() してしまった問題です。
異常時処理でfclose() しているのに正常処理でもfclose() してしまう場合が多いです。

この問題は以下のオプションで検出できます。
gcc -fsanitize=leak 動的解析
gcc -fanalyzer 静的解析


■fcloseが少なすぎるバグ例

#include    <stdio.h>
int f(void){
    FILE    *fp1 = fopen("/usr/include/stdio.h","r");
    if(fp1 == NULL) {
        return 1;
    }
    FILE    *fp2 = fopen("テスト用(存在しないファイル)","r");
    if(fp2 == NULL){
        return 1;   //問題:fclose(fp1) 忘れ
    }
    //  処理色々~~~~~
    //  後始末
    fclose(fp1);
    fclose(fp2);
    return 0;
}
int main(void){
    for(int i = 0;i < 1000;i++){
        f();
    }
}

■goto文を使ったfclose問題対策(推奨)

#include    <stdio.h>
int main(void) {
    FILE    *fp1 = NULL ;
    FILE    *fp2 = NULL ;
    
    fp1 = fopen("/usr/include/stdio.h","r");
    if(fp1 == NULL){
        goto err;
    }
    fp2 = fopen("テスト用(存在しないファイル)","r");
    if(fp2 == NULL){
        goto err;   
    }
    //
    //処理色々
    //
err:
    //クリーンナップ処理
    if(fp1) fclose(fp1) ; 
    if(fp2) fclose(fp2) ;
} 

関数末尾に資源解放の
クリーンナップ処理をまとめ、
そこへgoto で飛ぶ手法を推奨します。

注意事項:

fp1,fp2は関数の先頭でNULLで初期化する必要があります。
C99式に使用する直前で変数宣言すると
クリーンナップ処理で未初期化変数を参照する可能性があります。


参考:

MEM12-C. リソースの使用および解放の最中に発生するエラーが原因で関数を終了する場合に、Goto 連鎖の使用を検討する

CWE-1341


fclose(NULL)はSegmentation fault

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void NG_fclose(void){
    char    buf[BUFSIZ];
    FILE *fp = fopen("/usr/include/stdio.h","r");
    if(fp == NULL){
        perror(NULL);
        return  ;
    }
    fgets(buf,BUFSIZ,fp);
    fclose(fp);
    fp = NULL;  //間違い
    fclose(fp);
}
int main(void){
    NG_fclose();
}

fclose(NULL)は問題があります。
多分Segmentation faultします。

●free(NULL)は大丈夫

free(NULL)は問題ありません。
free(NULL)を実行しても何も起きません。