GWP-ASan

GWP-ASan is a native memory allocator feature that helps find use-after-free and heap-buffer-overflow bugs. Its informal name is a recursive acronym,"GWP-ASan Will Provide Allocation SANity". Unlike HWASan or Malloc Debug, GWP-ASan does not require source or recompilation (that is, works with prebuilts), and works on both 32- and 64-bit processes (although 32-bit crashes have less debugging information). This topic outlines the actions you need to take to enable this feature in your app. GWP-ASan is available on apps that target Android 11 (API level 30) or higher.

Overview

GWP-ASan is enabled on some randomly-selected system applications and platform executables upon process start-up (or when the zygote forks). Enable GWP-ASan in your own app to help you find memory-related bugs, and to prepare your app for ARM Memory Tagging Extension (MTE) support. The allocation sampling mechanisms also provide reliability against queries of death.

Once enabled, GWP-ASan intercepts a randomly-chosen subset of heap allocations, and places them into a special region that catches difficult-to-detect heap memory corruption bugs. Given enough users, even this low sampling rate will find heap memory safety bugs that aren't being found through regular testing. For example, GWP-ASan has found a significant number of bugs in the Chrome browser (many of which are still under restricted view).

GWP-ASan collects additional information about all of the allocations that it intercepts. This information is available when GWP-ASan detects a memory safety violation and is automatically placed into the native crash report, which can aid significantly in debugging (see Example).

GWP-ASan is designed to not incur any significant CPU overhead. GWP-ASan introduces a small, fixed RAM overhead when enabled. This overhead is decided by the Android system and is currently approximately 70 kibibytes (KiB) for each affected process.

Opt-in your app

GWP-ASan may be enabled by apps on a per-process level by using the android:gwpAsanMode tag in the app manifest. The following options are supported:

  • Always disabled (android:gwpAsanMode="never"): This setting completely disables GWP-ASan in your app and is the default for non-system apps.

  • Default (android:gwpAsanMode="default" or unspecified): Android 13 (API level 33) and lower - GWP-ASan is disabled. Android 14 (API level 34) and higher - Recoverable GWP-ASan is enabled.

  • Always enabled (android:gwpAsanMode="always"): This setting enables GWP-ASan in your app, which includes the following:

    1. The operating system reserves a fixed amount of RAM for GWP-ASan operations, approximately ~70KiB for each affected process. (Enable GWP-ASan if your app is not critically sensitive to increases in memory usage.)

    2. GWP-ASan intercepts a randomly-chosen subset of heap allocations and places them into a special region that reliably detects memory safety violations.

    3. When a memory safety violation occurs in the special region, GWP-ASan terminates the process.

    4. GWP-ASan provides additional information about the fault in the crash report.

To enable GWP-ASan globally for your app, add the following to your AndroidManifest.xml file:

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

Additionally, GWP-ASan can be explicitly enabled or disabled for specific subprocesses of your app. You can target activities and services using processes that are explicitly opted-in or opted-out of GWP-ASan. See the following for an example:

<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 level 34) and higher support Recoverable GWP-ASan, which helps developers find heap-buffer-overflow and heap-use-after-free bugs in production without degrading user experience. When android:gwpAsanMode is unspecified in an AndroidManifest.xml, the app uses Recoverable GWP-ASan.

Recoverable GWP-ASan differs from the base GWP-ASan in the following ways:

  1. Recoverable GWP-ASan is enabled only on approximately 1% of app launches, rather than every application launch.
  2. When a heap-use-after-free or heap-buffer-overflow bug is detected, this bug appears in the crash report (tombstone). This crash report is available through the ActivityManager#getHistoricalProcessExitReasons API, the same as the original GWP-ASan.
  3. Instead of exiting after dumping the crash report, Recoverable GWP-ASan allows memory corruption to occur, and the app continues running. While the process may continue as usual, the app's behavior is no longer specified. Due to the memory corruption, the app may crash at some arbitrary point in the future, or it may continue without any user-visible impact.
  4. Recoverable GWP-ASan is disabled after the crash report is dumped. Therefore, an app can get only a single Recoverable GWP-ASan report per app launch.
  5. If a custom signal handler is installed in the app, it's never called for a SIGSEGV signal that's indicative of a Recoverable GWP-ASan fault.

Because Recoverable GWP-ASan crashes indicate real instances of memory corruption on end-user devices, we highly recommend triaging and fixing bugs identified by Recoverable GWP-ASan with a high priority.

Developer support

These sections outline issues that might occur when using GWP-ASan and how to address them.

Allocation/deallocation traces are missing

If you are diagnosing a native crash that appears to be missing allocation/deallocation frames, your application is likely missing frame pointers. GWP-ASan uses frame pointers to record allocation and deallocation traces for performance reasons, and is unable to unwind the stack trace if they are not present.

Frame pointers are on by default for arm64 devices, and off by default for arm32 devices. Because applications don't have control over libc, it is (in general) not possible for GWP-ASan to collect allocation/deallocation traces for 32-bit executables or apps. 64-bit applications should ensure that they are not built with -fomit-frame-pointer so that GWP-ASan can collect allocation and deallocation stack traces.

Reproducing safety violations

GWP-ASan is designed to catch heap memory safety violations on user devices. GWP-ASan provides as much context as possible about the crash (access trace of the violation, cause string, and allocation/deallocation traces), but it might still be hard to deduce how the violation occurred. Unfortunately, as the bug detection is probabilistic, GWP-ASan reports are often tricky to reproduce on a local device.

In these instances, if the bug affects 64-bit devices, you should use HWAddressSanitizer (HWASan). HWASan detects memory safety violations reliably on stack, heap, and globals. Running your application with HWASan might reliably reproduce the same result that's being reported by GWP-ASan.

In cases where running your application under HWASan is insufficient to root-cause a bug, you should try to fuzz the code in question. You can target your fuzzing efforts based on information in the GWP-ASan report, which can reliably detect and reveal underlying code health problems.

Example

This example native code has a 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));
}

For a test run using the example code above, GWP-ASan successfully caught the illegal usage and triggered the crash report below. GWP-ASan has automatically enhanced the report by providing information about the type of crash, the allocation metadata, and the associated allocation and deallocation stack traces.

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
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)
      ...

More information

To learn more about the implementation details of GWP-ASan, see the LLVM documentation. To learn more about Android native crash reports, see Diagnosing Native Crashes.