Scudo Hardened Allocator¶
はじめに¶
Scudo Hardened Allocator は、当初 LLVM Sanitizers' CombinedAllocator に基づく、ユーザーモードアロケータです。優れたパフォーマンスを維持しながら、ヒープベースの脆弱性に対する追加の緩和策を提供することを目的としています。Scudo は現在 Fuchsia のデフォルトアロケータであり、Android 11 以降の Android でも使用されています。
「Scudo」という名前はイタリア語の shield(スペイン語では Escudo)に由来しています。
設計¶
アロケータ¶
Scudo はセキュリティを念頭に置いて設計されましたが、セキュリティとパフォーマンスのバランスをうまく取ることが目標です。高度にチューニング可能で構成可能になるように設計されており、デフォルトの構成を提供していますが、ユーザーが自分のユースケースに最適なパラメータを考案することを推奨します。
アロケータは、以下のように異なる目的に使用される複数のコンポーネントを組み合わせています。
プライマリ・アロケータ: 32 ビットおよび 64 ビットのアーキテクチャに固有な、高速で効率的です。予約済みのメモリ領域を同じサイズのブロックに分割することで、小さなアロケーションサイズを処理します。現在は、2 つのプライマリ・アロケータが実装されており、32 ビットおよび 64 ビットのアーキテクチャに対応しています。コンパイル時のオプションで構成できます。
セカンダリ・アロケータ: 遅く、オペレーティングシステムの基本的なメモリマッピングを利用して、大きなアロケーションサイズを処理します。セカンダリ・バックの割り当てはガードページで囲まれています。こちらもコンパイル時のオプションで構成できます。
スレッド固有のデータレジストリ: 各スレッドのローカルキャッシュの動作を定義します。現在、2 つのモデルが実装されています。各スレッドが独自のキャッシュを保持する排他モデル(ELF TLS を使用)と、スレッドがキャッシュの固定サイズのプールを共有する共有モデルです。
隔離: ブロックがすぐに再利用できるようにするのを防ぎ、破棄操作を遅らせる方法を提供します。保持されているブロックは、特定のサイズ基準に達するとリサイクルされます。これは本質的に遅延フリーリストであり、使用後解放の一部を緩和するのに役立ちます。この機能は、パフォーマンスとメモリフットプリントの点で非常にコストが高く、主にランタイムオプションによって制御され、デフォルトでは無効になっています。
アロケーションヘッダー¶
アロケータによってアプリケーションに返されるヒープメモリの各チャンクには、ヘッダーが先行します。これには次の 2 つの目的があります。
チャンクに関するさまざまな情報を格納し、ヒープ操作の一貫性を確保するために利用できます。
潜在的な破損を検出します。この目的のために、ヘッダーにチェックサムが追加され、ヘッダーにアクセスした際に破損が検出されます(破損したヘッダーにアクセスしないと、破損が検出されません)。
次の情報がヘッダーに保存されます。
プライマリにバックされた割り当てに対するチャンクの領域を識別するチャンクのクラス ID、またはセカンダリにバックされた割り当てに対する 0。
チャンクの状態(使用可能、割り当て済み、または隔離済み)。
割り当て API で使用される潜在的な不一致を検出するための割り当てタイプ(malloc、new、new[]、または memalign)。
再割り当てまたはサイズ指定解除操作に必要なチャンクのサイズ(プライマリ)または未使用バイト数(セカンダリ)。
チャンクのオフセット。つまり、返されたチャンクの先頭からバックエンドの割り当て(「ブロック」)の先頭までのバイト単位の距離。
16 ビットのチェックサム。
このヘッダーは、サポートされているすべてのプラットフォームで 8 バイト以内に収まり、各割り当てにわずかなオーバーヘッドを発生させます。
チェックサムは、グローバルシークレット、チャンクポインター自体、およびチェックサムフィールドをゼロ化したヘッダー 8 バイトの CRC32(ハードウェアサポートにより高速化)を使用して計算されます。暗号的に強力であることは想定されていません。
ヘッダーは、競合を防止するために、アトミックにロードおよび保存されます。2 つの連続したチャンクが異なるスレッドに属する可能性があるため、これは重要です。ローカルコピーで作業し、比較交換プリミティブを使用してヒープメモリ内のヘッダーを更新し、二重フェッチを回避します。
ランダム性¶
ランダム性は、割り当て者が提供する追加のセキュリティに関する重要な要素です。割り当て者は、OS のメモリマッピングプリミティブがメモリ内の(ほとんど)予測不可能な場所にページを提供し、バイナリが ASLR でコンパイルされることを信頼しています。これらの仮定のいずれかが正しくない場合、セキュリティは大幅に低下します。Scudo では、ブロックがプライマリで割り当てられる方法がさらにランダム化され、キャッシュがスレッドに割り当てられる方法がランダム化される可能性があります。
メモリの再利用¶
プライマリ割り当て者とセカンダリ割り当て者は、再利用に関して異なる動作をします。セカンダリマップされた割り当ては、解除時にマップ解除できる一方、プライマリではそうではありません。そのため、プロセスの RSS が着実に増加する可能性があります。これを防止するために、プライマリ内の連続した空きメモリブロックによってカバーされるページを、基盤 OS が許可する場合に解放できます。通常、これはプロセスの RSS にはカウントされず、後続のアクセスでゼロが埋められることを意味します。これは解除パスで行われ、この動作を調整する複数のオプションがあります。
使用¶
プラットフォーム¶
Fuchsia または Android バージョン 11 以降を使用している場合、メモリはすでに Scudo によってサービスされています(Android Svelte 設定では引き続き jemalloc が使用されていることに注意してください)。
ライブラリ¶
割り当て静的ライブラリは、
CMakeルールの恩恵によってLLVMツリーから構築できます。関連テストはscudo_standalone
CMakeルールの恩恵によって実行できます。check-scudo_standalone
プロジェクトに静的ライブラリをリンクするには、リンカーによって異なる
リンカーフラグ(または同等なもの)の使用が必要になる場合があります。さらに、その他のフラグが必要になることもあります。whole-archive
リンクされたバイナリは、Scudoの割り当てと割り当て解除関数を使用する必要があります。
Scudoは次のようにして構築することもできます。
cd $LLVM/compiler-rt/lib
clang++ -fPIC -std=c++17 -msse4.2 -O2 -pthread -shared \
-I scudo/standalone/include \
scudo/standalone/*.cpp \
-o $HOME/libscudo.so
そして、次のように既存のバイナリと使用できます。
LD_PRELOAD=$HOME/libscudo.so ./a.out
Clang¶
最近のバージョンのClang(rL317337以降)では、割り当ての「旧」バージョンは、ターゲットプラットフォームがサポートされている場合は、
コマンドライン引数を使用してコンパイル時にバイナリと一緒にリンクできます。現在、Scudoと互換性のある他のサニタイザーはUBSanだけです(例:-fsanitize=scudo
)。Scudoでコンパイルすると、出力バイナリに対してPIEも適用されます。-fsanitize=scudo,undefined
将来的には、スタンドアロンScudoバージョンに移行します。
オプション¶
割り当てのいくつかの側面は、次のようにプロセスごとに構成できます。
コンパイル時に、
をデフォルトで設定したいオプション文字列に定義します。SCUDO_DEFAULT_OPTIONS
解析されるオプション文字列を返す
関数をプログラム内で定義します。この関数は、デフォルトの可視性を持つ__scudo_default_options
プロトタイプを持つ必要があります。これにより、コンパイル時の定義がオーバーライドされます。extern "C" const char* __scudo_default_options(void)
解析されるオプション文字列を含む環境変数SCUDO_OPTIONSを通じて。この方法で定義されたオプションは、
による定義をオーバーライドします。__scudo_default_options
Scudoに固有のパラメーターを使用して標準
APIを介して。mallopt
オプション文字列を処理するときは、ASanと同様の構文に従い、個別のオプションをコロンで区切って同じ文字列に割り当てることができます。
たとえば、環境変数を使用する場合
SCUDO_OPTIONS="delete_size_mismatch=false:release_to_os_interval_ms=-1" ./a.out
または、関数を使用する場合
extern "C" const char *__scudo_default_options() {
return "delete_size_mismatch=false:release_to_os_interval_ms=-1";
}
使用可能な「文字列」オプションを以下に示します。
オプション |
デフォルト |
説明 |
quarantine_size_kb |
0 |
チャンクの実際の割り当て解除を遅延するために使用される隔離のサイズ(Kb単位)。値を下げるとメモリ使用量が減りますが、軽減の有効性が低下します。負の値は、デフォルト値にフォールバックします。これとthread_local_quarantine_size_kbのどちらもをゼロに設定すると、隔離が完全に無効になります。 |
quarantine_max_chunk_size |
0 |
チャンクを隔離できるサイズ(バイト単位)。 |
thread_local_quarantine_size_kb |
0 |
グローバル隔離のオフロードに使用されるスレッドごとのキャッシュのサイズ(Kb単位)。値を下げるとメモリ使用量が減る可能性がありますが、グローバル隔離との競合が増加する可能性があります。これとquarantine_size_kbのどちらもをゼロに設定すると、隔離が完全に無効になります。 |
dealloc_type_mismatch |
false |
malloc/delete、new/free、new/delete[]などでエラーを報告するかどうか。 |
delete_size_mismatch |
true |
newとdeleteのサイズが一致しない場合にエラーを報告するかどうか。 |
zero_contents |
false |
割り当て時にチャンクのコンテンツをゼロにするかどうかを指定します。 |
pattern_fill_contents |
false |
割り当て時にチャンクのコンテンツをバイトパターンで埋めるかどうかを指定します。 |
may_return_null |
true |
致命的なエラーが発生しなかったときに、NULLポインタを返すことができるかどうか(強制終了しない)を指定します。 |
release_to_os_interval_ms |
5000 |
解放を試行できる最小間隔(ミリ秒)(負の値は再利用を無効化します)。 |
allocation_ring_buffer_size |
32768 |
スタックトレース収集が要求された場合、割り当てリングバッファに保持する過去の割り当ての数。 このバッファは、MTE障害レポートの割り当てと解放のスタックトレースを提供するために使用されます。バッファが大きいほど、(解放と)割り当てと障害の間に無関係な割り当てが多数発生する可能性があります。同期モードMTE障害に(解放と)割り当てのスタックトレースがない場合は、バッファサイズを増やしてみてください。 スタックトレース収集は、scudo_malloc_set_track_allocation_stacks関数を使用して要求できます。 |
例えば、ScudoがGWP-ASanサポートを使用してコンパイルされている場合に追加のフラグを指定できます。
次の「mallopt」オプションが使用できます(オプションはinclude/scudo/interface.h
で定義されています)
オプション |
説明 |
M_DECAY_TIME |
解放間隔オプションを指定された値に設定します(Androidではコンパイル時に指定された最小値と最大値に間隔をそれぞれ設定するために0または1のみが許可されます)。 |
M_PURGE |
直ちにメモリを再利用しますが、すべてを再利用するわけではありません。小さいサイズのクラスでは、時間がかかりすぎるため再利用されないメモリがまだいくらかのメモリに残っています。値は無視されます。 |
M_PURGE_ALL |
M_PURGEと同じですが、時間がかかる場合でも可能な限りのすべてのメモリを解放します。値は無視されます。 |
M_MEMTAG_TUNING |
特定のクラスのメモリエラーが検出される可能性を高くするために、アロケータのメモリタグの選択を調整します。value引数は |
M_THREAD_DISABLE_MEM_INIT |
スレッドごとのメモリ初期化を調整します。0が通常の動作で、1が自動ヒープ初期化を無効にします。 |
M_CACHE_COUNT_MAX |
セカンダリキャッシュにキャッシュできるエントリの最大数を設定します。 |
M_CACHE_SIZE_MAX |
セカンダリキャッシュにキャッシュできるエントリの最大サイズを設定します。 |
M_TSDS_COUNT_MAX |
コンパイル時に指定された制限まで使用できるTSDの最大数を引き上げます。 |
エラータイプ¶
予期しない動作が検出されると、アロケータはエラーメッセージを出力し、場合によってはプロセスを強制終了します。出力は通常"Scudo ERROR:"
で始まり、発生した問題と関係するポインタの簡単な要約が続きます。繰り返しますが、Scudoは軽減策として用意されており、問題の根本原因を調べるのに最も役立つツールではない可能性があります。この目的にはASanを考慮してください。
現在のエラーメッセージとその原因のリストを以下に示します。
"corrupted chunk header"
:チャンクヘッダーのチェックサム検証が失敗しました。これは次の2つの理由が考えられます。ヘッダーが(部分的または完全に)上書きされたか、関数に渡されたポインタがチャンクではありません。「race on chunk header」
: 2 つの異なるスレッドが、同時に同じヘッダーを操作しようとしています。これは通常、そのチャンクで操作を実行する際の競合状態や、全般的なロックの欠如の兆候です。「invalid chunk state」
: チャンクは、特定の操作に対して想定される状態ではありません。たとえば、解放しようとしているときに割り当てられていない、リサイクルしようとしているときに検疫されていないなどです。このエラーが発生する一般的な原因は、double-free です。「misaligned pointer」
: 基本的なアライメント要件を厳密に適用し、32 ビットプラットフォームでは 8 バイト、64 ビットプラットフォームでは 16 バイトです。当方の関数のポインターがこれらの条件に適合しない場合、確実に問題があります。「allocation type mismatch」
: オプションの割り当て解除のタイプの不一致チェックが有効になっている場合、チャンクで呼び出された割り当て解除関数は、それを割り当てるために呼び出された関数のタイプと一致する必要があります。このようなタイプの不一致により、セキュリティに影響が出ることはありませんが、状況によっては悪影響が出る可能性があります。「invalid sized delete」
: C++14 の sized delete 演算子が使用され、オプションのチェックが有効になっている場合、チャンクの割り当て解除時に渡されるサイズは、割り当て時に要求されたサイズと一致しません。これは、Intel C++ Compiler の場合のようにコンパイラの問題が原因であるか、割り当て解除されるオブジェクトでの型の混同が原因である可能性があります。「RSS limit exhausted」
: オプションで指定された最大 RSS を超えています。
その他にも、libc 割り当て API でのパラメータチェックに関連するエラーメッセージがありますが、理解は非常に簡単です。