GWP-ASan

GWP-ASan はネイティブ メモリ割り当て機能であり、解放後の使用のバグと、ヒープバッファ オーバーフローのバグを見つけられるようにします。GWP-ASan は再帰的頭字語であり、正式名称は「GWP-ASan Will Provide Allocation SANity」です。HWASanMalloc Debug とは異なり、GWP-ASan はソースや再コンパイルを必要とせず(つまり、プリビルドで動作します)、32 ビットプロセスと 64 ビットプロセスの両方で機能します(ただし、32 ビットのクラッシュの場合、デバッグ情報が少なくなります)。このトピックでは、アプリでこの機能を有効にするために必要なアクションの概要を説明します。GWP-ASan は Android 11(API レベル 30)以上をターゲットとするアプリで使用できます。

概要

ランダムに選択された一部のシステム アプリケーションやプラットフォームの実行可能ファイルでは、GWP-ASan は、プロセス起動時(または Zygote でのフォーク時)に有効になります。独自のアプリで GWP-ASan を有効にすると、メモリ関連のバグを見つけやすくなり、ARM Memory タグ付け拡張機能(MTE)のサポートにも役立ちます。割り当てサンプリング メカニズムは、クエリの過多に対する信頼性も提供します。

有効にすると、GWP-ASan はランダムに選択されたヒープ割り当てのサブセットをインターセプトし、特別なリージョンに配置して検出が困難なヒープメモリ破損バグを検出します。ユーザー数が十分であれば、通常のテストでは見つからないヒープメモリの安全性に関するバグを検出できます。 たとえば、GWP-ASan は Chrome ブラウザで多数のバグを検出しました(バグの多くはまだ表示が制限されています)。

GWP-ASan は、インターセプトするすべての割り当てに関する追加情報を収集します。この情報は、GWP-ASan がメモリの安全性の違反を検出し、ネイティブ コードでのクラッシュ レポートに自動的に挿入されるときに利用できます。これはデバッグに大いに役立ちます(をご覧ください)。

GWP-ASan は、大きな CPU オーバーヘッドを発生させないように設計されています。GWP-ASan を有効にすると、一定の小さな RAM オーバーヘッドが発生します。このオーバーヘッドは Android システムによって定義されるもので、現時点では、影響を受けるプロセスごとに約 70 キビバイト(KiB)です。

アプリを有効にする

GWP-ASan は、アプリのマニフェストの android:gwpAsanMode タグを使用することで、アプリごとにプロセスレベルで有効にできます。次のオプションがサポートされています。

  • 常に無効(android:gwpAsanMode="never"): この設定では、アプリ内の GWP-ASan が完全に無効になり、システム以外のアプリのデフォルトになります。

  • デフォルト(android:gwpAsanMode="default" または指定なし): Android 13(API レベル 33)以前 - GWP-ASan は無効です。Android 14(API レベル 34)以降 - Recoverable GWP-ASan が有効になります。

  • 常に有効(android:gwpAsanMode="always"): この設定では、アプリで GWP-ASan が有効になります。これには以下が含まれます。

    1. オペレーティング システムは、GWP-ASan オペレーション用に、影響を受けるプロセスごとに一定量の RAM(約 70 KB)を予約します。アプリがメモリ使用量の増加に深刻な影響を受けない場合は、GWP-ASan を有効にします。

    2. GWP-ASan は、ランダムに選択されたヒープ割り当てのサブセットをインターセプトし、特別なリージョンに配置してメモリの安全性に関する違反を確実に検出します。

    3. 特別なリージョンでメモリの安全性に関する違反が発生すると、GWP-ASan はプロセスを終了します。

    4. GWP-ASan は、クラッシュ レポートのエラーに関する追加情報を提供します。

アプリで GWP-ASan をグローバルに有効にするには、AndroidManifest.xml ファイルに以下を追加します:

<application android:gwpAsanMode="always">
  ...
</application>

また、GWP-ASan はアプリの特定のサブプロセスに対して明示的に有効または無効にできます。GWP-ASan から明示的に有効または無効にしたプロセスを使用して、アクティビティやサービスをターゲティングできます。以下の例をご覧ください。

<application>
  <processes>
    <!-- Create the (empty) application process -->
    <process />

    <!-- Create subprocesses with GWP-ASan both explicitly enabled and disabled. -->
    <process android:process=":gwp_asan_enabled"
               android:gwpAsanMode="always" />
    <process android:process=":gwp_asan_disabled"
               android:gwpAsanMode="never" />
  </processes>

  <!-- Target services and activities to be run on either the GWP-ASan enabled or disabled processes. -->
  <activity android:name="android.gwpasan.GwpAsanEnabledActivity"
            android:process=":gwp_asan_enabled" />
  <activity android:name="android.gwpasan.GwpAsanDisabledActivity"
            android:process=":gwp_asan_disabled" />
  <service android:name="android.gwpasan.GwpAsanEnabledService"
           android:process=":gwp_asan_enabled" />
  <service android:name="android.gwpasan.GwpAsanDisabledService"
           android:process=":gwp_asan_disabled" />
</application>

Recoverable GWP-ASan

Android 14(API レベル 34)以降は Recoverable GWP-ASan に対応しています。これにより、デベロッパーはユーザー エクスペリエンスを低下させることなく本番環境でヒープバッファ オーバーフローと解放後のヒープ使用のバグを検出できます。AndroidManifest.xmlandroid:gwpAsanMode が指定されていない場合、アプリは Recoverable GWP-ASan を使用します。

Recoverable GWP-ASan が基本の GWP-ASan と異なる点は、以下のとおりです。

  1. Recoverable GWP-ASan が有効となるのは、アプリの起動ごとではなく、アプリの起動の 1% 程度です。
  2. 検出された解放後のヒープ使用またはヒープバッファ オーバーフローのバグは、クラッシュ レポート(tombstone)に表示されます。このクラッシュ レポートは、元の GWP-ASan と同様に、ActivityManager#getHistoricalProcessExitReasons API で入手できます。
  3. クラッシュ レポートのダンプ後に終了する代わりに、Recoverable GWP-ASan ではメモリ破損の発生が許容され、アプリは実行し続けます。プロセスは通常どおり続行する可能性がありますが、アプリの動作は特定されなくなります。メモリ破損のため、アプリは将来的に任意の時点でクラッシュするか、ユーザーに目に見える影響を与えずに続行する可能性があります。
  4. Recoverable GWP-ASan は、クラッシュ レポートのダンプ後に無効になります。したがって、1 つのアプリが取得できる Recoverable GWP-ASan のレポートは、アプリの起動ごとに 1 つのみです。
  5. カスタム シグナル ハンドラがアプリに実装されていても、Recoverable GWP-ASan 障害を示す SIGSEGV シグナルでは呼び出されません。

Recoverable GWP-ASan のクラッシュは、エンドユーザー デバイスでメモリ破損が実際に発生していることを示すものであるため、Recoverable GWP-ASan で検出されたバグは優先して修正することを強くおすすめします。

デベロッパー サポート

ここでは、GWP-ASan を使用する際に発生する可能性のある問題とその対処方法について説明します。

割り当て / 割り当て解除トレースが見つからない

割り当て / 割り当て解除フレームがないように見えるネイティブ コードでのクラッシュを診断している場合は、アプリケーションにフレーム ポインタがない可能性があります。 GWP-ASan は、パフォーマンス上の理由からフレーム ポインタを使用して割り当てと割り当て解除のトレースを記録しており、スタック トレースが存在しない場合はアンワインドできません。

フレーム ポインタは、デフォルトで arm64 デバイスでは有効に、arm32 デバイスでは無効になっています。アプリケーションは libc を制御できないため、GWP-ASan は、通常 32 ビットの実行可能ファイルまたはアプリの割り当て / 割り当て解除トレースを収集できません。64 ビット アプリケーションでは、GWP-ASan が割り当てと割り当て解除のスタック トレースを収集できるように、-fomit-frame-pointer を使用してビルドしないようにする必要があります。

安全性に関する違反の再現

GWP-ASan は、ユーザーのデバイスでヒープメモリの安全性に関する違反を検出するように設計されています。 GWP-ASan では、クラッシュ(違反のアクセス トレース、原因の文字列、割り当て/割り当てのトレース)に関する情報をできるだけ多く提供していますが、違反の原因を推測するのは困難です。残念ながら、バグ検出は確率的であるため、GWP-ASan レポートはローカル デバイスで再現するのが難しいことがよくあります。

このような場合、バグが 64 ビットデバイスに影響する場合は、HWAddressSanitizer(HWASan)を使用してください。HWASan は、スタック、ヒープ、グローバルのメモリの安全に関する違反を確実に検出します。HWASan を使用してアプリケーションを実行すると、GWP-ASan によって報告された同じ結果を確実に再現できます。

HWASan でアプリケーションを実行してもバグの根本原因にならない場合は、問題のコードを fuzz にしてください。GWP-ASan レポートの情報に基づくファジングの成果を活用できます。これによりコードの健全性の問題を確実に検出して、その因果関係を明らかにすることができます。

このネイティブ コードの例には、ヒープ使用後のバグがあります。

#include <jni.h>
#include <string>
#include <string_view>

jstring native_get_string(JNIEnv* env) {
   std::string s = "Hellooooooooooooooo ";
   std::string_view sv = s + "World\n";

   // BUG: Use-after-free. `sv` holds a dangling reference to the ephemeral
   // string created by `s + "World\n"`. Accessing the data here is a
   // use-after-free.
   return env->NewStringUTF(sv.data());
}

extern "C" JNIEXPORT jstring JNICALL
Java_android11_test_gwpasan_MainActivity_nativeGetString(
    JNIEnv* env, jobject /* this */) {
  // Repeat the buggy code a few thousand times. GWP-ASan has a small chance
  // of detecting the use-after-free every time it happens. A single user who
  // triggers the use-after-free thousands of times will catch the bug once.
  // Alternatively, if a few thousand users each trigger the bug a single time,
  // you'll also get one report (this is the assumed model).
  jstring return_string;
  for (unsigned i = 0; i < 0x10000; ++i) {
    return_string = native_get_string(env);
  }

  return reinterpret_cast<jstring>(env->NewGlobalRef(return_string));
}

上記のサンプルコードを使用してテストを実行した場合、GWP-ASan は不正な使用を検出し、以下のクラッシュ レポートをトリガーしました。GWP-ASan は、クラッシュのタイプ、割り当てメタデータ、関連する割り当てと割り当て解除のスタック トレースに関する情報を提供することで、自動的にレポートを拡張しました。

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'google/sargo/sargo:10/RPP3.200320.009/6360804:userdebug/dev-keys'
Revision: 'PVT1.0'
ABI: 'arm64'
Timestamp: 2020-04-06 18:27:08-0700
pid: 16227, tid: 16227, name: 11.test.gwpasan  >>> android11.test.gwpasan <<<
uid: 10238
signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x736ad4afe0
Cause: [GWP-ASan]: Use After Free on a 32-byte allocation at 0x736ad4afe0

backtrace:
      #00 pc 000000000037a090  /apex/com.android.art/lib64/libart.so (art::(anonymous namespace)::ScopedCheck::CheckNonHeapValue(char, art::(anonymous namespace)::JniValueType)+448)
      #01 pc 0000000000378440  /apex/com.android.art/lib64/libart.so (art::(anonymous namespace)::ScopedCheck::CheckPossibleHeapValue(art::ScopedObjectAccess&, char, art::(anonymous namespace)::JniValueType)+204)
      #02 pc 0000000000377bec  /apex/com.android.art/lib64/libart.so (art::(anonymous namespace)::ScopedCheck::Check(art::ScopedObjectAccess&, bool, char const*, art::(anonymous namespace)::JniValueType*)+612)
      #03 pc 000000000036dcf4  /apex/com.android.art/lib64/libart.so (art::(anonymous namespace)::CheckJNI::NewStringUTF(_JNIEnv*, char const*)+708)
      #04 pc 000000000000eda4  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (_JNIEnv::NewStringUTF(char const*)+40)
      #05 pc 000000000000eab8  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (native_get_string(_JNIEnv*)+144)
      #06 pc 000000000000edf8  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (Java_android11_test_gwpasan_MainActivity_nativeGetString+44)
      ...

deallocated by thread 16227:
      #00 pc 0000000000048970  /apex/com.android.runtime/lib64/bionic/libc.so (gwp_asan::AllocationMetadata::CallSiteInfo::RecordBacktrace(unsigned long (*)(unsigned long*, unsigned long))+80)
      #01 pc 0000000000048f30  /apex/com.android.runtime/lib64/bionic/libc.so (gwp_asan::GuardedPoolAllocator::deallocate(void*)+184)
      #02 pc 000000000000f130  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (std::__ndk1::_DeallocateCaller::__do_call(void*)+20)
      ...
      #08 pc 000000000000ed6c  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (std::__ndk1::basic_string<char, std::__ndk1::char_traits<char>, std::__ndk1::allocator<char> >::~basic_string()+100)
      #09 pc 000000000000ea90  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (native_get_string(_JNIEnv*)+104)
      #10 pc 000000000000edf8  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (Java_android11_test_gwpasan_MainActivity_nativeGetString+44)
      ...

allocated by thread 16227:
      #00 pc 0000000000048970  /apex/com.android.runtime/lib64/bionic/libc.so (gwp_asan::AllocationMetadata::CallSiteInfo::RecordBacktrace(unsigned long (*)(unsigned long*, unsigned long))+80)
      #01 pc 0000000000048e4c  /apex/com.android.runtime/lib64/bionic/libc.so (gwp_asan::GuardedPoolAllocator::allocate(unsigned long)+368)
      #02 pc 000000000003b258  /apex/com.android.runtime/lib64/bionic/libc.so (gwp_asan_malloc(unsigned long)+132)
      #03 pc 000000000003bbec  /apex/com.android.runtime/lib64/bionic/libc.so (malloc+76)
      #04 pc 0000000000010414  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (operator new(unsigned long)+24)
      ...
      #10 pc 000000000000ea6c  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (native_get_string(_JNIEnv*)+68)
      #11 pc 000000000000edf8  /data/app/android11.test.gwpasan/lib/arm64/libmy-test.so (Java_android11_test_gwpasan_MainActivity_nativeGetString+44)
      ...

詳細情報

GWP-ASan の実装の詳細については、LLVM のドキュメントをご覧ください。Android ネイティブ クラッシュ レポートの詳細については、ネイティブ コードでのクラッシュの診断をご覧ください。