تذكرة GWP-ASan

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

نظرة عامة

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

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

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

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

تفعيل تطبيقك

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

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

  • الإعداد التلقائي (android:gwpAsanMode="default" أو غير محدَّد): الإصدار 13 من نظام التشغيل Android (المستوى 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 الأصلية، يُرجى الاطّلاع على تشخيص أعطال التطبيقات الأصلية.