ダングリングポインタとは|dangling pointerの危険性と回避

ダングリングポインタ

無効なメモリ領域を指すポインタはダングリングポインタ(dangling pointer)と呼ばれる。とりわけ、本来有効だったメモリ領域が、解放処理などによって無効化されたにもかかわらず、そのメモリ領域を参照し続けているポインタのことをダングリングポインタと呼ぶ。

例えば、free関数等によって解放されたオブジェクトを、参照し続けているポインタ変数などは、無効なメモリアドレスを指していることからダングリングポインタの状態にあると言える。このような無効な領域を指すポインタは、誤って利用されると重大なバグを引き起こすことにも繋がるため、そのようなポインタ変数に対しては、空ポインタ(NULL)を代入することで、ダングリングポインタの発生や影響を回避することなどが求められる。

int *pointer = malloc(8); // メモリ確保
free(pointer);            // メモリ解放

pointer;                  // ダングリングポインタ
free(pointer);            // ダングリングポインタへの二重解放はクラッシュ(危険)

pointer = NULL;           // ダングリングポインタの発生を回避
free(pointer);            // 空ポインタへの二重解放は無効化(安全)

なお、ダングリングポインタは「ぶら下がりポインタ」と訳されることもある。ダングリングポインタと似たような用語として、ワイルドポインタ(wild pointer)というものもある(wild = 野生)。こちらは不正なメモリ領域を指すポインタのことを指しており、未初期化のポインタ変数や、誤ったポインタ演算によって発生したポインタ等に対して使われる。

目次

スポンサーリンク

ダングリングポインタの例

解放済みのオブジェクトポインタはダングリングポインタの良い例である。

char *p = malloc(1); // メモリ確保
free(p);             // メモリ解放

p;        // ポインタ変数`p`は既にダングリングポインタ
*p = 'c'; // ダングリングポインタへのアクセス(危険)

また、寿命を迎えたオブジェクトを参照し続けているポインタも、ダングリングポインタとなりうる。

char *p = NULL;
{
  // 自動変数`c`の生存期間はスコープ内のみ
  char c = 'c';
  p = &c; // ローカル変数の参照を保持
}
          // ローカル変数`c`の寿命は既に尽きている
p;        // よってポインタpはダングリングポインタ
printf("%c", *p); // 不定な動作を引き起こすため危険
int *f() {
  int i = 99; // 変数`i`の生存期間は関数内のみ
  return &i;
}

ローカル変数の生存期間に起因するダングリングポインタは、関数へのポインタ渡しを行う際にも影響するため注意しなければならない。次のような、ローカル変数へのポインタを特定のオブジェクト領域(グローバル変数、構造体メンバ変数、等)で継続的に保持続けるような処理は、ダングリングポインタの影響を受ける危険性がある。

int *p;
void f(int *i) { p = i; }

int main() {
  { int i = 99; f(&i); } // ローカル変数のエスケープが発生
  p; // ダングリングポイタとなりうる
}

他にもrealloc関数使用時にメモリブロックの変更が行われてしまうと、元のポインタがダングリングポインタに変化する。

char *a = malloc(1);     // メモリ確保
char *b = realloc(a, 2); // メモリ拡張

if (a != b) {
  b;     // メモリ領域の再確保によりアドレスが変化している
  a;     // ダングリングポインタ
  a = b; // 対応策
}

free(a); // 安全

ワイルドポインタの例

なお、未初期化のポインタ変数はワイルドポインタとなりうる。

int *p; // 未定義動作によりNULLで初期化されない可能性がある
p;      // ワイルドポインタの可能性あり
*p = 9; // 危険

誤ったポインタ演算によって、不正なメモリ領域を指すようになったポインタ変数はワイルドポインタとなる。

const char *s = "abc"; // 4byte分のメモリ領域を利用({'a', 'b', 'c', '\0'})

s = s + 2; // ポインタ演算
*s;        // 'c' (正常)

s = s + 3; // ワイルドポインタに変化
*s;        // '-' (不定な値)
           // ポインタ変数sは不正な領域を指している
           // ポインタ変数sは文字列`"abc"`の記憶領域から外れた領域を指している

ダングリングポインタの危険性

二重解放

ダングリングポインタの存在によって、解放済みのオブジェクトを二重に解放してしまう危険性を生み出す。オブジェクトの二重解放はプログラムのエラーやクラッシュの原因となるため、細心の注意を払う必要がある。

void* ptr = malloc(1);
free(ptr); // 正常
free(ptr); // エラー発生
// malloc: *** error for object 0x100700000: pointer being freed was not allocated
// *** set a breakpoint in malloc_error_break to debug

不正領域へのアクセス

存在しないメモリ領域へのアクセスによって、本来プログラムが意図しないような動作を引き起こす危険性がある。

char *a = malloc(1);
free(a);  // 解放

char *b = malloc(1);
*b = 'b';
*a = 'a'; // ダングリングポインタへの操作
printf("%c", *b); // "a" ("b"ではない!?)

このように全く関係のないメモリ領域を書き換えてしまう危険性がある。これはソフトウェアの脆弱性やセキュリティーの問題にも繋がるため、大変重大な問題と言える。

加えて、このような不正領域へのデリファレンスや書き換えは、構造体ポインタへのメンバアクセス時においても、プログラムのクラッシュや強制終了を引き起こさずにそのまま動いてしまうことが多く、問題の発見も遅れやすいという厄介さがある。

struct Number {
  int value;
  void print() { printf("%d", value); }
};

Number *ptr = new Number; // オブジェクトの動的確保
delete ptr;               // オブジェクトの解放
ptr->value = 9;           // クラッシュしない
printf("%d", ptr->value); // クラッシュしない
ptr->print();             // クラッシュしない(C++言語ではメンバ関数もそのまま呼ばれる)

// 出力結果: "99"

ダングリングポインタの回避

ダングリングポインタによる危険性や問題を回避するするためには、ポインタの初期化作業の徹底や、ソフトウェア設計による回避が必要になる。

ポインタ変数の初期化

使われなくなったポインタ変数を明示的にヌルポインタ(NULL)で初期化することで、ダングリングポインタによる影響を回避できる。

char *a = malloc(1);
free(a);

a = NULL; // ダングリングポインタへの参照を断ち切る

free(a);  // ヌルポインタに対する解放処理は無効化される

*a = 'a'; // エラー発生(問題の早期発見に繋がる)

C言語の場合はヌルポインタに対する解放処理は無効化されるため、ポインタの二重解放の問題にも対処することができる。

またヌルポインタへの操作はプログラムの強制終了やクラッシュを引き起こすことがあり、このことが逆に、脆弱性の早期発見に繋がったり、不正領域へのアクセスに伴うセキュリティーホールの問題を抑えることに繋がることになる。

ソフトウェア設計による回避

また、ダングリングポインタの問題を回避するためには、複数のポインタが同一のオブジェクトを参照しないような設計を意識する必要がある。

他にも生のポインタを直接やり取りするのではなく、カプセル化の技法やアクセサ、デストラクタの仕組みを用いてオブジェクトのやり取りや操作をより抽象化したり制限することが求められる。先程のrealloc関数の例ような、副作用を伴うAPI設計も極力回避する必要がある。

スマートポインタやARC(特殊なメモリ管理機構)における弱参照と強参照の概念をソフトウェア設計に取り入れることも有効的である。例えばARC(参照カウンタ方式)で弱参照指定されたポインタは、ダングリングポインタになる前に自動的にヌルポインタによる初期化が行われるため、ダングリングポインタの問題が起こらないようになっている。

ARCの実現は難しいが、スマートポインタなら一部の言語で比較的容易に実装ができる。中にはスマートポインタを標準でサポートしている言語も存在する(C++等)。

ただしスマートポインタを用いた設計には自前によるガード処理やアンラップ処理が必要になるため、利用の手間が掛かるという欠点がある。代替案としては、ARCやガベージコレクタを取り入れた言語への移行が考えられる。

プログラミング言語による回避

ガーベッジコレクションを採用したJavaや、エスケープ解析が強力なGo等の言語を用いることで、ダングリングポインタとは無縁のプログラミング環境を実現することができる。またARCを採用した言語(Swift, Objective-C)や次世代のメモリ管理方式(Ownershipモデル等)を採用したRust等の言語に移行する手もある。

ポインタを直接的に扱わない次世代のプログラミング言語やスクリプト言語(Ruby, Python, JavaScript)の利用も適切である。

広告