GWP-ASan

GWP-ASan 是一種原生記憶體配置器功能,可協助找出釋放後使用堆積緩衝區溢位錯誤。它的非正式名稱採用遞迴縮寫,也就是「GWP-ASan WillProvide Allocation SANity」。與 HWASanMalloc 偵錯 不同,GWP-ASan 不需要來源程式碼或重新編譯 (也就是使用預先建構的內容),且適用於 32 位元和 64 位元程序 (不過 32 位元錯誤的偵錯資訊較少)。 本主題大致介紹您必須在應用程式中執行哪些操作,才能啟用這項功能。另外,GWP-ASan 適用於以 Android 11 (API 級別 30) 以上版本為目標的應用程式。

總覽

啟動程序 (或產生 zygote 分支) 時,GWP-ASan 會在部分隨機選取的系統應用程式和平台執行檔中啟用。您可以在自己的應用程式中啟用 GWP-ASan,藉此找出記憶體相關錯誤,並讓您的應用程式做好支援 ARM 記憶體標記擴充功能 (MTE) 的準備。 配置取樣機制還能有效防範會導致程序停止運作的查詢

啟用之後,GWP-ASan 便會攔截隨機選取的堆積分配子集,並將這些子集放入特殊區域,以便找出難以偵測到的堆積記憶體損毀錯誤。只要使用者人數夠多,即使取樣率偏低,系統也能找出一般測試未發現的堆積記憶體安全錯誤。 例如,GWP-ASan 在 Chrome 瀏覽器中發現了大量錯誤 (其中許多錯誤的檢視權限仍處於受限狀態)。

GWP-ASan 會針對攔截的所有配置作業收集額外資訊。這些資訊會在 GWP-ASan 偵測到記憶體出現安全違規情形時提供,然後自動放入原生程式碼錯誤報告中,這對於偵錯而言很有幫助 (詳情請參閱範例)。

GWP-ASan 經精心設計,不會為 CPU 帶來大量負擔。啟用後,GWP-ASan 會產生較小的固定 RAM 負擔。這部分負擔取決於 Android 系統,目前每個受影響的作業程序約為 70 KiB。

選擇在應用程式中啟用

您可以使用應用程式資訊清單中的 android:gwpAsanMode 標記,在程序層級為個別應用程式啟用 GWP-ASan。以下是支援的選項:

  • 一律停用 (android:gwpAsanMode="never"):這項設定會完全停用應用程式中的 GWP-ASan,這也是非系統應用程式的預設設定。

  • 預設 (android:gwpAsanMode="default" 或未指定):Android 13 (API 級別 33) 以下版本 - GWP-ASan 已停用。Android 14 (API 級別 34) 以上版本 - 已啟用恢復 GWP-ASan

  • 一律啟用 (android:gwpAsanMode="always"):這項設定會啟用應用程式中的 GWP-ASan,其中包括:

    1. 作業系統會為 GWP-ASan 作業預留固定數量的 RAM,每個受影響的程序約為 70 KiB (如果您的應用程式對記憶體用量的增加不是非常敏感,請啟用 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

Android 14 (API 級別 34) 以上版本支援可復原的 GWP-ASan,可協助開發人員在實際工作環境中,找出實際工作環境中的堆積緩衝區溢位和釋放後使用的錯誤,而不會降低使用者體驗。如果在 AndroidManifest.xml 中未指定 android:gwpAsanMode,應用程式會使用可復原的 GWP-ASan。

可復原的 GWP-ASan 與基本 GWP-ASan 之間的差異如下:

  1. 只有大約 1% 的應用程式啟動 (而非每次啟動) 才會啟用可復原的 GWP-ASan。
  2. 偵測到釋放後的堆積使用或堆積緩衝區溢位錯誤時,錯誤報告 (tombstone) 中會顯示這個錯誤。您可以透過 ActivityManager#getHistoricalProcessExitReasons API 取得這份當機報告,與原始 GWP-ASan 版本相同。
  3. 可復原的 GWP-ASan 可以允許發生記憶體毀損情形,而且應用程式會持續運作,而非在傾印當機報告後退出。雖然程序可能會繼續照常,但無法再指定應用程式的行為。由於記憶體已損毀,應用程式日後可能會在某個時間點異常終止,或可能繼續而不對使用者造成任何影響。
  4. 當機報告傾印後,系統會停用可復原的 GWP-ASan。因此,每個應用程式啟動時只會收到一份可復原的 GWP-ASan 報告。
  5. 如果應用程式安裝了自訂信號處理常式,系統就一律不會呼叫 SIGSEGV 信號,表示可復原的 GWP-ASan 錯誤。

可復原的 GWP-ASan 當機事件是指使用者裝置上的實際記憶體毀損情形,因此強烈建議將可復原 GWP-ASan 識別的錯誤分類並修正為高優先順序。

開發人員支援

以下各節概略說明使用 GWP-ASan 時可能會遇到的問題,以及如何解決這些問題。

缺少配置/取消配置追蹤記錄

如果您診斷出的原生程式碼錯誤似乎缺少配置/取消配置框架,表示您的應用程式可能缺少框架指標。 出於效能考量,GWP-ASan 會使用框架指標記錄對配置和取消配置的追蹤情形,如果沒有框架指標,GWP-ASan 便無法解開堆疊追蹤。

arm64 裝置預設啟用框架指標,arm32 裝置則預設停用框架指標。由於應用程式無法控制 libc,因此 GWP-ASan 通常無法收集 32 位元執行檔或應用程式的配置/取消配置追蹤記錄。您應確保 64 位元應用程式不會使用 -fomit-frame-pointer 進行建構,如此 GWP-ASan 才能收集配置和取消配置堆疊追蹤。

重現安全違規情形

GWP-ASan 用於偵測使用者裝置上的堆積記憶體安全違規情形。 GWP-ASan 會盡可能針對錯誤 (違規情形的存取追蹤記錄、原因字串和配置/取消配置追蹤記錄) 提供最詳盡的背景資訊,但可能還是無法推斷出違規的原因。遺憾的是,由於錯誤偵測僅能提供錯誤發生的概率,因此 GWP-ASan 報告中的問題通常很難在本機裝置上重現。

在這些情況下,如果錯誤會影響到 64 位元裝置,則應使用 HWAddressSanitizer (HWASan)。HWASan 能有效地偵測堆疊、堆積和全域中的記憶體安全違規情形。使用 HWASan 執行應用程式或許可以有效地重現出 GWP-ASan 回報的結果。

如果在 HWASan 下執行應用程式後,仍無法找出造成錯誤的根本原因,請嘗試模糊處理相關程式碼。您可以根據 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 原生程式碼錯誤報告,請參閱診斷原生程式碼錯誤