تذكرة GWP-ASan

GWP-ASan هي ميزة لتخصيص الذاكرة الأصلية تساعد في العثور على أخطاء الاستخدام بعد تفريغها وتجاوز المخزن المؤقت. واسمها غير الرسمي هو اختصار متكرر، وهو "GWP-ASan Will Povide Allocation SANity". على عكس HWASan أو Malloc Debug، لا يتطلب GWP-ASan توفير مصدر أو إعادة تجميع (أي يعمل مع الإنشاءات المسبقة الإنشاء)، ويعمل على كل من العمليات بالإصدار 32 بت و64 بت (على الرغم من أن أعطال 32 بت تحتوي على معلومات تصحيح أخطاء أقل). يوضّح هذا الموضوع الإجراءات التي يجب اتخاذها لتفعيل هذه الميزة في تطبيقك. تتوفّر GWP-ASan على التطبيقات التي تستهدف الإصدار Android 11 (المستوى 30 من واجهة برمجة التطبيقات) أو الإصدارات الأحدث.

نظرة عامة

يتم تفعيل GWP-ASan في بعض تطبيقات النظام التي يتم اختيارها عشوائيًا وعمليات التنفيذ على النظام الأساسي عند بدء تشغيل العملية (أو عند استخدام شوكات zygote). يمكنك تفعيل GWP-ASan في تطبيقك الخاص لمساعدتك في رصد الأخطاء المتعلّقة بالذاكرة وتجهيز تطبيقك لإتاحة إضافة وضع علامات الذاكرة (MTE). توفّر آليات أخذ العينات أيضًا موثوقية مقابل طلبات البحث عن الوفاة.

وبعد تفعيل هذه الميزة، يعترض GWP-ASan مجموعة فرعية يتم اختيارها عشوائيًا من توزيعات كومة الذاكرة المؤقتة، ويضعها في منطقة خاصة ترصد أخطاء تلف الذاكرة التي يصعب اكتشافها. فبالنظر إلى عدد كافٍ من المستخدمين، سيجد حتى هذا المعدل المنخفض لأخذ العينات أخطاء أمان الذاكرة المؤقتة التي لا يتم العثور عليها من خلال الاختبار المنتظم. على سبيل المثال، رصدت GWP-ASan عددًا كبيرًا من الأخطاء في متصفِّح Chrome (لا يزال العديد منها خاضعًا لقيود العرض).

يجمع GWP-ASan معلومات إضافية حول جميع عمليات التوزيع التي يعترضها. تتوفر هذه المعلومات عندما يرصد GWP-ASan انتهاكًا لأمان الذاكرة ويتم وضعه تلقائيًا في تقرير الأعطال الأصلي، ما قد يساعد بشكل كبير في تصحيح الأخطاء (راجع مثال).

تم تصميم GWP-ASan بحيث لا يتحمل أي أعباء كبيرة على وحدة المعالجة المركزية (CPU). يقدم GWP-ASan مقدارًا عامًا صغيرًا وثابتًا لذاكرة الوصول العشوائي عند التفعيل. يحدد نظام Android هذه النفقات العامة ويبلغ حاليًا حوالي 70 كيبيبايت (KiB) لكل عملية متأثرة.

تفعيل تطبيقك

يمكن تفعيل GWP-ASan من خلال التطبيقات على مستوى كل عملية باستخدام علامة android:gwpAsanMode في بيان التطبيق. تتوفّر الخيارات التالية:

  • غير مفعّل دائمًا (android:gwpAsanMode="never"): يؤدي هذا الإعداد إلى إيقاف GWP-ASan في تطبيقك تمامًا وهو الإعداد التلقائي للتطبيقات غير النظامية.

  • الإعداد التلقائي (android:gwpAsanMode="default" أو غير محدّد): Android 13 (المستوى 33 لواجهة برمجة التطبيقات) والإصدارات الأقدم - تم إيقاف GWP-ASan. Android 14 (المستوى 34 من واجهة برمجة التطبيقات) والإصدارات الأحدث - تم تفعيل GWP-ASan القابل للاسترداد.

  • مفعَّل دائمًا (android:gwpAsanMode="always"): يعمل هذا الإعداد على تفعيل GWP-ASan في تطبيقك، ويتضمّن ما يلي:

    1. يحتفظ نظام التشغيل بمقدار ثابت من ذاكرة الوصول العشوائي لعمليات GWP-ASan، حوالي 70 كيلوبايت لكل عملية متأثرة. (يمكنك تفعيل 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 (المستوى 34 من واجهة برمجة التطبيقات) والإصدارات الأحدث مع أداة GWP-ASan القابلة للاسترداد، ما يساعد المطوّرين في العثور على أخطاء تجاوز المخزن المؤقت وأخطاء استخدام الذاكرة بعد تفريغها أثناء التشغيل بدون التأثير سلبًا في تجربة المستخدم. عندما لا يتم تحديد android:gwpAsanMode في AndroidManifest.xml، يستخدم التطبيق GWP-ASan القابل للاسترداد.

تختلف GWP-ASan القابل للاسترداد عن GWP-ASan الأساسي في الطرق التالية:

  1. يتم تفعيل GWP-ASan القابل للاسترداد فقط في% 1 تقريبًا من عمليات تشغيل التطبيقات، بدلاً من كل عملية تشغيل للتطبيق.
  2. عند رصد خطأ في استخدام كومة الذاكرة المؤقتة أو عطل في المخزن المؤقت، يظهر هذا الخطأ في تقرير التعطُّل (Tombstone). يتوفر تقرير الأعطال هذا من خلال واجهة برمجة تطبيقات ActivityManager#getHistoricalProcessExitReasons مثل واجهة برمجة تطبيقات GWP-ASan الأصلية.
  3. بدلاً من الخروج بعد ملء تقرير الأعطال، يسمح GWP-ASan للاسترداد بتلف الذاكرة، ويستمر تشغيل التطبيق. على الرغم من أنّ العملية قد تستمر كالمعتاد، إلا أنّه لم يتم تحديد سلوك التطبيق. بسبب تلف الذاكرة، قد يتعطّل التطبيق في مرحلة عشوائية في المستقبل أو قد يستمر في العمل بدون أي تأثير مرئي للمستخدم.
  4. يتم إيقاف أداة GWP-ASan القابلة للاسترداد بعد نقل تقرير الأعطال. لذلك، يمكن للتطبيق الحصول على تقرير GWP-ASan واحد فقط قابل للاسترداد لكل عملية تشغيل للتطبيق.
  5. في حال تثبيت معالِج إشارة مخصّص في التطبيق، لا يتم استدعاؤه أبدًا لإشارة SIGSEGV التي تشير إلى خطأ GWP-ASan قابل للاسترداد.

بما أنّ أعطال GWP-ASan القابلة للاسترداد تشير إلى حالات تلف في الذاكرة على أجهزة المستخدمين النهائيين، ننصح بشدة بفرز الأخطاء التي تم تحديدها من خلال أداة GWP-ASan القابلة للاسترداد ذات الأولوية العالية.

دعم مطوّر البرامج

توضّح هذه الأقسام المشاكل التي قد تحدث عند استخدام GWP-ASan وكيفية معالجتها.

آثار التخصيص/التخصيص غير متوفّرة

إذا كنت بصدد تشخيص عطل أصلي يبدو أنه ينقصك إطارات التخصيص/التخصيص، من المحتمل أن تطبيقك يفتقد مؤشرات الإطارات. يستخدم GWP-ASan مؤشرات الإطارات لتسجيل عمليات تتبُّع التخصيص وموقع الصفقة لأسباب تتعلق بالأداء، ولا يمكنه تخفيف تتبُّع تسلسل استدعاء الدوال البرمجية إذا لم يكن متوفرًا.

تكون مؤشرات عرض الإطارات مفعَّلة تلقائيًا لأجهزة Arm64، وتكون غير مفعَّلة تلقائيًا في أجهزة Arm32. نظرًا لأنّ التطبيقات لا تتحكم في libc، فلا يمكن (بشكل عام) لـ GWP-ASan جمع آثار التخصيص/التوزيع في عمليات التنفيذ أو التطبيقات بنظام 32 بت. يجب أن تضمن تطبيقات الإصدار 64 بت عدم إنشاؤها باستخدام -fomit-frame-pointer حتى يتمكن GWP-ASan من جمع عمليات تتبُّع تسلسل استدعاء الدوال البرمجية وتوزيعها.

إعادة إنتاج انتهاكات السلامة

تم تصميم 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، يمكنك الاطّلاع على تشخيص الأعطال الأصلية.