GWP-ASan

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

개요

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="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을 사용할 때 발생할 수 있는 문제와 이를 해결하는 방법을 설명합니다.

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

할당 및 할당 해제 프레임이 누락된 것으로 보이는 네이티브 충돌을 진단하는 경우 애플리케이션에 프레임 포인터가 누락되었을 수 있습니다. GWP-ASan은 성능상의 이유로 프레임 포인터를 사용하여 할당 및 할당 해제 트레이스를 기록하고 이러한 트레이스가 존재하지 않으면 스택 트레이스를 해제할 수 없습니다. 프레임 포인터가 사용 설정된 코드(-fno-omit-frame-pointer)를 다시 컴파일하여 할당 및 할당 해제 스택 트레이스를 가져와야 합니다. 프레임 포인터는 arm64 기기에 기본적으로 사용 설정되어 있고 arm32 기기에는 기본적으로 사용 중지되어 있습니다.

안전 위반 사항 재현

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