
ダングリングポインタ(英: dangling pointer)、またはワイルドポインタ(英: wild pointer)は、コンピュータプログラミングにおいて適切な型の有効なオブジェクトを指していないポインタのことである。これらはメモリ安全性違反の特殊なケースである。より一般的には、有効な宛先に解決されない参照をダングリング参照(英: dangling reference)およびワイルド参照(英: wild reference)と呼ぶ。
ダングリングポインタはオブジェクトの破棄時に発生する。特定のポインタが指しているオブジェクトが削除または割り当て解除された際に、そのポインタの値が変更されず、割り当て解除されたメモリ上の位置を指し続けたままになることによって生じる。システムが以前に解放されたメモリを再割り当てした場合、プログラムがその(現在ダングリング状態の)ポインタを間接参照すると、メモリには全く異なるデータが含まれている可能性があるため、未定義動作による予測不可能な動作が発生する可能性がある。プログラムがダングリングポインタによって参照されるメモリに書き込みを行った場合、無関係なデータが暗黙のうちに破損し、発見が極めて困難な潜在的バグを引き起こす恐れがある。メモリが別のプロセスに再割り当てされていた場合、ダングリングポインタを間接参照しようとするとセグメンテーション違反(UNIX、Linux)や一般保護違反(Windows)が発生する可能性がある。
プログラムにカーネルのメモリ割り当て器が使用する管理データを上書きできる十分な権限がある場合、この破損によってシステムの不安定化を招く恐れがある。ガベージコレクションを備えたオブジェクト指向言語では、到達不可能なオブジェクト(つまり、外部からのポインタを持たないオブジェクト)のみを破棄することでダングリング参照を防いでおり、これはトレースまたは参照カウントによって保証される。しかし、ファイナライザがオブジェクトへの新たな参照を作成する可能性があり、ダングリング参照を防ぐためにオブジェクトの復活が必要になる場合がある。
ワイルドポインタ(初期化されていないポインタとも呼ばれる)は、既知の状態への初期化が行われる前にポインタが使用された場合に発生し、一部のプログラミング言語で起こり得る。これらはダングリングポインタと同様の不安定な動作を示すが、宣言された変数が初期化される前にアクセスされると、多くのコンパイラがコンパイル時に警告を出すため、検出されずに放置される可能性は比較的低い。
多くの言語(例えばC言語)において、メモリからオブジェクトを明示的に削除したり、リターン時にスタックフレームを破棄したりしても、関連するポインタは変更されない。そのメモリ領域が他の目的で使用されるようになったとしても、ポインタは依然としてメモリ内の同じ場所を指している。
簡単な例を以下に示す。
{
char* dp = NULL;
// ...
{
char c;
dp = &c;
}
// cはスコープ外になる
// dpはここでダングリングポインタとなる
}
オペレーティングシステムが実行時にヌルポインタへの参照を検出できる場合、上記に対する解決策は、内部ブロックを抜ける直前に`dp`に0(ヌル)を割り当てることである。別の解決策としては、さらなる初期化なしに`dp`が二度と使用されないことを何らかの方法で保証することである。
ダングリングポインタのもう一つの頻繁な原因は、`malloc()`と`free()`のライブラリ呼び出しが入り混じった組み合わせである。ポインタが指すメモリブロックが解放されると、そのポインタはダングリング状態になる。前の例と同様に、これを避ける一つの方法は、以下に示すように、参照を解放した後にポインタを必ずヌルにリセットすることである。
#include <stdlib.h>
void func() {
char* dp = (char*)malloc(sizeof(char) * 10);
// ...
free(dp); // dpはダングリングポインタになる
dp = NULL; // dpはダングリング状態ではなくなる
// ...
}
非常にありがちなミスとして、スタックに割り当てられたローカル変数のアドレスを返してしまうことがある。呼び出された関数が戻ると、これらの変数のための領域は割り当て解除され、技術的にはそれらは「ゴミの値」となる。
int* func(void) {
int num = 1234;
// ...
return #
}
`func`を呼び出した直後であれば、ポインタからの読み取りが依然として正しい値(1234)を返すこともあるが、その後に呼び出された関数が`num`に割り当てられていたスタック領域を他の値で上書きする可能性があり、ポインタは正しく機能しなくなる。もし`num`へのポインタを返す必要がある場合は、`num`は関数を超えたスコープを持たなければならず、たとえば静的変数(`static`)として宣言されるべきである。
アントニ・クレチュマル(Antoni Kreczmar, 1945–1996)は、ダングリング参照現象が発生しない完全なオブジェクト管理システムを作成した。同様のアプローチは、FisherとLeBlancによって「ロックとキー(Locks-and-keys)」という名称で提案されている。
ワイルドポインタは、最初の使用前に必要な初期化を省略することによって作成される。したがって、厳密に言えば、初期化を強制しないプログラミング言語におけるすべてのポインタは、ワイルドポインタとして始まる。
これは初期化を省略したことよりも、初期化処理を飛び越えて(ジャンプして)しまったことによって発生することが最も多い。ほとんどのコンパイラはこれについて警告を出すことができる。
int f(int i) {
char* dp; // dpはワイルドポインタである
static char* scp; /* scpはワイルドポインタではない。
* 静的変数は開始時に0に初期化され、
* その後は前回の呼び出しからの値を保持する。
* この機能の使用は、コメントがない場合は
* 悪いスタイルと見なされることがある */
}
バッファオーバーフローのバグと同様に、ダングリングポインタやワイルドポインタのバグは頻繁にセキュリティホールとなる。例えば、ポインタが仮想関数の呼び出しに使用された場合、仮想関数テーブル(vtable)のポインタが上書きされていることによって、別の(おそらくエクスプロイトコードを指す)アドレスが呼び出される可能性がある。あるいは、ポインタがメモリへの書き込みに使用された場合、他のデータ構造が破損する可能性がある。ポインタがダングリング状態になった後にメモリが読み取られるだけであっても、(次にそこに割り当てられた構造体に興味深いデータが置かれた場合)情報漏洩につながったり、(現在無効となったメモリがセキュリティチェックに使用された場合)権限昇格攻撃につながる恐れがある。新しいメモリチャンクを割り当てることなく、解放された後にダングリングポインタが使用される場合、これはUse After Free(解放後使用)の脆弱性として知られている。例えば、CVE-2014-1776はMicrosoft Internet Explorer 6から11におけるUse After Freeの脆弱性であり、これは持続的標的型攻撃(APT)によるゼロデイ攻撃で利用された。Use After Freeはメモリ破壊の脆弱性(CWE-416)として定義されている。
C言語における最も単純なテクニックは、ポインタのリセットを保証する代替バージョンの`free()`(または類似の)関数を実装することである。しかし、この手法は、ポインタのコピーを含んでいる可能性のある他のポインタ変数まではクリアしない。
#include <assert.h>
#include <stdlib.h>
// 安全なバージョンのfree()
static void safeFree(void** pp) {
// デバッグモードでは、ppがNULLであればアボートする
assert(pp);
// free(NULL)は適切に機能するため、デバッグモードのassert以外にチェックは不要である
free(*pp); // チャンクを割り当て解除する(free(NULL)が有効であることに注意)
*pp = NULL; // 元のポインタをリセットする
}
int f(int i) {
char* p = NULL;
char* p2;
p = (char*)malloc(1000); // チャンクを取得する
p2 = p; // ポインタをコピーする
// ここでチャンクを使用する
safeFree((void**)&p); // 安全に解放する。p2変数には影響しない
safeFree((void**)&p); // pはNULLにリセットされているため、この2回目の呼び出しは失敗しない
char c = *p2; // p2は依然としてダングリングポインタであるため、これは未定義動作となる
return i + c;
}
この代替バージョンは、`malloc()`を呼び出す前に空のポインタの妥当性を保証するためにも使用できる。
safeFree(&p); // チャンクが解放されたか確信がない場合
p = (char*)malloc(1000); // 今すぐ割り当てる
これらの用途は`#define`ディレクティブを通じて隠蔽し、便利なマクロ(一般的なものとしては`#define XFREE(ptr) safeFree((void**)&(ptr))`)を構築してメタ言語のようなものを作成したり、独立したツールライブラリに組み込んだりすることができる。いずれの場合も、このテクニックを使用するプログラマは、本来`free()`が使用されるべきすべての場所で安全なバージョンを使用する必要があり、そうしなければ再び同じ問題を引き起こすことになる。また、この解決策は単一のプログラムやプロジェクトのスコープに限定されるため、適切に文書化されるべきである。
より構造化された解決策のうち、C++でダングリングポインタを回避する一般的なテクニックはスマートポインタを使用することである。スマートポインタは通常、オブジェクトを回収するために参照カウントを使用する。その他のテクニックとして、トゥームストーン方式やロックとキー方式などがある。
別のアプローチとしては、CおよびC++の標準的なメモリ割り当て関数をガベージコレクタに置き換える保守的なガベージコレクタである、Boehm GC(Boehm garbage collector)を使用することである。このアプローチは、明示的な解放を無効にし、ガベージコレクションによってオブジェクトを回収することで、ダングリングポインタのエラーを完全に排除する。
さらに別のアプローチは、CHERI(Capability Hardware Enhanced RISC Instructions)のようなシステムを使用することである。これは、ポインタの生存期間情報を含めることで無効なアクセスを防ぐための追加のメタデータとともにポインタを保存する。CHERIは通常、これらの追加チェックを実行するためにCPUでのサポートを必要とする。
Javaのような言語では、明示的にメモリの割り当てを解除するメカニズムがないため、ダングリングポインタは発生しない。その代わり、ガベージコレクタがメモリを割り当て解除するが、それはオブジェクトがどの参照からも到達できなくなった場合に限られる。
Rust言語では、型システムが拡張されて変数の生存期間やRAII(Resource Acquisition Is Initialization)も含まれるようになった。言語の機能を意図的に無効にしない限り、ダングリングポインタはコンパイル時に捕捉され、プログラミングエラーとして報告される。
ダングリングポインタのエラーを明らかにするための一般的なプログラミングテクニックの1つは、ポインタが指す記憶領域が解放された後、ポインタにヌルポインタや無効なアドレスを設定することである。ヌルポインタが間接参照されると(ほとんどの言語において)プログラムは即座に終了するため、データ破損や予測不可能な動作の可能性がなくなる。これにより、根本的なプログラミングのミスを発見し、解決するのが容易になる。ただし、このテクニックはポインタのコピーが複数存在している場合には役に立たない。
一部のデバッガは、解放されたデータを自動的に上書きして破壊する。通常は`0xDEADBEEF`などの特定のパターンが使用される(例えば、MicrosoftのVisual C/C++デバッガは、何が解放されたかに応じて`0xCC`、`0xCD`、または`0xDD`を使用する)。これは、データを無用なものにすることで再利用を防ぎ、同時に非常に目立つようにする(このパターンは、メモリがすでに解放されていることをプログラマに示す役割を果たす)。
ダングリングポインタの使用を検出するために、Polyspace、TotalView、Valgrind、Mudflap、AddressSanitizerなどのツール、またはLLVMベースのツールを使用することもできる。
その他のツール(SoftBound、Insure++、CheckPointerなど)は、ソースコードを計装してポインタの正当な値(「メタデータ」)を収集・追跡し、各ポインタへのアクセスが妥当であるかをメタデータと照合して確認する。
少数のクラスが疑わしい場合の別の戦略として、それらのメンバ関数を一時的にすべて仮想関数(virtual)にする方法がある。クラスのインスタンスが破棄・解放された後、仮想関数テーブルへのポインタは`NULL`に設定されるため、メンバ関数の呼び出しはすべてプログラムをクラッシュさせ、デバッガ上で原因となったコードを特定できるようになる。
Linuxシステムではデフォルトで無効になっているが、Android 16で有効にできるARM64メモリタグ付け拡張機能(MTE)は、Use After Freeやバッファオーバーフローを検出するとセグメンテーション違反を引き起こす。