GWP-ASan

GWP-ASan 是一种原生内存分配器功能,可帮助查找释放后使用 (use-after-free)堆缓冲区溢出 (heap-buffer-overflow) 错误。它的非正式名称是一个递归缩写,即“GWP-ASan Will Provide Allocation SANity”(GWP-ASan 提供分配健全性)。与 HWASanMalloc Debug 不同,GWP-ASan 不需要源代码或重新编译(即适用于预构建文件),并且适用于 32 位和 64 位进程(尽管 32 位崩溃的调试信息较少)。本主题概述了您需要在应用中启用此功能所采取的操作。GWP-ASan 适用于目标平台为 Android 11 (API level 30) 或更高版本的应用。

概览

GWP-ASan 在进程启动时(或 zygote 分叉时)会在一些随机选择的系统应用和平台可执行文件上启用。在您自己的应用中启用 GWP-ASan 可帮助您查找与内存相关的 bug,并为您的应用做好准备以支持ARM 内存标记扩展 (MTE)。分配抽样机制还提供针对“死亡查询” (queries of death) 的可靠性。

启用后,GWP-ASan 会拦截随机选择的一部分堆分配,并将其放入一个特殊区域,该区域可捕获难以检测的堆内存损坏 bug。在有足够用户的情况下,即使是这种低采样率也能发现通过常规测试无法发现的堆内存安全 bug。例如,GWP-ASan 已在 Chrome 浏览器中发现大量 bug(其中许多仍在受限视图下)。

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,受影响的每个进程大约为 ~70KiB。(如果您的应用对内存使用量增加不特别敏感,请启用 GWP-ASan。)

    2. GWP-ASan 会拦截随机选择的一部分堆分配,并将其放入一个特殊区域,该区域可可靠地检测内存安全违规。

    3. 当特殊区域中发生内存安全违规时,GWP-ASan 会终止进程。

    4. GWP-ASan 在崩溃报告中提供了有关故障的附加信息。

若要为您的应用全局启用 GWP-ASan,请将以下内容添加到您的 AndroidManifest.xml 文件中:

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

此外,GWP-ASan 可以针对您应用的特定子进程明确启用或禁用。您可以针对明确选择启用或选择禁用 GWP-ASan 的进程来定位 Activity 和 Service。请参阅以下示例:

<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,这有助于开发者在生产环境中发现堆缓冲区溢出和堆释放后使用 (heap-use-after-free) 错误,同时不会降低用户体验。当 AndroidManifest.xml 中未指定 android:gwpAsanMode 时,应用会使用可恢复 GWP-ASan。

可恢复 GWP-ASan 与基础 GWP-ASan 的不同之处如下:

  1. 可恢复 GWP-ASan 仅在大约 1% 的应用启动时启用,而不是每次应用启动时都启用。
  2. 当检测到堆释放后使用 (heap-use-after-free) 或堆缓冲区溢出 (heap-buffer-overflow) bug 时,此 bug 会出现在崩溃报告 (tombstone) 中。此崩溃报告可通过 ActivityManager#getHistoricalProcessExitReasons API 获取,与原始 GWP-ASan 相同。
  3. 可恢复 GWP-ASan 不会在转储崩溃报告后退出,而是允许内存损坏发生,并且应用会继续运行。虽然进程可能会照常继续,但应用的行为不再受限。由于内存损坏,应用可能会在未来的某个任意时间点崩溃,或者可能继续运行而对用户没有可见影响。
  4. 在崩溃报告转储后,可恢复 GWP-ASan 将被禁用。因此,每次应用启动只能获取一份可恢复 GWP-ASan 报告。
  5. 如果应用中安装了自定义信号处理程序,则对于指示可恢复 GWP-ASan 故障的 SIGSEGV 信号,该处理程序永远不会被调用。

由于可恢复 GWP-ASan 崩溃表明了最终用户设备上内存损坏的真实实例,因此我们强烈建议您优先处理和修复由可恢复 GWP-ASan 发现的 bug。

开发者支持

这些部分概述了使用 GWP-ASan 时可能出现的问题以及如何解决这些问题。

缺少分配/解除分配跟踪

如果您正在诊断似乎缺少分配/解除分配帧的原生崩溃,则您的应用可能缺少帧指针。GWP-ASan 出于性能原因使用帧指针来记录分配和解除分配跟踪,并且如果不存在帧指针,则无法展开堆栈跟踪。

arm64 设备默认启用帧指针,arm32 设备默认禁用帧指针。由于应用无法控制 libc,GWP-ASan(通常)无法收集 32 位可执行文件或应用的分配/解除分配跟踪。64 位应用应确保其构建时使用 -fomit-frame-pointer,以便 GWP-ASan 可以收集分配和解除分配堆栈跟踪。

重现安全违规

GWP-ASan 旨在捕获用户设备上的堆内存安全违规。GWP-ASan 提供了尽可能多的崩溃上下文信息(违规的访问跟踪、原因字符串以及分配/解除分配跟踪),但仍然可能难以推断违规是如何发生的。不幸的是,由于 bug 检测是概率性的,GWP-ASan 报告通常很难在本地设备上重现。

在这些情况下,如果 bug 影响 64 位设备,您应该使用 HWAddressSanitizer (HWASan)。HWASan 可可靠地检测堆栈、堆和全局变量上的内存安全违规。使用 HWASan 运行您的应用可能会可靠地重现 GWP-ASan 报告的相同结果。

如果使用 HWASan 运行您的应用不足以找出 bug 的根本原因,您应该尝试模糊测试相关代码。您可以根据 GWP-ASan 报告中的信息来定向您的模糊测试工作,这可以可靠地检测并揭示底层代码健康问题。

示例

此示例原生代码存在堆释放后使用 (heap use-after-free) bug

#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 原生崩溃报告的更多信息,请参阅诊断原生崩溃