GWP-ASan

GWP-ASan là tính năng trình phân bổ bộ nhớ gốc giúp tìm lỗi use-after-freeheap-buffer-overflow. Tên không chính thức của tính năng này là tên viết tắt đệ quy "GWP-ASan Will Provide Allocation SANity". Không giống như HWASan hay Malloc Debug, GWP-ASan không yêu cầu nguồn hoặc việc biên dịch lại (nghĩa là chạy được với các bản tạo sẵn), và hoạt động trên cả quy trình 32 cũng như 64 bit (mặc dù sự cố trên 32 bit có ít thông tin gỡ lỗi hơn). Chủ đề này nêu những việc bạn cần làm để bật tính năng này trong ứng dụng. GWP-ASan có sẵn trên các ứng dụng nhắm mục tiêu đến Android 11 (API cấp 30) trở lên.

Tổng quan

GWP-ASan được bật trên một số tệp thực thi được chọn ngẫu nhiên của nền tảng và ứng dụng hệ thống khi khởi động quy trình (hoặc khi zygote phát triển nhánh). Bật GWP-ASan trong ứng dụng của riêng bạn để dễ dàng tìm lỗi liên quan đến bộ nhớ và để chuẩn bị ứng dụng của bạn cho khả năng hỗ trợ Tiện ích gắn thẻ bộ nhớ (MTE) ARM. Các cơ chế lấy mẫu phân bổ cũng cung cấp độ tin cậy, giúp tránh truy vấn khiến hệ thống gặp sự cố (query of death).

Sau khi được bật, GWP-ASan sẽ chặn một tập hợp con được chọn ngẫu nhiên gồm các nội dung phân bổ bộ nhớ khối xếp, rồi đưa vào một vùng đặc biệt phát hiện các lỗi hỏng bộ nhớ khối xếp khó phát hiện. Khi có đủ người dùng, ngay cả tốc độ lấy mẫu thấp này cũng sẽ tìm được các lỗi về an toàn của bộ nhớ khối xếp không phát hiện được thông qua quy trình kiểm thử thông thường. Ví dụ: GWP-ASan đã phát hiện một số lỗi đáng kể trong trình duyệt Chrome (nhiều lỗi trong đó vẫn ở chế độ xem bị hạn chế).

GWP-ASan sẽ thu thập thêm thông tin về tất cả nội dung phân bổ mà tính năng này chặn. Thông tin này được cung cấp khi GWP-ASan phát hiện một lỗi vi phạm về an toàn bộ nhớ và tự động được đưa vào báo cáo trục trặc mã gốc, có thể hỗ trợ đáng kể khi gỡ lỗi (xem phần Ví dụ).

GWP-ASan được thiết kế để không gây hao tổn đáng kể cho CPU. GWP-ASan chỉ gây một mức hao tổn RAM nhỏ, cố định khi bật. Mức hao tổn này do hệ thống Android quyết định và hiện nằm ở mức khoảng 70 kibibyte (KiB) đối với mỗi quá trình bị ảnh hưởng.

Chọn sử dụng cho ứng dụng của bạn

Các ứng dụng có thể bật GWP-ASan ở cấp độ mỗi quy trình bằng cách sử dụng thẻ android:gwpAsanMode trong tệp kê khai ứng dụng. Các tuỳ chọn sau được hỗ trợ:

  • Luôn tắt (android:gwpAsanMode="never"): Chế độ cài đặt này tắt hẳn GWP-ASan trong ứng dụng của bạn và là chế độ mặc định cho các ứng dụng không thuộc hệ thống.

  • Mặc định (android:gwpAsanMode="default" hoặc chưa được chỉ định): Android 13 (API cấp 33) trở xuống – GWP-ASan bị tắt. Android 14 (API cấp 34) trở lên – GWP-ASan có thể khôi phục đã được bật.

  • Luôn bật (android:gwpAsanMode="always"): Chế độ cài đặt này bật GWP-ASan trong ứng dụng của bạn, bao gồm:

    1. Hệ điều hành dành một lượng RAM cố định cho các hoạt động của GWP-ASan. Mức này là khoảng ~70 KiB cho mỗi quy trình bị ảnh hưởng. (Bật GWP-ASan nếu ứng dụng không quá nhạy cảm đối với việc tăng mức sử dụng bộ nhớ.)

    2. GWP-ASan chặn một tập hợp con được chọn ngẫu nhiên gồm các nội dung phân bổ bộ nhớ khối xếp và đưa vào một vùng đặc biệt phát hiện các lỗi vi phạm về an toàn bộ nhớ một cách đáng tin cậy.

    3. Khi một lỗi vi phạm về an toàn bộ nhớ xảy ra ở vùng đặc biệt, GWP-ASan sẽ chấm dứt quy trình đó.

    4. GWP-ASan cung cấp thêm thông tin về lỗi trong báo cáo sự cố.

Để bật GWP-ASan trên toàn cục cho ứng dụng, hãy thêm nội dung sau đây vào tệp AndroidManifest.xml của bạn:

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

Ngoài ra, bạn có thể bật hoặc tắt rõ ràng GWP-ASan cho các quy trình phụ cụ thể của ứng dụng. Bạn có thể nhắm mục tiêu đến các hoạt động và dịch vụ sử dụng quy trình được chọn sử dụng hoặc không sử dụng GWP-ASan một cách rõ ràng. Hãy xem ví dụ sau đây:

<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 có thể khôi phục

Android 14 (API cấp 34) trở lên hỗ trợ GWP-ASan có thể khôi phục, giúp nhà phát triển tìm các lỗi heap-buffer-overflow và lỗi heap-use-after-free trong phiên bản chính thức mà không làm giảm chất lượng trải nghiệm người dùng. Khi android:gwpAsanMode không được chỉ định trong AndroidManifest.xml, ứng dụng sẽ dùng GWP-ASan có thể khôi phục.

GWP-ASan có thể khôi phục khác với GWP-ASan cơ sở ở những điểm sau:

  1. GWP-ASan có thể khôi phục chỉ được bật trên khoảng 1% số lần khởi chạy ứng dụng, thay vì mỗi lần khởi chạy ứng dụng.
  2. Khi phát hiện lỗi heap-use-after-free hoặc heap-buffer-overflow, lỗi này sẽ xuất hiện trong báo cáo sự cố (tombstone). Báo cáo sự cố này được cung cấp thông qua API ActivityManager#getHistoricalProcessExitReasons, giống như GWP-ASan ban đầu.
  3. Thay vì thoát sau khi kết xuất báo cáo sự cố, GWP-ASan có thể khôi phục cho phép lỗi bộ nhớ xảy ra và ứng dụng sẽ tiếp tục chạy. Mặc dù quy trình này có thể tiếp tục như bình thường, nhưng hành vi của ứng dụng sẽ không được chỉ định nữa. Do lỗi bộ nhớ, ứng dụng có thể gặp sự cố tại một thời điểm nào đó trong tương lai hoặc có thể tiếp tục mà người dùng có thể không nhận thấy bất kỳ tác động nào.
  4. GWP-ASan có thể khôi phục sẽ bị tắt sau khi báo cáo sự cố được kết xuất. Do đó, ứng dụng chỉ có thể nhận một báo cáo GWP-ASan có thể khôi phục mỗi lần khởi chạy ứng dụng.
  5. Nếu bạn cài đặt một trình xử lý tín hiệu tuỳ chỉnh trong ứng dụng, thì trình xử lý này sẽ không bao giờ được gọi cho tín hiệu SIGSEGV. Đây là dấu hiệu cho thấy lỗi GWP-ASan có thể khôi phục.

Vì sự cố GWP-ASan có thể khôi phục cho thấy các trường hợp lỗi bộ nhớ của các phiên bản thực trên thiết bị của người dùng cuối, bạn nên phân loại và khắc phục các lỗi được xác định bằng GWP-ASan có thể khôi phục với mức độ ưu tiên cao.

Hỗ trợ dành cho nhà phát triển

Các phần này nêu ra các vấn đề có thể xảy ra khi sử dụng GWP-ASan và cách giải quyết các vấn đề đó.

Thiếu dấu vết phân bổ/giải phóng

Nếu bạn đang chẩn đoán một trục trặc mã gốc có vẻ bị thiếu khung phân bổ/giải phóng, thì ứng dụng có thể thiếu con trỏ khung. GWP-ASan sử dụng con trỏ khung để ghi lại các dấu vết phân bổ và giải phóng vì lý do hiệu suất và không thể tháo gỡ dấu vết ngăn xếp nếu các dấu vết đó không hiện diện.

Theo mặc định, con trỏ khung được bật cho thiết bị arm64 và tắt cho thiết bị arm32. Do các ứng dụng không có quyền kiểm soát libc, nên (nhìn chung) GWP-ASan không thể thu thập dấu vết phân bổ/giải phóng cho các ứng dụng hoặc tệp thực thi 32 bit. Các ứng dụng 64 bit phải đảm bảo không được tạo bằng -fomit-frame-pointer để GWP-ASan có thể thu thập dấu vết ngăn xếp phân bổ và giải phóng.

Tái lập lỗi vi phạm về an toàn

GWP-ASan được thiết kế để phát hiện các lỗi vi phạm về an toàn bộ nhớ khối xếp trên thiết bị của người dùng. GWP-ASan cung cấp nhiều ngữ cảnh nhất có thể về sự cố (dấu vết truy cập của lỗi vi phạm, chuỗi nguyên nhân và dấu vết phân bổ/giải phóng), nhưng có thể vẫn khó suy ra cách lỗi vi phạm xảy ra. Thật không may, vì việc phát hiện lỗi mang tính xác suất, nên các báo cáo GWP-ASan thường rất khó tái lập trên một thiết bị cục bộ.

Trong những trường hợp này, nếu lỗi ảnh hưởng đến các thiết bị 64 bit, bạn nên sử dụng HWAddressSanitizer (HWASan). HWASan phát hiện lỗi vi phạm về an toàn bộ nhớ một cách đáng tin cậy trên ngăn xếp, bộ nhớ khối xếp và các giá trị chung. Việc chạy ứng dụng bằng HWASan có thể tái lập một cách đáng tin cậy kết quả tương tự mà GWP-ASan đang báo cáo.

Trong trường hợp việc chạy ứng dụng bằng HWASan không đủ để tìm được nguyên nhân gây ra lỗi, bạn nên cố gắng kiểm thử mờ (fuzz) mã liên quan. Bạn có thể nhắm mục tiêu cho nỗ lực kiểm thử mờ dựa trên thông tin trong báo cáo GWP-ASan. Báo cáo này có thể phát hiện và tiết lộ các vấn đề về cơ bản về tình trạng của mã một cách đáng tin cậy.

Ví dụ

Mã gốc ví dụ sau có lỗi use-after-free trên bộ nhớ khối xếp:

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

Đối với một lần chạy kiểm thử sử dụng mã mẫu ở trên, GWP-ASan đã phát hiện thành công việc sử dụng không hợp lệ và kích hoạt báo cáo sự cố ở bên dưới. GWP-ASan đã tự động cải thiện báo cáo bằng cách cung cấp thông tin về loại sự cố, siêu dữ liệu phân bổ, cũng như các dấu vết ngăn xếp phân bổ và giải phóng được đi kèm.

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

Thông tin khác

Để tìm hiểu thêm thông tin chi tiết về cách triển khai GWP-ASan, hãy xem bài viết Tài liệu LLVM. Để tìm hiểu thêm về báo cáo sự cố gốc của Android, hãy xem bài viết Chẩn đoán các trục trặc mã gốc.