メモリ安全性とは、バッファオーバーフローやダングリングポインタなど、メモリアクセスを扱う際に発生するさまざまなソフトウェアバグやセキュリティ脆弱性から保護されている状態のことです。[ 1 ]例えば、Java は実行時エラー検出で配列の境界やポインタの逆参照をチェックするため、メモリが安全です。 [ 1 ]対照的に、C、C++、Fortranなどのプログラミング言語では、境界チェックの仕組みがない直接メモリ アドレスとして実装されたポインタを使用して任意のポインタ演算が許可されているため、[ 2 ]メモリが安全ではありません。[ 3 ]メモリが安全でないコードは、通常、低レベルのプログラミングで見られ、高レベルのプログラミング言語では、一般的にガベージ コレクションまたは静的解析( Rustなど) を組み込んで、このようなエラーを防止しています。
メモリ エラーは、フォーク ボムなどの問題を回避するために、リソース管理 (コンピューティング)およびタイム シェアリングシステムの文脈で最初に検討されました。[ 4 ]フィンガードのバッファ オーバーフローを悪用したMorris ワームが登場するまで、開発は主に理論的なものでした。[ 5 ]その後、コンピュータ セキュリティの分野は急速に発展し、リターン トゥ libc 攻撃などの多数の新しい攻撃と、実行不可能なスタック[ 6 ]やアドレス空間レイアウトのランダム化などの防御技術が急増しました。ランダム化はほとんどのバッファ オーバーフロー攻撃を防ぎ、攻撃者がアドレスを取得するためにヒープ スプレーやその他のアプリケーション依存の方法を使用する必要があるものの、その採用は遅れています。[ 5 ]ただし、この技術の展開は通常、ライブラリとスタックの位置のランダム化に限定されています。
2019年、マイクロソフトのセキュリティエンジニアは、セキュリティ脆弱性の70%がメモリの安全性の問題によって引き起こされていると報告しました。[ 7 ] 2020年には、 Googleのチームも同様に、 Chromiumの「深刻なセキュリティバグ」の70%がメモリの安全性の問題によって引き起こされていると報告しました。重要なソフトウェアにおける他の多くの著名な脆弱性やエクスプロイトは、最終的にはメモリの安全性の欠如に起因しており、Heartbleed [ 8 ]やsudoの長年にわたる権限昇格バグ[ 9 ]などが含まれます。メモリの安全性の問題から生じる脆弱性やエクスプロイトの蔓延と深刻さから、複数のセキュリティ研究者は、メモリの安全性の問題を特定することを「樽の中の魚を撃つ」と表現しています。[ 10 ]
現代の高水準プログラミング言語の中には、デフォルトでメモリが安全なものもありますが、完全に安全とは言えません。なぜなら、それらの言語は自身のコードのみをチェックし、やり取りするシステムをチェックしないからです。ガベージコレクションという形での自動メモリ管理は、メモリ安全性の問題を防ぐ最も一般的な手法です。これは、言語ランタイム内で割り当てられたすべてのデータに対して、解放後使用などの一般的なメモリ安全性エラーを防ぐためです。[ 11 ]すべての配列アクセスに対する自動境界チェックと、生ポインタ演算のサポートがないことを組み合わせれば、ガベージコレクション言語は強力なメモリ安全性の保証を提供します(ただし、外部関数インターフェースの使用など、明示的に安全でないとマークされた低レベル操作については、保証が弱くなる可能性があります)。しかし、ガベージコレクションのパフォーマンスオーバーヘッドにより、これらの言語は特定のパフォーマンスが重要なアプリケーションには適していません。[ 1 ]
手動メモリ管理を使用する言語では、通常、実行時にメモリの安全性は保証されません。代わりに、メモリの安全性は、コンパイラが静的プログラム解析と自動定理証明によって保証するか、プログラマが実行時に慎重に管理する必要があります。[ 11 ]例えば、Rust プログラミング言語はメモリの安全性を確保するために借用チェッカーを実装していますが、 [ 12 ] CとC++はメモリの安全性を保証していません。C と C++ で書かれたソフトウェアの量が膨大であるため、C の静的メモリ解析を提供するCoverityのような外部静的解析ツールの開発が促進されています。 [ 13 ]
DieHard [ 14 ] 、その再設計版である DieHarder [ 15 ]、およびAllinea 分散デバッグ ツールは、独自のランダムな仮想メモリ ページにオブジェクトを割り当てる特殊なヒープ アロケータであり、無効な読み取りと書き込みを、それらを引き起こした正確な命令で停止してデバッグできます。保護はハードウェア メモリ保護に依存するため、オーバーヘッドは通常大きくありませんが、プログラムが割り当てを多用する場合は大幅に増加する可能性があります。[ 16 ]ランダム化はメモリ エラーに対する確率的な保護のみを提供しますが、バイナリを再リンクすることで既存のソフトウェアに簡単に実装できる場合が多くあります。
Valgrindの memcheck ツールは命令セットシミュレータを使用し、コンパイルされたプログラムをメモリチェック仮想マシンで実行することで、実行時メモリエラーのサブセットを確実に検出します。ただし、通常はプログラムの実行速度を 40 倍低下させ、[ 17 ]さらにカスタムメモリ割り当てを明示的に通知する必要があります。[ 18 ] [ 19 ]
ソースコードにアクセスできれば、Boehm ガベージ コレクタのように、ポインタの正当な値 (「メタデータ」) を収集して追跡し、メタデータに対して各ポインタ アクセスの有効性をチェックするライブラリが存在します。[ 20 ]一般的に、トレース ガベージ コレクションとすべてのメモリ アクセスに対するランタイム チェックの挿入を使用することで、メモリの安全性を安全に保証できます。このアプローチにはオーバーヘッドがありますが、Valgrind よりは少なくなります。ガベージ コレクションを使用するすべての言語はこのアプローチを採用しています。[ 1 ] C および C++ の場合、CheckPointer [ 21 ]やAddressSanitizer (平均で 2 倍の速度低下を課す) [ 22 ]など、コンパイル時にコードを変換してランタイムでメモリの安全性チェックを行うツールが多数存在します。
BoundWardenは、コンパイル時の変換と実行時の同時監視技術を組み合わせた新しい空間メモリ強制アプローチです。[ 23 ]
ファジングテストはメモリ安全性のバグを見つけるのに適しており、AddressSanitizerなどの動的チェッカーと組み合わせて使用されることが多い。
メモリエラーにはさまざまな種類があります。[ 24 ] [ 25 ]
言語や環境によっては、他の種類のバグもメモリの安全性を損なう可能性があります。
一部のリストでは、メモリ安全性の一部として、競合状態(共有メモリへの同時読み書き)も含まれる場合があります(例えば、アクセス制御のため)。Rustプログラミング言語は、書き込み側が最大1つ、読み取り側が1つ以上であることを保証することで、デフォルトで多くの種類のメモリ関連の競合状態を防止します。Javaなどの他の多くのプログラミング言語は、メモリ関連の競合状態を自動的に防止しませんが、それでも一般的に「メモリ安全」な言語とみなされています。したがって、競合状態への対策は、言語がメモリ安全であるとみなされるために必ずしも必要ではないと考えられています。