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. 可復原的 GWP-ASan 只會在約 1% 的應用程式啟動時啟用,而非每次應用程式啟動時。
  2. 當系統偵測到堆積使用後釋放或堆積緩衝區溢位錯誤時,這類錯誤會顯示在當機報告 (墓碑) 中。這份當機報告可透過 ActivityManager#getHistoricalProcessExitReasons API 取得,與原始 GWP-ASan 相同。
  3. 可復原的 GWP-ASan 不會在轉儲當機報告後離開,而是允許記憶體毀損情形發生,並讓應用程式繼續執行。雖然程序可能會照常進行,但應用程式的行為不再指定。由於記憶體損毀,應用程式可能會在日後的某個任意時間點停止運作,或是繼續運作,但不會對使用者造成任何可見影響。
  4. 當機報告傾印後,可復原的 GWP-ASan 會停用。因此,每個應用程式啟動時,只能取得單一可復原的 GWP-ASan 報告。
  5. 如果應用程式中安裝了自訂信號處理常式,則系統不會針對代表可復原 GWP-ASan 錯誤的 SIGSEGV 信號呼叫該處理常式。

由於可復原的 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 原生程式碼錯誤報告,請參閱診斷原生程式碼錯誤