【C言語】
メモリリーク
-推奨する検出方法とエラー処理-

warning: leak of ‘xp’ [CWE-401]

警告:mallocで獲得したメモリ領域が解放されていない

[-Wanalyzer-malloc-leak]

この記事の概要

この記事では、C言語のメモリリーク検出と推奨されるエラー処理方法を解説しています。静的解析ツール(-fanalyzer)や動的解析ツール(-fsanitize=leak)の活用法を紹介し、リソースリークを防ぐためのgotoを使ったエラー処理が推奨されています。一方で、do-while(0)を用いる非推奨の方法についても議論されています。具体例を通じて、安全で効率的なメモリ管理の重要性を強調しています。


メモリリークの例

#include <stdio.h>
#include <stdlib.h>
void    leak(size_t x,size_t y,size_t z){
    //メモリ獲得処理
    if(x == 0)  
        return ;
    size_t *xp = malloc(x);
    if(xp == NULL){
        perror(NULL);
        return  ;
    }
    *xp = x;
    
    if(y == 0)  
        return ;//メモリリーク
    size_t *yp = malloc(x*y);
    if(yp == NULL){
        perror(NULL);
        return ;//メモリリーク
    }
    *yp = y;

    if(z == 0)  
        return ;//メモリリーク
    size_t *zp = malloc(x*y*z);
    if(zp == NULL){
        perror(NULL);
        return ;//メモリリーク
    }
    *zp = z;

    //本処理
    printf("%zx:%zx:%zx\n",*xp,*yp,*zp);
    
    //解放処理
    free(xp);
    free(yp);
    free(zp);
}
int main(void){
    for(size_t x = 0;x<0x10;x++){
        for(size_t y = 0;y<0x10;y++){
            for(size_t z = 0;z<0x10;z++){
                leak(x,y,z);
            }
        }
    }
}

ハイライトの行でfreeしないでreturn するので
メモリリークが発生します。

リークするメモリの量が少ないと
3年掛かってシステムダウンする事も
あるとかないとか。

gcc -fanalyzer -gでメモリリークを検出する(静的解析)

gcc  -fanalyzer -g bug.c
In function ‘leak’:
bug.c:15:9: warning: leak of ‘xp’ [CWE-401] [-Wanalyzer-malloc-leak]
   15 |         return ;//メモリリーク
      |         ^~~~~~
  ‘leak’: events 1-7
    |
    |    5 |     if(x == 0)
    |      |       ^
    |      |       |
    |      |       (1) following ‘false’ branch (when ‘x != 0’)...
    |    6 |         return ;
    |    7 |     size_t *xp = malloc(x);
    |      |                  ~~~~~~~~~
    |      |                  |
    |      |                  (2) ...to here
    |      |                  (3) allocated here
    |    8 |     if(xp == NULL){
    |      |       ~
    |      |       |
    |      |       (4) assuming ‘xp’ is non-NULL
    |      |       (5) following ‘false’ branch (when ‘xp’ is non-NULL)...
    |......
    |   12 |     *xp = x;
    |      |     ~~~~~~~
    |      |         |
    |      |         (6) ...to here
    |......
    |   15 |         return ;//メモリリーク
    |      |         ~~~~~~
    |      |         |
    |      |         (7) ‘xp’ leaks here; was allocated at (3)
    |
bug.c:24:9: warning: leak of ‘xp’ [CWE-401] [-Wanalyzer-malloc-leak]

gccでコンパイル時に-fanalyzerオプションを付けると
メモリリークが発生するまでの詳細なトレース情報を表示してくれます。


gcc -fsanitize=leak -gでメモリリークを検出する(動的解析)

 $gcc -fsanitize=leak bug.c -g
$./a.out
=================================================================
==117020==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 14400 byte(s) in 225 object(s) allocated from:
    #0 0x7f897864d302 in __interceptor_malloc ../../../../src/libsanitizer/lsan/lsan_interceptors.cpp:75
    #1 0x55df0d8a9218 in leak /mnt/f/メモリリーク/bug.c:16
    #2 0x55df0d8a931c in main /mnt/f/メモリリーク/bug.c:44
    #3 0x7f8978439d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

Direct leak of 3720 byte(s) in 465 object(s) allocated from:
    #0 0x7f897864d302 in __interceptor_malloc ../../../../src/libsanitizer/lsan/lsan_interceptors.cpp:75
    #1 0x55df0d8a91d7 in leak /mnt/f/メモリリーク/bug.c:7
    #2 0x55df0d8a931c in main /mnt/f/メモリリーク/bug.c:44
    #3 0x7f8978439d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

SUMMARY: LeakSanitizer: 18120 byte(s) leaked in 690 allocation(s)

gccでコンパイル時に
-fsanitize=leakオプションを付けて、
プログラムを走らせると
メモリリークが発生する箇所の
ソースコードの行数を表示してくれます。


参考:

CWE-401: 有効期間後のメモリの解放が欠落している

CWE-416: Use After Free

C-FAQ7.20: 動的に割り当てた記憶領域は解放した後には使えないね。

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


■リソースリークの例

#include    <stdio.h>
#include    <stdlib.h>
#include    <unistd.h>
void    f(void){
    FILE    *fp1 = NULL;
    FILE    *fp2 = NULL;
    FILE    *fp3 = NULL;

    fp1 = fopen("/usr/include/stdio.h","r") ;
    if(fp1 == NULL) {
        perror("stdio.h");
        return  ;
    }
    fp2 = fopen("/usr/include/unistd.h","r") ;
    if(fp2 == NULL) {
        perror("unistd.h");
        return  ;//fp1解放漏れ
    }
    fp3 = fopen("/usr/include/windows.h","r") ;
    if(fp3 == NULL) {
        perror("windows.h");
        return  ;//fp1,fp2解放漏れ
    }
    
    puts("本体処理~色々");
    
    //  後始末
    fclose(fp3);
    fclose(fp2);
    fclose(fp1);
}
int main(void){
    for(;;){
        f();
        usleep(10000);
    }
}

fopen()で確保した領域を解放し忘れると
解放漏れ(メモリリーク)が発生します。
この例では fp1とfp2の解放漏れが発生します。

このように動的資源を複数回獲得した場合
解放漏れが発生しやすいです。


■gotoを使った推奨エラー処理

#include    <stdio.h>
#include    <stdlib.h>
#include    <unistd.h>
void    f(void){
    FILE    *fp1 = NULL;
    FILE    *fp2 = NULL;
    FILE    *fp3 = NULL;

    fp1 = fopen("/usr/include/stdio.h","r") ;
    if(fp1 == NULL) {
        perror("stdio.h");
        goto end ;
    }
    fp2 = fopen("/usr/include/unistd.h","r") ;
    if(fp2 == NULL) {
        perror("unistd.h");
        goto end ;
    }
    fp3 = fopen("/usr/include/windows.h","r") ;
    if(fp3 == NULL) {
        perror("windows.h");
        goto end ;
    }

    puts("本体処理~色々");
       
end://  後始末   
    if(fp3) fclose(fp3);
    if(fp2) fclose(fp2);
    if(fp1) fclose(fp1);
}
int main(void){
    for(;;){
        f();
        usleep(10000);
    }
}

 
関数末尾に解放処理を設け
そこにエラー発生時 
goto で飛ぶ方法を推奨します。


■do-while(0)を使った推奨エラー処理

#include    <stdio.h>
#include    <stdlib.h>
#include    <unistd.h>
void    f(void){
    FILE    *fp1 = NULL;
    FILE    *fp2 = NULL;
    FILE    *fp3 = NULL;
    do {
        fp1 = fopen("/usr/include/stdio.h","r") ;
        if(fp1 == NULL) {
            perror("stdio.h");
            break ;
        }
        fp2 = fopen("/usr/include/unistd.h","r") ;
        if(fp2 == NULL) {
            perror("unistd.h");
            break  ;
        }
        fp3 = fopen("/usr/include/windows.h","r") ;
        if(fp3 == NULL) {
            perror("windows.h");
            break  ;
        }

        puts("本体処理~色々");//字下げが深くなる

    } while(0);
    //  後始末
    if(fp3) fclose(fp3);
    if(fp2) fclose(fp2);
    if(fp1) fclose(fp1);
}
int main(void){
    for(;;){
        f();
        usleep(10000);
    }
}

このコードはエラーが発生したら、
breakして、
末尾のエラー処理までジャンプする
テクニックです。

可読性のためと称してgoto文を禁止し
do-while(0)を使って
字下げを深くするのは
本末転倒では?と考えるのは筆者だけ?