GWP-ASan

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

概要

ランダムに選択された一部のシステム アプリケーションやプラットフォームの実行可能ファイルでは、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="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>

デベロッパー サポート

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

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

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

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

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 ネイティブ クラッシュ レポートの詳細については、ネイティブ クラッシュの診断をご覧ください。