GWP-ASan

はじめに

GWP-ASan は、本番環境における使用後解放とヒープバッファオーバーフローのバグを検出するサンプリングアロケータフレームワークです。これは非公式には再帰的頭字語であり、「GWP-ASan Will Provide Allocation SANity」(GWP-ASan はアロケーションの正気を保障する)を意味します。

GWP-ASan は、重要な適応を備えた古典的な Electric Fence Malloc Debugger に基づいています。具体的には、ごくわずかのパーセンテージのアロケーションのみをサンプリングの対象として選択し、ガードページをこれらのサンプリングされたアロケーションにのみ適用します。サンプリングは十分に限定されているため、パフォーマンスのオーバーヘッドを非常に低く抑えることができます。

プロセスの一生の間固定されている、微小で調整可能なメモリオーバーヘッドがあります。アロケーションの平均サイズによって異なりますが、既定の設定を使用すると、プロセスあたり約 40KiB です。

GWP-ASan と ASan

GWP-ASan は AddressSanitizer と異なり、大幅なパフォーマンスのオーバーヘッドを引き起こしません。ASan は、本番環境で実行可能になるためには専用のカナリアの使用を必要とする場合が多く、それ自体が非現実的であることがよくあります。

GWP-ASan は、ASan によって検出されるメモリの問題のごく一部のみを検出できます。さらに、GWP-ASan のバグ検出機能は確率論的なものに過ぎません。そのため、テスト環境や確実にエラーを検出することが実行の 2 倍の遅延やバイナリサイズの肥大化よりも重要な場合は、ASan を GWP-ASan よりも優先的に使用することをお勧めします。本番環境の大多数の場合、その影響は大きすぎて、GWP-ASan が非常に役に立ちます。

設計

注意: GWP-ASan の実装は大きな揺れがあり、これらの詳細は変更される場合があります。現在、Chromium に用意されている実装など、GWP-ASan の別の実装があります。長期的なサポートの目標は、妥当な範囲で機能の同等性を確保し、compiler-rt をリファレンス実装としてサポートすることです。

アロケータのサポート

GWP-ASan は従来のアロケータの代わりではありません。その代わりに、サンプリング対象のアロケータを GWP-ASan にリダイレクトするようにスタブをサポートするアロケータに挿入します。これらのスタブは一般に malloc()free()realloc() の実装で実装されています。スタブは非常に小さく、ほとんどのアロケータで GWP-ASan を使用するのがかなり容易です。スタブは同じ一般的なパターンに従います (例 malloc() 擬似コード)

#ifdef INSTALL_GWP_ASAN_STUBS
  gwp_asan::GuardedPoolAllocator GWPASanAllocator;
#endif

void* YourAllocator::malloc(..) {
#ifdef INSTALL_GWP_ASAN_STUBS
  if (GWPASanAllocator.shouldSample(..))
    return GWPASanAllocator.allocate(..);
#endif

  // ... the rest of your allocator code here.
}

次に、すべてのサポート アロケータで必要なのは -DINSTALL_GWP_ASAN_STUBS でコンパイルして GWP-ASan ライブラリにリンクすることだけです。パフォーマンス上の理由から、GWP-ASan ライブラリの静的リンクを強くお勧めします。

保護されたアロケーション プール

GWP-ASan のコアは保護されたアロケーション プールです。各サンプリングされたアロケーションは、1 つ以上のアクセス可能なページからなる独自 の *保護された* スロットを使用して裏付けられています。各保護されたスロットは、アクセス禁止としてマップされる 2 つの *ガード* ページに囲まれています。すべての保護されたスロットのコレクションは、*保護されたアロケーション プール* を構成します。

バッファー アンダーフロー/オーバーフローの検出

これらのガード ページでバッファー オーバーフローとバッファー アンダーフローの検出が得られます。メモリ アクセスが割り当てられたバッファーを超過すると、アクセスできないガード ページに触れ、メモリ例外が発生します。この例外は内部クラッシュ ハンドラーによってキャッチされ、処理されます。各アロケーションは割り当てられ、解放された場所 (およびどのスレッドによって) に関するメタデータで記録されるため、バグの根本原因を特定するのに役立つ情報を提供できます。

アロケーションは、アンダーフローとオーバーフローの両方の等しい検出を提供するために、左揃えまたは右揃えのどちらかにランダムに選択されます。

解放後の使用の検出

保護されたアロケーション プールは、解放後の使用の検出も提供します。サンプリングされたアロケーションが解放されるたびに、その保護されたスロットをアクセス不能としてマッピングします。したがって、解放後にメモリにアクセスするとクラッシュ ハンドラーがトリガーされ、エラーのソースに関する有用な情報を提供できます。

サンプリングされたアロケーションの解放後の使用検出は一時的なものであることに注意してください。バグを検出しながらメモリのオーバーヘッドを一定に保つために、解放されたスロットは将来のアロケーションを保護するためにランダムに再利用されます。

使い方

GWP-ASan は、すでに Scudo Hardened Allocator 標準で搭載されてしいます。そのため、-fsanitize=scudo で構築すれば、GWP-ASan を試す最も迅速かつ簡単な方法です。

オプション

GWP-ASan の構成は、サポートするアロケータによって管理されます。Scudo で使用される一般的な構成管理ライブラリを提供します。これにより、次の方法を使用して GWP-ASan のいくつかの側面を構成できます。

  • GWP-ASanライブラリのコンパイル時に、オプション文字列に設定したいオプションを-DGWP_ASAN_DEFAULT_OPTIONSに設定します。GWP-ASanをcompiler-rt/LLVMの一部としてビルドしている場合、cmake構成時に追加します(例 : cmake ... -DGWP_ASAN_DEFAULT_OPTIONS="...")。GWP-ASanをcompiler-rtの外側でビルドしている場合、optional/options_parser.cppをビルド時に-DGWP_ASAN_DEFAULT_OPTIONS="..."を指定します。

  • __gwp_asan_default_options関数をプログラムに定義することで、解析するオプション文字列を返します。この関数は、次のプロトタイプ: extern "C" const char* __gwp_asan_default_options(void)、デフォルトの可視性を持たなければなりません。これによりコンパイル時の定義が上書きされます。

  • 割り当てのサポートによっては(Scudoがこのメカニズムをサポートしています): オプション文字列を含む環境変数を通して。Scudoの場合、SCUDO_OPTIONS=GWP_ASAN_${OPTION_NAME}=${VALUE}(例: SCUDO_OPTIONS=GWP_ASAN_SampleRate=100)を通してです。このように定義されたオプションは__gwp_asan_default_optionsを通して行われたすべての定義を上書きします。

オプション文字列はASanと似た構文に従い、別個のオプションはコロンで区切って同じ文字列の中に割り当てることができます。

例: 環境変数の使用

GWP_ASAN_OPTIONS="MaxSimultaneousAllocations=16:SampleRate=5000" ./a.out

または、関数の使用

extern "C" const char *__gwp_asan_default_options() {
  return "MaxSimultaneousAllocations=16:SampleRate=5000";
}

使用可能なオプションを以下に示します。

オプション

デフォルト

説明

Enabled

true

GWP-ASanは有効ですか?

PerfectlyRightAlign

false

割り当てが右揃えされた場合、ページ境界まで完全に整合させる必要がありますか? デフォルト(false)では、パフォーマンス上の理由により、割り当てサイズを2の累乗(2、4、8、16)の最も近い整数(最大16バイト)に丸めます。これをtrueにすると、パフォーマンスを犠牲にして単一バイトのバッファーオーバーフローが見つかる場合があり、一部のアーキテクチャとの互換性がなくなる可能性があります。

MaxSimultaneousAllocations

16

プールで利用可能な同時に監視される割り当ての数。

SampleRate

5000

ページがGWP-ASanサンプリング用に選択される確率(1 / SampleRate)。最大(2^31 - 1)までのサンプルレートがサポートされています。

InstallSignalHandlers

true

動的ロード中、GWP-ASanのSIGSEGVシグナルハンドラをインストールします。これにより、メモリエラーを報告する際に、割り当てと割り当て解除のスタックトレースを提供することで、エラーレポートが向上します。GWP-ASanのシグナルハンドラはシグナルを以前にインストールされた任意のハンドラに転送し、さらなるシグナルハンドラをインストールするユーザープログラムは同じ処理が行われることを確認する必要があります。注意: 以前にインストールされたSIGSEGVハンドラがSIG_IGNの場合、エラーレポートをダンプした後にプロセスを終了します。

下記のコードには使用後に解放するバグがあり、string_viewstring+演算のテンポラリ結果への参照として作成されています。使用後に解放は、8行目でsvが参照解除されるときに発生します。

1: #include <iostream>
2: #include <string>
3: #include <string_view>
4:
5: int main() {
6:   std::string s = "Hellooooooooooooooo ";
7:   std::string_view sv = s + "World\n";
8:   std::cout << sv;
9: }

このコードをScudo+GWP-ASanでコンパイルすると、このバグが確率的に検出され、詳細なエラーレポートが表示されます。

$ clang++ -fsanitize=scudo -g buggy_code.cpp
$ for i in `seq 1 500`; do
    SCUDO_OPTIONS="GWP_ASAN_SampleRate=100" ./a.out > /dev/null;
  done
|
| *** GWP-ASan detected a memory error ***
| Use after free at 0x7feccab26000 (0 bytes into a 41-byte allocation at 0x7feccab26000) by thread 31027 here:
|   ...
|   #9 ./a.out(_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St17basic_string_viewIS3_S4_E+0x45) [0x55585c0afa55]
|   #10 ./a.out(main+0x9f) [0x55585c0af7cf]
|   #11 /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7fecc966952b]
|   #12 ./a.out(_start+0x2a) [0x55585c0867ba]
|
| 0x7feccab26000 was deallocated by thread 31027 here:
|   ...
|   #7 ./a.out(main+0x83) [0x55585c0af7b3]
|   #8 /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7fecc966952b]
|   #9 ./a.out(_start+0x2a) [0x55585c0867ba]
|
| 0x7feccab26000 was allocated by thread 31027 here:
|   ...
|   #12 ./a.out(main+0x57) [0x55585c0af787]
|   #13 /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7fecc966952b]
|   #14 ./a.out(_start+0x2a) [0x55585c0867ba]
|
| *** End GWP-ASan report ***
| Segmentation fault

これらのスタックトレースを象徴化するには、多少の注意が必要です。Scudoは現在、展開するためにbacktrace_symbols()<execinfo.h>から使用するGNUのbacktrace_symbols()を使用しています。この展開器は、通常のbinary+offsetフォームではなく、function+offsetフォームで人々が判読可能なスタックトレースを提供します。addr2lineや同様のツールを使用して正確な行番号を回復するには、function+offsetbinary+offsetに変換する必要があります。ヘルバースクリプトはcompiler-rt/lib/gwp_asan/scripts/symbolize.shで利用できます。このスクリプトを使用すると、各可能な行を象徴化しようとし、問題が発生した場合は以前の出力を利用します。これにより、次の出力が得られます。

$ cat my_gwp_asan_error.txt | symbolize.sh
|
| *** GWP-ASan detected a memory error ***
| Use after free at 0x7feccab26000 (0 bytes into a 41-byte allocation at 0x7feccab26000) by thread 31027 here:
| ...
| #9 /usr/lib/gcc/x86_64-linux-gnu/8.0.1/../../../../include/c++/8.0.1/string_view:547
| #10 /tmp/buggy_code.cpp:8
|
| 0x7feccab26000 was deallocated by thread 31027 here:
| ...
| #7 /tmp/buggy_code.cpp:8
| #8 /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7fecc966952b]
| #9 ./a.out(_start+0x2a) [0x55585c0867ba]
|
| 0x7feccab26000 was allocated by thread 31027 here:
| ...
| #12 /tmp/buggy_code.cpp:7
| #13 /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7fecc966952b]
| #14 ./a.out(_start+0x2a) [0x55585c0867ba]
|
| *** End GWP-ASan report ***
| Segmentation fault