GWP-ASan هي ميزة تخصّص الذاكرة الأصلية التي تساعد في العثور على أخطاء استخدام ذاكرة معيّنة بعد تفريغها (use-after-free) وتجاوز سعة المخزن المؤقت للذاكرة الديناميكية (heap-buffer-overflow) bugs. اسمها غير الرسمي هو اختصار متكرّر، وهو "GWP-ASan Will Provide Allocation SANity". على عكس HWASan أو Malloc Debug، لا تتطلّب GWP-ASan مصدرًا أو إعادة تجميع (أي أنّها تعمل مع الإصدارات المُعدّة مسبقًا)، وتعمل على كل من العمليات 32 بت و64 بت (على الرغم من أنّ أعطال 32 بت تتضمّن معلومات أقل لتصحيح الأخطاء). يحدّد هذا الموضوع الإجراءات التي عليك اتّخاذها لتفعيل هذه الميزة في تطبيقك. تتوفّر GWP-ASan في التطبيقات التي تستهدف Android 11 (مستوى واجهة برمجة التطبيقات 30) أو إصدارًا أحدث.
نظرة عامة
يتم تفعيل GWP-ASan في بعض تطبيقات النظام والملفات التنفيذية للمنصة التي تم اختيارها عشوائيًا عند بدء تشغيل العملية (أو عند تفرّع عملية zygote). يمكنك تفعيل GWP-ASan في تطبيقك لمساعدتك في العثور على الأخطاء المتعلّقة بالذاكرة وإعداد تطبيقك لدعم ميزة ARM Memory Tagging Extension (MTE). توفّر آليات أخذ عيّنات من عمليات تخصيص الذاكرة أيضًا موثوقية في ما يتعلّق بـ طلبات الإيقاف.
بعد التفعيل، تعترض GWP-ASan مجموعة فرعية تم اختيارها عشوائيًا من عمليات تخصيص الذاكرة الديناميكية، وتضعها في منطقة خاصة ترصد أخطاء تلف الذاكرة الديناميكية التي يصعب رصدها. إذا كان عدد المستخدمين كافيًا، سيؤدي حتى معدّل أخذ العيّنات المنخفض هذا إلى العثور على أخطاء أمان الذاكرة الديناميكية التي لا يتم العثور عليها من خلال الاختبار المنتظم. على سبيل المثال، عثرت GWP-ASan على عدد كبير من الأخطاء في متصفّح Chrome (لا يزال العديد منها ضمن العرض المقيّد).
تجمع GWP-ASan معلومات إضافية عن جميع عمليات تخصيص الذاكرة التي تعترضها. تتوفّر هذه المعلومات عندما ترصد GWP-ASan انتهاكًا لأمان الذاكرة وتضعها تلقائيًا في تقرير الأعطال الأصلية، ما قد يساعد بشكل كبير في تصحيح الأخطاء (راجِع المثال).
تم تصميم GWP-ASan بحيث لا يؤدي إلى أي حمل كبير على وحدة المعالجة المركزية. تؤدي GWP-ASan إلى حمل صغير وثابت على ذاكرة الوصول العشوائي (RAM) عند تفعيلها. يحدّد نظام Android هذا الحمل، ويبلغ حاليًا 70 كيلوبايت (KiB) تقريبًا لكل عملية متأثرة.
تفعيل الميزة في تطبيقك
يمكن للتطبيقات تفعيل GWP-ASan على مستوى كل عملية باستخدام العلامة android:gwpAsanMode في بيان التطبيق. الإعدادات التالية متاحة:
إيقاف الميزة دائمًا (
android:gwpAsanMode="never"): يؤدي هذا الإعداد إلى إيقاف GWP-ASan تمامًا في تطبيقك، وهو الإعداد التلقائي للتطبيقات غير تطبيقات النظام.الإعداد التلقائي (
android:gwpAsanMode="default"أو غير محدّد): Android 13 (المستوى 33 لواجهة برمجة التطبيقات ) والإصدارات الأقدم: تكون GWP-ASan غير مفعّلة. Android 14 (المستوى 34 لواجهة برمجة التطبيقات) والإصدارات الأحدث: تكون ميزة Recoverable GWP-ASan مفعّلة.تفعيل الميزة دائمًا (
android:gwpAsanMode="always"): يؤدي هذا الإعداد إلى تفعيل GWP-ASan في تطبيقك، ما يتضمّن ما يلي:يحجز نظام التشغيل مقدارًا ثابتًا من ذاكرة الوصول العشوائي لعمليات GWP-ASan، ويبلغ هذا المقدار 70 كيلوبايت تقريبًا لكل عملية متأثرة. (يمكنك تفعيل 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>
ميزة Recoverable GWP-ASan
يتوافق Android 14 (مستوى واجهة برمجة التطبيقات 34) والإصدارات الأحدث مع ميزة Recoverable GWP-ASan، ما يساعد المطوّرين في العثور على أخطاء تجاوز سعة المخزن المؤقت للذاكرة الديناميكية (heap-buffer-overflow) وأخطاء استخدام ذاكرة معيّنة بعد تفريغها (heap-use-after-free) في مرحلة الإصدار العلني بدون التأثير سلبًا في تجربة المستخدم. عندما لا يتم تحديد android:gwpAsanMode في ملف AndroidManifest.xml، يستخدم التطبيق ميزة Recoverable GWP-ASan.
تختلف ميزة Recoverable GWP-ASan عن ميزة GWP-ASan الأساسية بالطرق التالية:
- لا يتم تفعيل ميزة Recoverable GWP-ASan إلا في% 1 تقريبًا من عمليات تشغيل التطبيق، وليس في كل عملية تشغيل.
- عند رصد خطأ استخدام ذاكرة معيّنة بعد تفريغها (heap-use-after-free) أو خطأ تجاوز سعة المخزن المؤقت للذاكرة الديناميكية (heap-buffer-overflow)، يظهر هذا الخطأ في تقرير الأعطال (tombstone). يتوفّر تقرير الأعطال هذا
من خلال
ActivityManager#getHistoricalProcessExitReasonsواجهة برمجة التطبيقات، تمامًا كما في ميزة GWP-ASan الأصلية. - بدلاً من الخروج بعد تفريغ تقرير الأعطال، تسمح ميزة Recoverable GWP-ASan بحدوث تلف في الذاكرة، ويستمر تشغيل التطبيق. على الرغم من أنّ العملية قد تستمر كالمعتاد، لم يعُد سلوك التطبيق محدّدًا. بسبب تلف الذاكرة، قد يتعطّل التطبيق في مرحلة عشوائية في المستقبل، أو قد يستمر بدون أي تأثير مرئي للمستخدم.
- يتم إيقاف ميزة Recoverable GWP-ASan بعد تفريغ تقرير الأعطال. لذلك، لا يمكن أن يحصل التطبيق إلا على تقرير واحد من Recoverable GWP-ASan لكل عملية تشغيل.
- إذا تم تثبيت معالج إشارات مخصّص في التطبيق، لا يتم استدعاؤه مطلقًا لإشارة SIGSEGV تشير إلى خطأ في Recoverable GWP-ASan.
بما أنّ أعطال Recoverable GWP-ASan تشير إلى حالات حقيقية لتلف الذاكرة على أجهزة المستخدمين النهائيين، ننصح بشدة بتحديد أولويات الأخطاء التي تم رصدها من خلال Recoverable 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، ما يمكن أن يرصد بشكل موثوق مشاكل سلامة الرمز البرمجي الأساسية ويكشف عنها.
مثال
يتضمّن هذا الرمز البرمجي الأصلي مثالاً على خطأ استخدام ذاكرة معيّنة بعد تفريغها (heap-use-after-free):
#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، يُرجى الاطّلاع على مقالة تشخيص الأعطال الأصلية.