GWP-ASan

GWP-ASan adalah fitur pengalokasi memori native yang membantu menemukan bug use-after-free dan heap-buffer-overflow. Nama informalnya adalah singkatan berulang dari "GWP-ASan Will Provide Allocation SANity". Tidak seperti HWASan atau Debug Malloc, GWP-ASan tidak memerlukan sumber atau kompilasi ulang (kompatibel dengan bawaan), dan berfungsi pada pemrosesan 32-bit dan 64-bit (meskipun error 32-bit memiliki informasi debug yang lebih sedikit). Topik ini menguraikan tindakan yang harus dilakukan untuk mengaktifkan fitur ini di aplikasi. GWP-ASan tersedia di aplikasi yang menargetkan Android 11 (API level 30) atau yang lebih tinggi.

Ringkasan

GWP-ASan diaktifkan di executable platform dan aplikasi tertentu yang dipilih secara acak saat proses dimulai (atau saat zygote melakukan fork). Aktifkan GWP-ASan di aplikasi Anda untuk membantu menemukan bug terkait memori, dan menyiapkan aplikasi untuk dukungan Ekstensi Pemberian Tag Memori ARM (MTE). Mekanisme pengambilan sampel alokasi juga memberikan keandalan terhadap kueri yang error.

Setelah diaktifkan, GWP-ASan mencegat subkumpulan alokasi heap yang dipilih secara acak, dan menempatkannya ke region khusus yang menangkap bug kerusakan memori heap yang sulit dideteksi. Dengan memperhitungkan banyaknya pengguna, frekuensi sampling rendah ini akan menemukan bug keamanan memori heap yang tidak ditemukan melalui pengujian reguler. Misalnya, GWP-ASan menemukan sejumlah besar bug di browser Chrome (sebagian besar di antaranya masih dalam tampilan terbatas).

GWP-ASan mengumpulkan informasi tambahan tentang semua alokasi yang dicegatnya. Informasi ini tersedia saat GWP-ASan mendeteksi pelanggaran keamanan memori dan otomatis ditempatkan ke dalam laporan masalah pada native code, yang dapat membantu proses debug secara signifikan (lihat Contoh).

GWP-ASan dirancang untuk tidak menimbulkan overhead CPU signifikan apa pun. GWP-ASan memperkenalkan overhead RAM yang kecil dan berukuran tetap ketika diaktifkan. Overhead ini ditentukan oleh sistem Android dan saat ini berukuran sekitar 70 kibibyte (KiB) untuk setiap proses yang terpengaruh.

Mengikutsertakan aplikasi Anda

GWP-ASan dapat diaktifkan oleh aplikasi pada level per proses menggunakan tag android:gwpAsanMode dalam manifes aplikasi. Opsi-opsi berikut didukung:

  • Selalu nonaktif (android:gwpAsanMode="never"): Setelan ini sepenuhnya menonaktifkan GWP-ASan di aplikasi Anda dan merupakan setelan default untuk aplikasi non-sistem.

  • Default (android:gwpAsanMode="default" atau tidak ditentukan): Android 13 (API level 33) dan yang lebih rendah - GWP-ASan dinonaktifkan. Android 14 (level API 34) dan yang lebih tinggi - Recoverable GWP-ASan diaktifkan.

  • Selalu aktif (android:gwpAsanMode="always"): Setelan ini mengaktifkan GWP-ASan di aplikasi Anda, yang mencakup hal berikut:

    1. Sistem operasi menggunakan jumlah RAM yang berukuran tetap untuk operasi GWP-ASan, yaitu sekitar 70 KiB untuk setiap proses yang terpengaruh. (Aktifkan GWP-ASan jika aplikasi Anda tidak terlalu mempermasalahkan peningkatan penggunaan memori.)

    2. GWP-ASan mengintersep subset alokasi heap yang dipilih secara acak dan menempatkannya ke region khusus yang mampu mendeteksi pelanggaran keamanan memori.

    3. Saat pelanggaran keamanan memori terjadi di region khusus, GWP-ASan akan menghentikan prosesnya.

    4. GWP-ASan memberikan informasi tambahan terkait masalah dalam laporan kerusakan.

Untuk mengaktifkan GWP-ASan secara global untuk aplikasi Anda, tambahkan baris berikut ke file AndroidManifest.xml Anda:

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

Selain itu, GWP-ASan dapat diaktifkan atau dinonaktifkan secara eksplisit untuk subproses aplikasi tertentu Anda. Anda dapat menargetkan aktivitas dan layanan menggunakan proses yang diikutsertakan atau tidak diikutsertakan secara eksplisit dalam GWP-ASan. Lihat contoh berikut ini:

<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 yang dapat dipulihkan

Android 14 (level API 34) dan yang lebih baru mendukung GWP-ASan yang Dapat Dipulihkan, yang membantu developer menemukan bug heap-buffer-overflow dan heap-use-after-free dalam produksi tanpa menurunkan pengalaman pengguna. Jika android:gwpAsanMode tidak ditentukan dalam AndroidManifest.xml, aplikasi akan menggunakan Recoverable GWP-ASan.

Pulihkan GWP-ASan berbeda dengan GWP-ASan dasar dalam hal berikut:

  1. GWP-ASan yang dapat dipulihkan hanya diaktifkan pada sekitar 1% peluncuran aplikasi, bukan setiap peluncuran aplikasi.
  2. Saat bug heap-use-after-free atau heap-buffer-overflow terdeteksi, bug ini akan muncul dalam laporan error (tombstone). Laporan error ini tersedia melalui ActivityManager#getHistoricalProcessExitReasons API, sama seperti GWP-ASan asli.
  3. Daripada keluar setelah mengeluarkan laporan error, Pulihkan GWP-ASan memungkinkan kerusakan memori terjadi, dan aplikasi terus berjalan. Meskipun proses ini dapat berlanjut seperti biasa, perilaku aplikasi tidak lagi ditentukan. Karena kerusakan memori, aplikasi mungkin error pada beberapa titik arbitrer di masa mendatang, atau dapat berlanjut tanpa dampak yang terlihat oleh pengguna.
  4. GWP-ASan yang dapat dipulihkan dinonaktifkan setelah laporan error dibuang. Oleh karena itu, aplikasi hanya bisa mendapatkan satu laporan Pulihkan GWP-ASan yang Dapat Dipulihkan per peluncuran aplikasi.
  5. Jika pengendali sinyal kustom diinstal di aplikasi, pengendali tersebut tidak pernah dipanggil untuk sinyal SIGSEGV yang menunjukkan kesalahan Recoverable GWP-ASan.

Karena error Recoverable GWP-ASan menunjukkan instance nyata kerusakan memori pada perangkat pengguna akhir, sebaiknya Anda memprioritaskan dan memperbaiki bug yang diidentifikasi oleh Recoverable GWP-ASan dengan prioritas tinggi.

Dukungan developer

Bagian ini menguraikan masalah yang mungkin terjadi saat menggunakan GWP-ASan dan cara mengatasinya.

Rekaman aktivitas alokasi/dealokasi tidak ada

Jika Anda mendiagnosis masalah pada native code yang tampaknya tidak memiliki frame alokasi/dealokasi, kemungkinan aplikasi tidak memiliki pointer frame. GWP-ASan menggunakan pointer frame untuk mencatat alokasi dan dealokasi rekaman aktivitas untuk alasan performa, dan tidak dapat melepas pelacakan tumpukan jika pointer frame tersebut tidak ada.

Pointer frame aktif secara default untuk perangkat arm64, dan dinonaktifkan secara default untuk perangkat arm32. Karena aplikasi tidak memiliki kontrol atas libc, GWP-ASan (umumnya) tidak mungkin mengumpulkan rekaman aktivitas alokasi/dealokasi untuk executable atau aplikasi 32-bit. Aplikasi 64-bit harus memastikan bahwa aplikasi tersebut tidak di-build dengan -fomit-frame-pointer agar GWP-ASan dapat mengumpulkan alokasi dan dealokasi pelacakan tumpukan.

Mereproduksi pelanggaran keamanan

GWP-ASan dirancang untuk menangkap pelanggaran keamanan memori heap pada perangkat pengguna. GWP-ASan memberikan sebanyak mungkin konteks tentang error (akses terhadap rekaman aktivitas, string penyebab, dan alokasi/dealokasi rekaman aktivitas), tetapi mungkin masih sulit untuk menyimpulkan bagaimana pelanggaran tersebut terjadi. Sayangnya, karena deteksi bug bersifat probabilistik, laporan GWP-ASan sering kali sulit direproduksi di perangkat lokal.

Dalam hal ini, jika bug memengaruhi perangkat 64 bit, Anda harus menggunakan HWAddressSanitizer (HWASan). HWASan mendeteksi pelanggaran keamanan memori dengan andal pada stack, heap, dan secara global. Menjalankan aplikasi dengan HWASan dapat secara andal mereproduksi hasil yang sama seperti yang dilaporkan oleh GWP-ASan.

Dalam kasus saat menjalankan aplikasi dengan HWASan ternyata tidak cukup untuk mengidentifikasi akar masalah bug, sebaiknya coba samarkan kode yang dimaksud. Anda dapat menargetkan upaya penyamaran berdasarkan informasi pada laporan GWP-ASan, yang dapat mendeteksi dan mengungkapkan masalah kesehatan kode yang mendasarinya dengan andal.

Contoh

Contoh kode native ini memiliki bug use-after-free heap:

#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));
}

Untuk pengujian yang menggunakan kode contoh di atas, GWP-ASan berhasil menangkap penggunaan ilegal dan memicu laporan kerusakan di bawah. GWP-ASan otomatis menyempurnakan laporan dengan memberikan informasi tentang jenis error, metadata alokasi, serta alokasi dan dealokasi pelacakan tumpukan.

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

Informasi selengkapnya

Untuk mempelajari lebih lanjut detail implementasi GWP-ASan, lihat dokumentasi LLVM. Untuk mempelajari lebih lanjut laporan masalah pada native code, lihat Mendiagnosis Masalah pada Native Code.