GWP-ASan

GWP-ASan은 해제 후 사용힙 버퍼 오버플로우 버그를 찾는 데 도움이 되는 네이티브 메모리 할당자 기능입니다. 비공식 이름은 다음과 같이 반복되는 약어입니다. 'GWP-ASan Will Provide Allocation SANity.' HWASan이나 Malloc Debug와 달리 GWP-ASan은 소스나 재컴파일이 필요하지 않고(즉, 사전 빌드와 호환) 32비트 프로세스와 64비트 프로세스에서 모두 작동합니다(32비트 비정상 종료에는 디버깅 정보가 더 적음). 이 주제에서는 앱에서 이 기능을 사용 설정하는 데 필요한 작업을 간략하게 설명합니다. GWP-ASan은 Android 11(API 수준 30) 이상을 타겟팅하는 앱에서 사용할 수 있습니다.

개요

GWP-ASan은 프로세스 시작 시 또는 zygote가 포크할 때 무작위로 선택된 일부 시스템 애플리케이션 및 플랫폼 실행 파일에서 사용 설정됩니다. 자체 앱에서 GWP-ASan을 사용 설정하면 메모리 관련 버그를 더 쉽게 찾고 앱에서 ARM Memory Tagging Extension(MTE) 지원을 준비할 수 있습니다. 할당 샘플링 메커니즘은 종료 쿼리에 안정성도 제공합니다.

일단 사용 설정되면 GWP-ASan은 무작위로 선택된 힙 할당의 하위 집합을 가로채서 감지하기 어려운 힙 메모리 손상 버그를 포착하는 특수 영역에 배치합니다. 사용자 수가 충분할 경우 샘플링 레이트가 이렇게 낮더라도 정기적인 테스트를 통해 발견되지 않는 힙 메모리 안전 버그가 발견됩니다. 예를 들어 GWP-ASan은 Chrome 브라우저에서 상당히 많은 버그를 발견했습니다. 이 중 상당수는 여전히 제한된 뷰에 있습니다.

GWP-ASan은 가로채는 모든 할당에 관한 추가 정보를 수집합니다. 이 정보는 GWP-ASan이 메모리 안전 위반을 감지하고 디버깅에 큰 도움이 될 수 있는 네이티브 비정상 종료 보고서에 자동으로 배치될 때 사용할 수 있습니다( 참고).

GWP-ASan은 상당한 CPU 오버헤드를 발생시키지 않도록 설계되었습니다. GWP-ASan이 사용 설정되면 작고 고정된 RAM 오버헤드를 발생시킵니다. 이 오버헤드는 Android 시스템에서 결정되며 영향받는 각 프로세스에 현재 약 70KiB입니다.

앱 선택

GWP-ASan은 앱 매니페스트에서 android:gwpAsanMode 태그를 사용하여 프로세스 수준별로 앱에서 사용 설정할 수 있습니다. 다음과 같은 옵션이 지원됩니다.

  • 항상 사용 중지됨(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을 명시적으로 선택 또는 선택 해제한 프로세스를 사용하여 활동과 서비스를 타겟팅할 수 있습니다. 다음 예를 참고하세요.

<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.xmlandroid:gwpAsanMode가 지정되지 않은 경우 앱은 복구 가능한 GWP-ASan을 사용합니다.

복구 가능한 GWP-ASan은 다음과 같은 점에서 기본 GWP-ASan과 다릅니다.

  1. 복구 가능한 GWP-ASan은 모든 애플리케이션이 실행될 때마다가 아니라 앱 실행의 약 1% 에서만 사용 설정됩니다.
  2. heap-use-after-free 또는 heap-buffer-overflow 버그가 감지되면 비정상 종료 보고서 (Tombstone)에 이 버그가 표시됩니다. 이 비정상 종료 보고서는 원본 GWP-ASan과 동일한 ActivityManager#getHistoricalProcessExitReasons API를 통해 사용할 수 있습니다.
  3. 비정상 종료 보고서를 덤프한 후 종료하는 대신 복구 가능한 GWP-ASan을 사용하면 메모리 손상이 발생할 수 있으며 앱이 계속 실행됩니다. 프로세스는 평소와 같이 계속될 수 있지만 앱의 동작이 더 이상 지정되지 않습니다. 메모리 손상으로 인해 앱이 향후 임의의 시점에 비정상 종료되거나 사용자에게 표시되는 영향 없이 계속될 수 있습니다.
  4. 비정상 종료 보고서가 덤프된 후 복구 가능한 GWP-ASan이 사용 중지됩니다. 따라서 앱은 앱 실행당 복구 가능한 GWP-ASan 보고서 하나만 가져올 수 있습니다.
  5. 맞춤 신호 핸들러가 앱에 설치된 경우 복구 가능한 GWP-ASan 오류를 나타내는 SIGSEGV 신호에 관해 호출되지 않습니다.

복구 가능한 GWP-ASan 비정상 종료는 최종 사용자 기기에서 실제 메모리 손상 인스턴스를 나타내므로 복구 가능한 GWP-ASan에 의해 식별된 버그를 높은 우선순위로 선별하고 수정하는 것이 좋습니다.

개발자 지원

이 섹션에서는 GWP-ASan을 사용할 때 발생할 수 있는 문제와 이를 해결하는 방법을 설명합니다.

할당 및 할당 해제 트레이스 누락

할당 및 할당 해제 프레임이 누락된 것으로 보이는 네이티브 충돌을 진단하는 경우 애플리케이션에 프레임 포인터가 누락되었을 수 있습니다. GWP-ASan은 성능상의 이유로 프레임 포인터를 사용하여 할당 및 할당 해제 트레이스를 기록하고 이러한 트레이스가 존재하지 않으면 스택 트레이스를 해제할 수 없습니다.

프레임 포인터는 arm64 기기에 기본적으로 사용 설정되어 있고 arm32 기기에는 기본적으로 사용 중지되어 있습니다. 애플리케이션은 libc를 제어할 수 없으므로 일반적으로 GWP-ASan은 32비트 실행 파일이나 앱의 할당/할당 해제 트레이스를 수집할 수 없습니다. 64비트 애플리케이션은 GWP-ASan이 할당 및 할당 해제 스택 트레이스를 수집할 수 있도록 -fomit-frame-pointer로 빌드되지 않았다고 보장해야 합니다.

안전 위반 재현

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 네이티브 비정상 종료 보고서에 관한 자세한 내용은 네이티브 충돌 진단을 참고하세요.