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을 사용 설정합니다.운영체제는 GWP-ASan 작업을 위해 고정된 RAM 용량을 예약합니다. 영향받는 각 프로세스에 약 70KiB입니다. 앱이 메모리 사용량 증가에 심각하게 민감하지 않다면 GWP-ASan을 사용 설정하세요.
GWP-ASan은 무작위로 선택된 힙 할당의 하위 집합을 가로채 메모리 안전 위반을 안정적으로 감지하는 특수 영역에 배치합니다.
메모리 안전 위반이 특수 영역에서 발생하면 GWP-ASan은 프로세스를 종료합니다.
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을 지원합니다. 이를 통해 개발자는 사용자 환경을 저하시키지 않고 프로덕션에서 heap-buffer-overflow 및 heap-use-after-free 버그를 찾을 수 있습니다. android:gwpAsanMode
가 AndroidManifest.xml
에 지정되어 있지 않은 경우 앱은 복구 가능한 GWP-ASan을 사용합니다.
복구 가능한 GWP-ASan은 다음과 같은 점에서 기본 GWP-ASan과 다릅니다.
- 복구 가능한 GWP-ASan은 모든 애플리케이션 실행이 아닌 약 1%의 앱 실행에서만 사용 설정됩니다.
- heap-use-after-free 또는 heap-buffer-overflow 버그가 감지되면 비정상 종료 보고서(Tombstone)에 이 버그가 표시됩니다. 이 비정상 종료 보고서는 원본 GWP-ASan과 동일한
ActivityManager#getHistoricalProcessExitReasons
API를 통해 사용할 수 있습니다. - 비정상 종료 보고서를 덤프한 후 종료하는 대신 복구 가능한 GWP-ASan을 사용하면 메모리 손상이 발생할 수 있으며 앱이 계속 실행됩니다. 프로세스는 평소와 같이 계속될 수 있지만 앱의 동작이 더 이상 지정되지 않습니다. 메모리 손상으로 인해 앱이 향후 임의의 시점에 비정상 종료되거나 사용자에게 표시되는 영향 없이 계속될 수도 있습니다.
- 복구 가능한 GWP-ASan이 비정상 종료 보고서가 덤프된 후 사용 중지됩니다. 따라서 앱은 앱 실행당 하나의 복구 가능한 GWP-ASan 보고서만 가져올 수 있습니다.
- 맞춤 신호 핸들러가 앱에 설치된 경우 복구 가능한 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 네이티브 비정상 종료 보고서에 관한 자세한 내용은 네이티브 충돌 진단을 참고하세요.