メモリリークから学ぶ自作 calloc エラーハンドリングの実装
2024-01-15
azblob://2024/01/15/eyecatch/2024-01-15-calloc-overflow-000.jpg

calloc を実装して使用してみたところメモリリークが多発しました。
原因が判明したので自戒としてまとめておきます。

なぜメモリリークが多発したのか
 

結論、size_t * size_t の値が (stdint.h 内で定義されている)SIZE_MAX の値を超えて、メモリがオーバーフローしており、期待通りのメモリ領域を確保できていなかったためです。
どういうことが順を追って確認していきます。

calloc とは
 

FreeBSDより、以下のように説明されています。


The calloc() function allocates space for number objects, each size bytes in length.The result is identical to calling malloc() with an argument of number * size, with the exception that the allocated memory is explicitly initialized to zero bytes.

要するに、任意のデータ型1つを表すために必要なバイト数かける必要な個数分だけメモリを確保し、初期値として、すべての値を 0 埋めにする関数です。

リーク発生の根源
 

上で記述した通り、calloc の処理の中では以下のような計算が行われます。

{任意のデータ型1つを表すために必要なバイト数} * {必要な個数}


これらの値は calloc の size_t 型の引数として渡されて計算します。
まさにこのとき、size_t の最大値を超えた場合に期待通りのメモリを確保することができず、メモリリークが発生していました。
例えば、リークが発生していたテストケースとして、双方の引数に INT_MAX(int の最大値) と LONG_MAX(long の最大値) を渡したときです。
実際に SIZE_MAX, INT_MAX, LONG_MAX の値を確認しながら、メモリがオーバーフローするか確認します。
まず、これらの値はそれぞれ以下の通りです。

- SIZE_MAX: 18,446,744,073,709,551,615 ※実行環境によって差異あり
- INT_MAX:  2,147,483,647
- LONG_MAX: 9,223,372,036,854,775,807


次にそれぞれの値の桁数を確認します。

- 10,000,000,000,000,000,000 < 18,446,744,073,709,551,615 < 100,000,000,000,000,000,000
- 1,000,000,000 < 2,147,483,647 < 10,000,000,000
- 1,000,000,000,000,000,000 < 9,223,372,036,854,775,807 < 10,000,000,000,000,000,000



- $\log 10^{19}$  $<$  $\log {SIZE\_MAX}$  $<$  $\log 10^{20}$
- $\log 10^{9}$  $<$  $\log {INT\_MAX}$  $<$  $\log 10^{10}$
- $\log 10^{18}$  $<$  $\log {LONG\_MAX}$  $<$  $\log 10^{19}$

上記から SIZE_MAX, INT_MAX, LONG_MAX の桁数はそれぞれ 20, 10, 19 桁であることがわかりました。
また、INT_MAX * LONG_MAX 、つまり、10桁と19桁の積が20桁を超えるのは自明であるので、メモリオーバーフローが発生することが確認できました。

解決案 1
 

{任意のデータ型1つを表すために必要なバイト数} * {必要な個数} の値が SIZE_MAX を超えた場合を基準としてエラー処理をする。
実際にコードの起こすとこのような感じになります。

C#include <stddef.h>
#include <stdint.h>
#include <errno.h>

void *my_calloc(size_t count, size_t size)
{
    ...
    //error handling
    if (count * size > SIZE_MAX)
    {
        errno = ENOMEM;
        return (NULL);
    }
    ...
}


これで適切にメモリ領域を確保することができると思ったのですが、よく考えたら count * size を計算している時点でメモリがオーバフローするので没となりました。

解決案 2


計算結果に可逆性があるかどうかを判断基準として、メモリがオーバーフローするかどうかを判定し、エラーハンドリングをする。

C#include <stddef.h>
#include <errno.h>

void *my_calloc(size_t count, size_t size)
{
    ...
    size_t bytes;
    
    bytes = count * size;
    //error handling
    if (bytes / count == size)
    {
        errno = ENOMEM;
        return (NULL);
    }
    ...
}


この判定基準を用いてエラーハンドリングを行ったところ、期待通りの挙動を実現することができました。

まとめ


結果として、計算結果の可逆性を用いることでメモリがオーバーフローしたかどうか判定することで calloc としての期待通りの挙動を実現することができました。
メモリオーバーフローの概念をぼんやりとしか考えれていなかったので、非常にためになった実装でした。

tag C