メモリ安全性とは、バッファオーバーフローやダングリングポインタなど、メモリアクセスを扱う際に発生するさまざまなソフトウェアバグやセキュリティ脆弱性から保護されている状態のことです。[ 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 ] [ 12 ]
言語や環境によっては、他の種類のバグもメモリの安全性を損なう原因となる可能性があります。
一部のリストでは、メモリ安全性の一部として、競合状態(共有メモリへの同時読み書き)も含まれる場合があります(例えば、アクセス制御のため)。Rustプログラミング言語は、書き込み側が最大1つ、読み取り側が1つ以上であることを保証することで、デフォルトで多くの種類のメモリ関連の競合状態を防止します。Javaなどの他の多くのプログラミング言語は、メモリ関連の競合状態を自動的に防止しませんが、それでも一般的に「メモリ安全」な言語とみなされています。したがって、競合状態への対策は、言語がメモリ安全であるとみなされるために必ずしも必要ではないと考えられています。
現代の高級プログラミング言語の中には、デフォルトでメモリが安全なものもありますが、完全に安全とは言えません。なぜなら、それらの言語は自身のコードのみをチェックし、やり取りするシステムをチェックしないからです。ガベージコレクションという形での自動メモリ管理は、メモリ安全性の問題を防ぐ最も一般的な手法です。これは、言語ランタイム内で割り当てられたすべてのデータに対して、解放後使用などの一般的なメモリ安全性エラーを防ぐためです。[ 19 ]すべての配列アクセスに対する自動境界チェックと生ポインタ演算のサポートがないことを組み合わせれば、ガベージコレクションを備えた言語は強力なメモリ安全性の保証を提供します(ただし、外部関数インターフェースの使用など、明示的に安全でないとマークされた低レベル操作については、保証が弱くなる可能性があります)。しかし、ガベージコレクションのパフォーマンスオーバーヘッドにより、これらの言語は特定のパフォーマンスが重要なアプリケーションには適していません。[ 1 ]
手動メモリ管理を使用する言語では、メモリの安全性は通常、ランタイムによって保証されません。代わりに、メモリの安全性は、コンパイラが静的プログラム解析と自動定理証明によって保証するか、プログラマがランタイムで慎重に管理する必要があります。[ 19 ]
静的解析とは、プログラムを実行せずに検査する手法です。プログラム、解析ツール、および外部の前提条件によっては、プログラムが安全、危険、または不確定であると判断される場合があります。
Rustプログラミング言語は、メモリの安全性を確保するために借用チェッカーを実装しています。デフォルトでは、安全性が証明されたプログラムのみの実行を許可します。キーワードは、unsafeチェッカーが安全であると証明できない(ただし、プログラマが実際の使用では安全であると主張する)構造を使用するためのエスケープ ハッチを提供します。その中の実際のコードが不健全性を引き起こす場合、結果は未定義の動作になります。[ 20 ]「unsafe」を含まない Rust およびそれを含むコードのサブセットの正しさを証明するための形式ツールが存在しますunsafe。[ 21 ]
CとC++ はメモリ安全性の保証を提供しません。C と C++ で書かれたソフトウェアの量が膨大であるため、C の静的メモリ解析を提供するCoverityのような外部静的解析ツールの開発が促進されました。 [ 22 ]また、 Frama-Cなどの形式ツールも C で利用可能です。
メモリ安全でない操作をチェックするもう一つの方法は、実行時にチェックを挿入することです。この方法により、安全性の違反箇所を検出できます。すべてのコードパスを実行できる場合、プログラムがメモリ安全であることを証明できる可能性さえあります。
DieHard [ 23 ] 、その再設計版である DieHarder [ 24 ]、およびAllinea 分散デバッグツールは、独自のランダムな仮想メモリ ページにオブジェクトを割り当てる特殊なヒープ アロケータであり、無効な読み取りと書き込みを、それらを引き起こした正確な命令で停止してデバッグできます。保護はハードウェア メモリ保護に依存しているため、オーバーヘッドは通常はそれほど大きくありませんが、プログラムが割り当てを多用する場合は大幅に増加する可能性があります。[ 25 ]
Valgrindの memcheck ツールは命令セットシミュレータを使用し、コンパイルされたプログラムをメモリチェック仮想マシンで実行することで、実行時メモリエラーのサブセットを確実に検出します。ただし、通常はプログラムの実行速度を 40 倍低下させ、[ 26 ]さらにカスタムメモリ割り当てを明示的に通知する必要があります。[ 27 ] [ 28 ]
ソースコードにアクセスできるライブラリには、Boehm ガベージ コレクタのように、ポインタの正当な値 (「メタデータ」) を収集および追跡し、各ポインタ アクセスをメタデータと照合して有効性をチェックするものがあります。[ 29 ] (一般的に、メモリの安全性は、トレース ガベージ コレクションとすべてのメモリ アクセスに対するランタイム チェックの挿入を使用して安全に保証できます。このアプローチにはオーバーヘッドがありますが、Valgrind よりは少なくなります。ガベージ コレクションを使用するすべての言語はこのアプローチを採用しています。 [ 1 ] ) C および C++ には、コンパイル時にコードを変換してランタイムでメモリの安全性チェックを実行するツールが多数存在します。たとえば、CheckPointer [ 30 ]やAddressSanitizerなどがあり、これらは平均して 2 倍の速度低下を伴います。[ 31 ]
その他のアプローチとしては、以下のものがあります。
ファジングテストは、プログラム内のすべてのコードパスを探索しようとするものです。特にAddressSanitizerなどの動的チェッカーと組み合わせて使用すると、メモリ安全性のバグを見つけるのに非常に適しています。
従来のC言語の関数の中には、パラメータに境界値を指定できないものがいくつかあります。例えば、指定strlenされたポインタをゼロバイトが見つかるまで単純に走査するだけで、境界外アクセスが発生する可能性があります。C29strnlenのなどの代替関数は境界値チェックを追加し、サイズパラメータが割り当ての境界値と一致する場合に安全性を確保します。