أ.
تطوير المشاريع المعقدة الكبيرة أمر مستحيل بدون استخدام منهجيات وأدوات البرمجة للمساعدة في التحكم في جودة التعليمات البرمجية. بادئ ذي بدء ، إنه معيار ترميز مختص ، ومراجعات الكود ، واختبارات الوحدة ، ومحللات الكود الديناميكي والثابت. كل هذا يساعد على تحديد العيوب في الكود في المراحل الأولى من التطوير. توضح هذه المقالة إمكانات المحلل الثابت PVS-Studio للكشف عن الأخطاء ونقاط الضعف المحتملة في رمز نظام تشغيل Android. نأمل أن تجذب المقالة القراء إلى منهجية تحليل الكود الثابت وأنهم سيرغبون في تنفيذه في عملية تطوير مشاريعهم الخاصة.
مقدمة
لقد مر عام منذ كتابة
مقال كبير حول الأخطاء في نظام التشغيل Tizen ، وأردت أن أجري مرة أخرى دراسة مثيرة للاهتمام بنفس القدر لبعض أنظمة التشغيل. وقع الاختيار على Android.
رمز نظام تشغيل Android هو الجودة ، وتم اختباره جيدًا. أثناء التطوير ، يتم استخدام محلل كود ثابت ثابت على الأقل ، كما يتضح من تعليقات النموذج:
بشكل عام ، يعد هذا مشروعًا مثيرًا للاهتمام وعالي الجودة ، ويعتبر العثور على الأخطاء فيه تحديًا لمحللنا الثابت PVS-Studio.
أعتقد أنه فقط من خلال حجم المقالة ، يفهم القارئ أن محلل PVS-Studio قام بعمل ممتاز ووجد الكثير من العيوب في رمز نظام التشغيل هذا.
تعداد الضعف المشترك
ستجد في المقالة مراجع إلى
تعداد الضعف العام (CWE). دعونا نشرح سبب الإشارة إلى هذه القائمة وسبب أهميتها من وجهة نظر أمنية.
غالبًا ما لا يكون سبب الثغرات في البرامج مجموعة صعبة من الظروف ، ولكن خطأ برنامج عادي. هنا سيكون من المناسب اقتباس هذا الاقتباس من
prqa.com :
أفاد المعهد الوطني للمعايير والتكنولوجيا (NIST) أن 64 ٪ من ثغرات البرامج تنبع من أخطاء البرمجة وليس نقص ميزات الأمان.
يمكنك أن تقرأ في المقالة "
كيف يمكن لـ PVS-Studio المساعدة في العثور على الثغرات الأمنية؟ " مع بعض الأمثلة على الأخطاء البسيطة التي أدت إلى الثغرات في مشاريع مثل MySQL و iOS و NAS و illumos-gate.
وفقًا لذلك ، يمكن إزالة العديد من الثغرات الأمنية من خلال اكتشاف الأخطاء الشائعة في الوقت المناسب وإصلاحها. وهنا يدخل تعداد الضعف العام المشهد.
الأخطاء مختلفة ، وليست جميع الأخطاء خطيرة من وجهة نظر أمنية. يتم جمع الأخطاء التي يمكن أن تسبب نقاط ضعف في تعداد الضعف العام. يتم تحديث هذه القائمة ، وربما تكون هناك أخطاء يمكن أن تؤدي إلى نقاط ضعف ، ولكن لم يتم تضمينها بعد في هذه القائمة.
ومع ذلك ، إذا تم تصنيف الخطأ وفقًا لـ CWE ، فهذا يعني أنه من الممكن نظريًا أنه يمكن استغلاله على أنه ثغرة (
CVE ). نعم ، احتمال ذلك ضئيل. في حالات نادرة جدًا ، يتحول CWE إلى CVE. ومع ذلك ، إذا كنت ترغب في حماية التعليمات البرمجية الخاصة بك من نقاط الضعف ، فيجب عليك ، إذا أمكن ، العثور على أكبر عدد ممكن من الأخطاء الموضحة في CWE وإصلاحها.
من الناحية التخطيطية ، تظهر العلاقة بين PVS-Studio والأخطاء و CWE و CVE في الشكل:
تصنف بعض الأخطاء على أنها CWE. يمكن لـ PVS-Studio اكتشاف العديد من هذه الأخطاء ، وبالتالي منع بعض هذه العيوب من أن تصبح نقاط ضعف (CVE).
يمكننا أن نقول أن PVS-Studio يحدد العديد من نقاط الضعف المحتملة قبل أن تسبب الضرر. وبالتالي ، فإن PVS-Studio هي أداة اختبار أمان التطبيقات الثابتة (
SAST ).
الآن ، أعتقد أنه من الواضح لماذا ، عند وصف الأخطاء ، اعتبرت أنه من المهم ملاحظة كيفية تصنيفها وفقًا لـ CWE. وبهذه الطريقة يكون من الأسهل إظهار أهمية استخدام محلل الكود الثابت في المشاريع المهمة ، التي ترتبط بها أنظمة التشغيل بشكل واضح.
الاختيار الروبوت
لتحليل المشروع ، استخدمت إصدار محلل PVS-Studio الإصدار 6.24. يدعم المحلل حاليًا اللغات والمترجمين التاليين:
- نوافذ Visual Studio 2010-2017 C و C ++ و C ++ / CLI و C ++ / CX (WinRT) و C #
- نوافذ منضدة IAR المدمجة ، مترجم C / C ++ لـ ARM C ، C ++
- ويندوز / لينكس Keil µVision، DS-MDK، ARM Compiler 5/6 C، C ++
- ويندوز / لينكس ستوديو Texas Instruments Code Composer ، أدوات إنشاء رمز ARM C ، C ++
- Windows / Linux / macOS. Clang C، C ++
- Linux / macOS. GCC C و C ++
- نوافذ MinGW C، C ++
ملاحظة ربما غاب بعض قرائنا عن الأخبار التي
دعمناها للعمل في بيئة macOS وسيهتمون بهذا المنشور: "إصدار
PVS-Studio لنظام macOS: 64 نقطة ضعف في Apple XNU Kernel ".
لم تكن عملية التحقق من شفرة مصدر Android مشكلة ، لذلك لن أتطرق إليها. كانت المشكلة ، بالأحرى ، انشغالي بمهام أخرى ، ولهذا السبب لم أجد الوقت والطاقة للنظر في التقرير بعناية كما أريد. ومع ذلك ، حتى نظرة سريعة كانت أكثر من كافية لجمع مجموعة كبيرة من الأخطاء المثيرة للاهتمام لمقال صلب.
الأهم من ذلك: أطلب من مطوري Android ليس فقط تصحيح الأخطاء الموضحة في المقالة ، ولكن أيضًا إجراء تحليل مستقل أكثر دقة. نظرت إلى تقرير المحلل بشكل سطحي وكان من الممكن أن يخطئ الكثير من الأخطاء الجسيمة.
في الاختبار الأول ، سينتج المحلل الكثير من الإيجابيات الكاذبة ، لكن
هذه ليست مشكلة . فريقنا جاهز للمساعدة في التوصيات حول تكوين المحلل لتقليل عدد الإيجابيات الخاطئة. نحن مستعدون أيضًا لتوفير مفتاح ترخيص لمدة شهر أو أكثر ، إذا لزم الأمر. بشكل عام ،
اكتب إلينا ، سنساعدك ونخبرك.
الآن دعونا نرى الأخطاء ونقاط الضعف المحتملة التي تمكنت من العثور عليها. آمل أن تستمتع بما يمكن لمحلل الشفرة الثابتة PVS-Studio العثور عليه. قراءة جيدة.
مقارنات لا معنى لها
يعتبر المحلل أن التعبيرات غير طبيعية إذا كانت دائمًا صحيحة أو خاطئة. يتم تصنيف هذه التحذيرات ، وفقًا لتعداد الضعف العام ، على النحو التالي:
يولد المحلل العديد من هذه التحذيرات ، ولسوء الحظ ، فإن معظمها خاطئ لرمز Android. في هذه الحالة ، لا يلوم المحلل. فقط الرمز مكتوب هكذا.
سأوضح ذلك بمثال بسيط.
#if GENERIC_TARGET const char alternative_config_path[] = "/data/nfc/"; #else const char alternative_config_path[] = ""; #endif CNxpNfcConfig& CNxpNfcConfig::GetInstance() { .... if (alternative_config_path[0] != '\0') { .... }
يقوم المحلل هنا بإنشاء تحذير: التعبير V547 CWE-570 'Altern_config_path [0]! =' \ 0 '' خطأ دائمًا. 401
والحقيقة هي أن الماكرو
GENERIC_TARGET غير محدد ، ومن وجهة نظر المحلل ، يبدو الرمز كما يلي:
const char alternative_config_path[] = ""; .... if (alternative_config_path[0] != '\0') {
يلتزم المحلل ببساطة بإصدار تحذير ، حيث أن السطر فارغ ، ويقع دائمًا الطرف صفر عند الإزاحة الصفرية. وبالتالي ، المحلل على حق رسميا في إصدار تحذير. ومع ذلك ، من وجهة نظر عملية ، لا فائدة من هذا التحذير.
لسوء الحظ ، لا يمكن فعل شيء مع مثل هذه الحالات. سيتعين علينا مراجعة جميع هذه التحذيرات بشكل منهجي ووضع علامة على العديد من الأماكن على أنها إيجابية كاذبة حتى لا يقوم المحلل بإصدار الرسائل في هذه السطور. يجب فعل ذلك حقًا ، لأنه بالإضافة إلى الرسائل التي لا معنى لها ، سيتم العثور على الكثير من العيوب الحقيقية.
أعترف بصراحة أنني لم أكن مهتمًا بالنظر بعناية في التحذيرات من هذا النوع ، وذهبت إليها بشكل سطحي. ومع ذلك ، سيكون هذا كافيًا لإظهار أن مثل هذه التشخيصات مفيدة للغاية وتجد أخطاء مثيرة للاهتمام.
أريد أن أبدأ بالوضع الكلاسيكي عندما يتم تنفيذ وظيفة مقارنة كائنين بشكل غير صحيح. لماذا الكلاسيكية؟ هذا هو نمط خطأ نموذجي نواجهه باستمرار في مجموعة متنوعة من المشاريع. على الأرجح ، هناك ثلاثة أسباب لحدوثه:
- وظائف المقارنة بسيطة ويتم كتابتها "تلقائيًا" وتستخدم تقنية النسخ واللصق. عند كتابة مثل هذا الرمز ، يكون الشخص غافلًا وغالبًا ما يقوم بعمل أخطاء إملائية.
- عادة ، لا يتم إجراء مراجعة التعليمات البرمجية لمثل هذه الوظائف ، حيث أنها كسولة للغاية للنظر في الوظائف البسيطة والمملة.
- عادة لا يتم إجراء اختبارات الوحدة لمثل هذه الوظائف. لأن الكسل. بالإضافة إلى ذلك ، فإن الوظائف بسيطة ، ولا يعتقد المبرمجون أن الأخطاء ممكنة فيها.
يتم وصف هذه الأفكار والأمثلة بمزيد من التفصيل في مقال "
الشر يعيش في وظائف المقارنة ".
static inline bool isAudioPlaybackRateEqual( const AudioPlaybackRate &pr1, const AudioPlaybackRate &pr2) { return fabs(pr1.mSpeed - pr2.mSpeed) < AUDIO_TIMESTRETCH_SPEED_MIN_DELTA && fabs(pr1.mPitch - pr2.mPitch) < AUDIO_TIMESTRETCH_PITCH_MIN_DELTA && pr2.mStretchMode == pr2.mStretchMode && pr2.mFallbackMode == pr2.mFallbackMode; }
لذا ، أمامنا الوظيفة الكلاسيكية لمقارنة كائنين من نوع
AudioPlaybackRate . وكما أعتقد ، يخمن القارئ أن هذا خطأ. يلاحظ محلل PVS-Studio نوعين من الأخطاء المطبعية على الفور:
- V501 CWE-571 توجد تعبيرات فرعية متطابقة إلى يسار ويمين عامل التشغيل '==': pr2.mStretchMode == pr2.mStretchMode AudioResamplerPublic.h 107
- V501 CWE-571 توجد تعبيرات فرعية متطابقة إلى يسار ويمين عامل التشغيل '==': pr2.mFallbackMode == pr2.mFallbackMode AudioResamplerPublic.h 108
تتم مقارنة الحقل
pr2.mStretchMode والحقل
pr2.mFallbackMode مع أنفسهم. اتضح أن الوظيفة لا تقارن الأشياء بدقة كافية.
تعيش المقارنة التالية عديمة الفائدة في وظيفة تخزن معلومات بصمات الأصابع في ملف.
static void saveFingerprint(worker_thread_t* listener, int idx) { .... int ns = fwrite(&listener->secureid[idx], sizeof(uint64_t), 1, fp); .... int nf = fwrite(&listener->fingerid[idx], sizeof(uint64_t), 1, fp); if (ns != 1 || ns !=1)
تم اكتشاف شذوذ في هذا الرمز من خلال تشخيصين في وقت واحد:
- V501 CWE-570 توجد تعبيرات فرعية متطابقة إلى اليسار وإلى اليمين من "||" عامل التشغيل: ns! = 1 || نانوثانية! = بصمة واحدة ج 126
- V560 CWE-570 جزء من التعبير الشرطي دائمًا ما يكون خطأ: ns! = 1. fingerprint.c 126
لا يوجد معالجة للحالة عندما لا يمكن للمكالمة الثانية لوظيفة
fwrite كتابة البيانات في ملف. وبعبارة أخرى ، لا
يتم التحقق من قيمة المتغير
nf . يجب أن يبدو الفحص الصحيح كما يلي:
if (ns != 1 || nf != 1)
ننتقل إلى الخطأ التالي المرتبط باستخدام
& عامل التشغيل.
#define O_RDONLY 00000000 #define O_WRONLY 00000001 #define O_RDWR 00000002 static ssize_t verity_read(fec_handle *f, ....) { .... if (f->mode & O_RDONLY && expect_zeros) { memset(data, 0, FEC_BLOCKSIZE); goto valid; } .... }
تحذير PVS-Studio: V560 CWE-570 جزء من التعبير الشرطي دائمًا ما يكون خطأ: f-> mode & 00000000. fec_read.cpp 322
لاحظ أن ثابت
O_RDONLY هو صفر. هذا يجعل التعبير
f-> mode & O_RDONLY لا طائل من ورائه ، كما هو دائمًا 0. وتبين أن حالة
if لم
يتم الوفاء بها مطلقًا ، وأن العبارة true هي كود
خام .
يجب أن يكون الفحص الصحيح كما يلي:
if (f->mode == O_RDONLY && expect_zeros) {
الآن دعونا نلقي نظرة على خطأ إملائي كلاسيكي عندما نسينا كتابة جزء من الشرط.
enum { .... CHANGE_DISPLAY_INFO = 1 << 2, .... }; void RotaryEncoderInputMapper::configure(nsecs_t when, const InputReaderConfiguration* config, uint32_t changes) { .... if (!changes || (InputReaderConfiguration::CHANGE_DISPLAY_INFO)) { .... }
تحذير PVS-Studio: V768 CWE-571 يتم استخدام ثابت التعداد 'CHANGE_DISPLAY_INFO' كمتغير من نوع منطقي. InputReader.cpp 3016
الشرط دائمًا صحيحًا ، لأن المعامل
InputReaderConfiguration :: CHANGE_DISPLAY_INFO هو ثابت يساوي 4.
إذا نظرت إلى كيفية كتابة الرمز في الحي ، يصبح من الواضح أنه في الواقع يجب أن تكون الحالة على هذا النحو:
if (!changes || (changes & InputReaderConfiguration::CHANGE_DISPLAY_INFO)) {
المقارنة التالية ، التي لا معنى لها ، التقيت في مشغل الحلقة.
void parse_printerAttributes(....) { .... ipp_t *collection = ippGetCollection(attrptr, i); for (j = 0, attrptr = ippFirstAttribute(collection); (j < 4) && (attrptr != NULL); attrptr = ippNextAttribute(collection)) { if (strcmp("....", ippGetName(attrptr)) == 0) { ....TopMargin = ippGetInteger(attrptr, 0); } else if (strcmp("....", ippGetName(attrptr)) == 0) { ....BottomMargin = ippGetInteger(attrptr, 0); } else if (strcmp("....", ippGetName(attrptr)) == 0) { ....LeftMargin = ippGetInteger(attrptr, 0); } else if (strcmp("....", ippGetName(attrptr)) == 0) { ....RightMargin = ippGetInteger(attrptr, 0); } } .... }
تحذير PVS-Studio: V560 CWE-571 دائمًا ما يكون جزء من التعبير الشرطي صحيحًا: (j <4). 926
لاحظ أن قيمة المتغير
j لا تتزايد في أي مكان. هذا يعني أن التعبير الفرعي
(j <4) صحيح دائمًا.
يشير أكبر عدد من العمليات المفيدة لمحلل PVS-Studio ، فيما يتعلق دائمًا بالظروف الحقيقية / الخاطئة ، إلى الكود حيث يتم التحقق من نتيجة إنشاء الكائنات باستخدام عامل التشغيل
الجديد . بعبارة أخرى ، يكتشف المحلل نمط الكود التالي:
T *p = new T; if (p == nullptr) return ERROR;
مثل هذه الشيكات لا طائل من ورائها. إذا لم يكن من الممكن تخصيص ذاكرة للكائن ،
فسيتم طرح استثناء من نوع
std :: bad_alloc ، ولن يتمكن من التحقق من قيمة المؤشر.
ملاحظة يمكن للمشغل
الجديد إرجاع
nullptr بكتابة
جديد (std :: nothrow) T. ومع ذلك ، هذا لا ينطبق على الأخطاء التي تمت مناقشتها. يأخذ محلل PVS-Studio في الاعتبار
(std :: nothrow) ولا يعطي تحذيرًا إذا تم إنشاء الكائن بهذه الطريقة.
قد يبدو أن مثل هذه الأخطاء غير ضارة. حسنًا ، فكر في الأمر ، فحصًا إضافيًا لا يعمل أبدًا. على أي حال ، سيتم طرح استثناء ستتم معالجته في مكان ما. لسوء الحظ ، يضع بعض المطورين في العبارة -الصحيحة لإجراءات
if if التي تحرر الموارد ، إلخ. نظرًا لأن هذا الرمز لا يتم تنفيذه ، فقد يؤدي إلى تسرب للذاكرة وأخطاء أخرى.
ضع في اعتبارك إحدى هذه الحالات التي لاحظتها في رمز Android.
int parse_apk(const char *path, const char *target_package_name) { .... FileMap *dataMap = zip->createEntryFileMap(entry); if (dataMap == NULL) { ALOGW("%s: failed to create FileMap\n", __FUNCTION__); return -1; } char *buf = new char[uncompLen]; if (NULL == buf) { ALOGW("%s: failed to allocate %" PRIu32 " byte\n", __FUNCTION__, uncompLen); delete dataMap; return -1; } .... }
تحذير PVS-Studio: V668 CWE-570 لا يوجد أي معنى في اختبار مؤشر "buf" مقابل قيمة خالية ، حيث تم تخصيص الذاكرة باستخدام عامل التشغيل "الجديد". سيتم إنشاء الاستثناء في حالة خطأ تخصيص الذاكرة. Scan.cpp 213
يرجى ملاحظة أنه إذا لم يكن من الممكن تخصيص كتلة ذاكرة ثانية ، فإن المبرمج يحاول تحرير الكتلة الأولى:
delete dataMap;
هذا الكود لن يتحكم ابدا هذا هو رمز ميت. في حالة حدوث استثناء ، سيحدث تسرب للذاكرة.
كتابة مثل هذا الرمز خطأ جوهري.
تم اختراع
المؤشرات الذكية لمثل هذه الحالات.
في المجموع ، اكتشف محلل PVS-Studio
176 مكانًا في رمز Android حيث يتم التحقق من المؤشر بعد إنشاء الكائنات باستخدام
جديد . لم أفهم مدى خطورة كل من هذه الأماكن ، وبالطبع ، لن أزعج المقالة بكل هذه التحذيرات. يمكن
للمهتمين رؤية تحذيرات أخرى في ملف
Android_V668.txt .
إلغاء الإشارة إلى مؤشر فارغ
يؤدي إلغاء الإشارة إلى مؤشر فارغ إلى سلوك برنامج غير محدد ، لذا من المفيد البحث عن مثل هذه الأماكن وإصلاحها. اعتمادًا على الموقف ، يمكن لمحلل PVS-Studio تصنيف هذه الأخطاء وفقًا لتعداد الضعف العام على النحو التالي:
- CWE-119 : تقييد غير مناسب للعمليات داخل حدود مخزن الذاكرة المؤقت
- CWE-476 : مرجع مؤشر NULL
- CWE-628 : استدعاء دالة مع وسيطات محددة بشكل غير صحيح
- CWE-690 : قيمة الإرجاع غير المحددة إلى NULL Pointer Dereference
غالبًا ما أجد مثل هذه الأخطاء في الرمز المسؤول عن معالجة المواقف غير القياسية أو غير الصحيحة. لا أحد يختبر مثل هذا الرمز ، ويمكن أن يعيش الخطأ فيه لفترة طويلة جدًا. فقط سيتم النظر في مثل هذه الحالة الآن.
bool parseEffect(....) { .... if (xmlProxyLib == nullptr) { ALOGE("effectProxy must contain a <%s>: %s", tag, dump(*xmlProxyLib)); return false; } .... }
تحذير PVS-Studio: قد يحدث V522 CWE-476 إلغاء الإشارة للمؤشر الصفري 'xmlProxyLib'. التأثيرات Config.cpp 205
إذا كان مؤشر
xmlProxyLib فارغًا ، فعندئذٍ يعرض المبرمج رسالة تصحيح ، حيث من الضروري إلغاء الإشارة إلى هذا المؤشر. عفوًا ...
الآن فكر في إصدار أكثر إثارة للاهتمام من الخطأ.
static void soinfo_unload_impl(soinfo* root) { .... soinfo* needed = find_library(si->get_primary_namespace(), library_name, RTLD_NOLOAD, nullptr, nullptr); if (needed != nullptr) {
تحذير PVS-Studio: V522 CWE-476 قد يتم إلغاء الإشارة إلى المؤشر الفارغ "المطلوب". زينر محمد عبدالله 1847
إذا
احتاج المؤشر
! = Nullptr ، يتم إصدار تحذير ، وهو سلوك مريب للغاية للبرنامج. يصبح من الواضح أخيرًا أن الكود يحتوي على خطأ إذا نظرت أدناه ورأيت أنه عند
الحاجة == nullptr ،
سيتم إلغاء الإشارة إلى المؤشر الفارغ في التعبير
المطلوب-> is_linked () .
على الأرجح ، يتم الخلط بين العوامل! = و == ببساطة هنا. إذا قمت بالاستبدال ، يكون رمز الوظيفة منطقيًا ويختفي الخطأ.
يشير الجزء الأكبر من التحذيرات بشأن إعادة الإشارة المحتملة لمؤشر فارغ إلى حالة النموذج:
T *p = (T *) malloc (N); *p = x;
يمكن لوظائف مثل
malloc و
strdup وما إلى ذلك إرجاع القيمة
الفارغة (NULL) إذا تعذر تخصيص الذاكرة. لذلك ، لا يمكنك تغيير مؤشرات الإشارة التي ترجع هذه الوظائف دون التحقق من المؤشر أولاً.
هناك العديد من الأخطاء المتشابهة ، لذلك سأعطي جزأين بسيطين من التعليمات البرمجية: الأول مع
مالوك والثاني مع
strdup .
DownmixerBufferProvider::DownmixerBufferProvider(....) { .... effect_param_t * const param = (effect_param_t *) malloc(downmixParamSize); param->psize = sizeof(downmix_params_t); .... }
تحذير PVS-Studio: V522 CWE-690 قد يكون هناك إحالة مرجعية لمؤشر فارغ محتمل "معلمة". خطوط التحقق: 245 ، 244. BufferProviders.cpp 245
static char* descriptorClassToDot(const char* str) { .... newStr = strdup(lastSlash); newStr[strlen(lastSlash)-1] = '\0'; .... }
تحذير PVS-Studio: V522 CWE-690 قد يكون هناك إحالة مرجعية لمؤشر خالٍ محتمل "newStr". خطوط التحقق: 203 ، 202. DexDump.cpp 203
قد يقول شخص ما أن هذه أخطاء طفيفة. إذا لم يكن هناك ذاكرة كافية ، فسوف يتعطل البرنامج ببساطة عند إلغاء الإشارة إلى المؤشر الفارغ ، وهذا أمر طبيعي. نظرًا لعدم وجود ذاكرة ، فلا يوجد شيء لمحاولة التعامل مع هذا الموقف بطريقة أو بأخرى.
مثل هذا الشخص خاطئ. يجب التحقق من المؤشرات! لقد درست هذا الموضوع بالتفصيل في مقالة "
لماذا من المهم التحقق مما عادت إليه الدالة malloc ". أوصي بشدة بقراءتها لكل من لم يقرأها.
باختصار ، الخطر هو أن الكتابة إلى الذاكرة قد لا تحدث بالضرورة بالقرب من العنوان صفر. من الممكن كتابة البيانات في مكان بعيد جدًا في صفحة الذاكرة غير المحمية ضد الكتابة ، وبالتالي تتسبب في حدوث خطأ بعيد المنال ، أو بشكل عام يمكن استخدام هذا الخطأ كنقطة ضعف. دعنا نرى ما أعنيه
بمثال دالة
check_size .
int check_size(radio_metadata_buffer_t **metadata_ptr, const uint32_t size_int) { .... metadata = realloc(metadata, new_size_int * sizeof(uint32_t)); memmove( (uint32_t *)metadata + new_size_int - (metadata->count + 1), (uint32_t *)metadata + metadata->size_int - (metadata->count + 1), (metadata->count + 1) * sizeof(uint32_t)); .... }
تحذير PVS-Studio: V769 CWE-119 يمكن أن يكون مؤشر '(uint32_t *) بيانات التعريف' في تعبير '(uint32_t *) metadata + new_size_int' فارغًا. في مثل هذه الحالة ، ستكون القيمة الناتجة بلا معنى ولا يجب استخدامها. خطوط التحقق: 91 ، 89. radio_metadata.c 91
لم أفهم منطق الوظيفة ، لكن هذا ليس ضروريًا. الشيء الرئيسي هو أنه يتم إنشاء مخزن مؤقت جديد ويتم نسخ البيانات هناك. إذا قامت دالة
realloc بإرجاع
NULL ، فسيتم نسخ البيانات إلى العنوان ((uint32_t *) NULL + metadata-> size_int - (metadata-> count + 1)).
إذا كانت
قيمة metadata-> size_int كبيرة ، فستكون النتائج مؤسفة. اتضح أن البيانات مكتوبة على جزء عشوائي من الذاكرة.
بالمناسبة ، هناك نوع آخر من إلغاء الإشارة إلى مؤشر صفري ، والذي يصنفه محلل PVS-Studio ليس CWE-690 ، ولكن كـ CWE-628 (وسيطة غير صالحة).
static void parse_tcp_ports(const char *portstring, uint16_t *ports) { char *buffer; char *cp; buffer = strdup(portstring); if ((cp = strchr(buffer, ':')) == NULL) .... }
تحذير PVS-Studio: V575 CWE-628 يتم تمرير المؤشر الفارغ المحتمل إلى وظيفة "strchr". افحص الحجة الأولى. خطوط التحقق: 47 ، 46. libxt_tcp.c 47
والحقيقة هي أن dereferencing المؤشر سيحدث عندما يتم
استدعاء الدالة
strchr . لذلك ، يفسر المحلل هذا الموقف على أنه تمرير قيمة غير صحيحة للدالة.
تسرد التحذيرات الـ
194 المتبقية من هذا النوع I في ملف
Android_V522_V575.txt .
بالمناسبة ، يتم إعطاء لمحة خاصة لجميع هذه الأخطاء من خلال التحذيرات التي تمت مناقشتها سابقًا حول التحقق من المؤشر بعد استدعاء العامل
الجديد . اتضح أن هناك 195 مكالمة إلى
وظائف malloc /
realloc /
strdup ، وما إلى ذلك ، عندما لا يتم تحديد المؤشر. ولكن هناك 176 مكانًا يتم فيها فحص المؤشر بعد استدعاء
جديد . موافق ، نهج غريب!
في النهاية ، يبقى لنا النظر في التحذيرات V595 و V1004 ، والتي ترتبط أيضًا باستخدام المؤشرات الخالية.
يكتشف V595 الحالات التي يتم فيها إلغاء الإشارة إلى المؤشر ثم التحقق منه. مثال اصطناعي:
p->foo(); if (!p) Error();
يكشف V1004 عن الوضع العكسي عندما تم فحص المؤشر أولاً ، ثم نسي القيام بذلك. مثال اصطناعي:
if (p) p->foo(); p->doo();
لنلقِ نظرة على بعض أجزاء رمز Android ، حيث حدثت أخطاء من هذا النوع. شرح خاص ميزاتها ليست مطلوبة. PV_STATUS RC_UpdateBuffer(VideoEncData *video, Int currLayer, Int num_skip) { rateControl *rc = video->rc[currLayer]; MultiPass *pMP = video->pMP[currLayer]; if (video == NULL || rc == NULL || pMP == NULL) return PV_FAIL; .... }
تحذير PVS-Studio: V595 CWE-476 تم استخدام مؤشر "الفيديو" قبل التحقق من صلاحيته باستخدام nullptr. خطوط التحقق: 385 ، 388. rate_control.cpp 385 static void resampler_reset(struct resampler_itfe *resampler) { struct resampler *rsmp = (struct resampler *)resampler; rsmp->frames_in = 0; rsmp->frames_rq = 0; if (rsmp != NULL && rsmp->speex_resampler != NULL) { speex_resampler_reset_mem(rsmp->speex_resampler); } }
تحذير PVS-Studio: V595 CWE-476 تم استخدام مؤشر "rsmp" قبل التحقق من صحته باستخدام nullptr. تحقق من الخطوط: 54 ، 57. resampler.c 54 void bta_gattc_disc_cmpl(tBTA_GATTC_CLCB* p_clcb, UNUSED_ATTR tBTA_GATTC_DATA* p_data) { .... if (p_clcb->status != GATT_SUCCESS) { if (p_clcb->p_srcb) { std::vector<tBTA_GATTC_SERVICE>().swap( p_clcb->p_srcb->srvc_cache); } bta_gattc_cache_reset(p_clcb->p_srcb->server_bda); } .... }
تحذير PVS-Studio: V1004 CWE-476 تم استخدام مؤشر "p_clcb-> p_srcb" بشكل غير آمن بعد التحقق منه في مقابل nullptr. التحقق من الخطوط: 695 ، 701. bta_gattc_act.cc 701ليس من المثير للاهتمام مراعاة التحذيرات الأخرى من هذا النوع. من بينها هناك أخطاء وتحذيرات كاذبة تنشأ بسبب كود مكتوب بشكل سيء أو صعب.كتبت عشرات التحذيرات المفيدة:- V1004 CWE-476 تم استخدام مؤشر 'ain' بشكل غير آمن بعد التحقق من صلاحيته باستخدام nullptr. تحقق من الخطوط: 101 ، 105. rsCpuIntrinsicBLAS.cpp 105
- V595 CWE-476 تم استخدام مؤشر "outError" قبل التحقق من صحته باستخدام nullptr. خطوط التحقق: 437 ، 450. Command.cpp 437
- V595 CWE-476 The 'out_last_reference' pointer was utilized before it was verified against nullptr. Check lines: 432, 436. AssetManager2.cpp 432
- V595 CWE-476 The 'set' pointer was utilized before it was verified against nullptr. Check lines: 4524, 4529. ResourceTypes.cpp 4524
- V595 CWE-476 The 'reply' pointer was utilized before it was verified against nullptr. Check lines: 126, 133. Binder.cpp 126
- V595 CWE-476 The 'video' pointer was utilized before it was verified against nullptr. Check lines: 532, 540. rate_control.cpp 532
- V595 CWE-476 The 'video' pointer was utilized before it was verified against nullptr. Check lines: 702, 711. rate_control.cpp 702
- V595 CWE-476 The 'pInfo' pointer was utilized before it was verified against nullptr. Check lines: 251, 254. ResolveInfo.cpp 251
- V595 CWE-476 تم استخدام مؤشر "العنوان" قبل التحقق منه مقابل nullptr. خطوط التحقق: 53 ، 55. DeviceHalHidl.cpp 53
- V595 CWE-476 تم استخدام مؤشر "halAddress" قبل أن يتم التحقق منه مقابل nullptr. خطوط التحقق: 55 ، 82. DeviceHalHidl.cpp 55
ثم شعرت بالملل ، وقمت بتصفية التحذيرات من هذا النوع. لذلك ، لا أعرف حتى عدد الأخطاء الحقيقية التي اكتشفها المحلل. هذه التحذيرات في انتظار بطلهم ، الذي سيدرسها بعناية ويجر تغييرات على الكود.أريد فقط أن ألفت انتباه قرائي الجدد إلى أخطاء من هذا النوع: NJ_EXTERN NJ_INT16 njx_search_word(NJ_CLASS *iwnn, ....) { .... NJ_PREVIOUS_SELECTION_INFO *prev_info = &(iwnn->previous_selection); if (iwnn == NULL) { return NJ_SET_ERR_VAL(NJ_FUNC_NJ_SEARCH_WORD, NJ_ERR_PARAM_ENV_NULL); } .... }
تحذير PVS-Studio: V595 CWE-476 تم استخدام المؤشر "iwnn" قبل التحقق من صلاحيته باستخدام nullptr. تحقق من الخطوط: 686 ، 689. ndapi.c 686يعتقد بعض الأشخاص أنه لا يوجد خطأ هنا ، لأنه "لا يوجد أي إشارة فعلية للمؤشر". يتم حساب عنوان متغير غير موجود ببساطة. علاوة على ذلك ، إذا كان المؤشر iwnn صفرًا ، فسيتم إنهاء الوظيفة. لذلك ، لم يحدث شيء سيئ لأننا قمنا بحساب عنوان أحد أعضاء الفصل بشكل غير صحيح.لا ، لا يمكنك التحدث بهذه الطريقة. يؤدي هذا الرمز إلى سلوك غير محدد ، وبالتالي لا يمكن كتابته على هذا النحو. يمكن للسلوك غير المحدد أن يتجلى ، على سبيل المثال ، على النحو التالي:- يرى المحول أن المؤشر غير معرّف : iwnn-> select_selection
- لا يمكنك إلغاء الإشارة إلى مؤشر فارغ ، لأن هذا سلوك غير محدد
- يستنتج المترجم أن مؤشر iwnn لا يساوي الصفر دائمًا
- يزيل المترجم الشيك الإضافي: if (iwnn == NULL)
- هذا كل شيء ، الآن عند تنفيذ البرنامج ، لا يتم التحقق من المؤشر الفارغ ، ويبدأ العمل بالمؤشر غير الصحيح لعضو الفصل
يتم وصف هذا الموضوع بمزيد من التفصيل في مقالتي "يؤدي إلغاء الإشارة إلى مؤشر فارغ إلى سلوك غير محدد ."لا يتم مسح البيانات الخاصة في الذاكرة
ضع في اعتبارك نوعًا خطيرًا من الثغرات المحتملة التي تم تصنيفها وفقًا لتعداد الضعف العام CWE-14 : إزالة المترجم من التعليمات البرمجية لمسح المخازن المؤقتة.باختصار ، فإن الخلاصة هي: يحق للمترجم إزالة استدعاء دالة memset إذا لم يعد يتم استخدام المخزن المؤقت بعد ذلك.عندما أكتب عن هذا النوع من الثغرات الأمنية ، تظهر التعليقات بالضرورة أن هذا مجرد خلل في المترجم يحتاج إلى إصلاح. لا ، هذا ليس خلل. وقبل الاعتراض ، يرجى قراءة المواد التالية:بشكل عام ، كل شيء خطير. هل هناك مثل هذه الأخطاء في Android؟ بالطبع هناك. هم بشكل عام العديد حيث يوجد: دليل :).دعنا نعود إلى كود Android ونلقي نظرة على بداية ونهاية وظيفة FwdLockGlue_InitializeRoundKeys ، لأن الوسط ليس مثيرًا للاهتمام بالنسبة لنا. static void FwdLockGlue_InitializeRoundKeys() { unsigned char keyEncryptionKey[KEY_SIZE]; .... memset(keyEncryptionKey, 0, KEY_SIZE);
تحذير PVS-Studio: V597 CWE-14 يمكن للمترجم حذف استدعاء وظيفة "memset" ، والذي يتم استخدامه لمسح المخزن المؤقت "keyEncryptionKey". يجب استخدام الدالة memset_s () لمسح البيانات الخاصة. FwdLockGlue.c 102 يتم إنشاءمصفوفة keyEncryptionKey على المكدس وتخزين المعلومات الخاصة. في نهاية الوظيفة ، يريدون ملء هذا المصفوفة بالأصفار حتى لا تصل بطريق الخطأ إلى حيث لا يجب. كيف يمكن للمعلومات أن تصل إلى حيث لا ينبغي أن ، مقال " الكتابة فوق الذاكرة - لماذا؟ " سوف اقول .لملء مصفوفة بمعلومات خاصة بالأصفار ، يتم استخدام وظيفة memset . يؤكد التعليق "Zero out key data" أننا نفهم كل شيء بشكل صحيح.المشكلة هي أنه مع احتمالية عالية جدًا ، سيقوم المترجم بحذف استدعاء وظيفة memset عند إنشاء نسخة الإصدار . نظرًا لعدم استخدام المخزن المؤقت بعد استدعاء memset ، فإن استدعاء وظيفة memset نفسها من وجهة نظر المترجم غير ضرورية.أكثر 10 التحذيرات كتبت ملف Android_V597.txt .حدث خطأ آخر حيث لا يتم الكتابة فوق الذاكرة ، على الرغم من أنه في هذه الحالة لا علاقة لوظيفة memset بها. void SHA1Transform(uint32_t state[5], const uint8_t buffer[64]) { uint32_t a, b, c, d, e; .... a = b = c = d = e = 0; }
تحذير PVS-Studio: V1001 CWE-563 يتم تعيين المتغير "a" ولكن لا يتم استخدامه حتى نهاية الوظيفة. اكتشف sha1.c 213PVS-Studio شذوذًا مرتبطًا بحقيقة أنه بعد تعيين القيم للمتغيرات ، لم تعد تُستخدم. صنّف المحلل هذا العيب على أنه CWE-563 : التعيين إلى متغير بدون استخدام. ومن الناحية الرسمية ، إنه على حق ، على الرغم من أننا هنا نتعامل مرة أخرى مع CWE-14. سوف يتخلص المترجم من هذه التعيينات ، لأنه من وجهة نظر C و C ++ فهي غير ضرورية. ونتيجة لذلك ، سيحتفظ المكدس بالقيم القديمة للمتغيرات a و b و c و d و e التي تخزن البيانات الخاصة.سلوك غير محدد / محدد للتنفيذ
على الرغم من أنك لست متعبًا ، فلنلق نظرة على حالة معقدة تتطلب وصفًا دقيقًا من جانبي. typedef int32_t GGLfixed; GGLfixed gglFastDivx(GGLfixed n, GGLfixed d) { if ((d>>24) && ((d>>24)+1)) { n >>= 8; d >>= 8; } return gglMulx(n, gglRecip(d)); }
تحذير PVS-Studio: V793 من الغريب أن تكون نتيجة عبارة '(d >> 24) + 1' جزءًا من الشرط. ربما كان ينبغي مقارنة هذا البيان بشيء آخر. fix.cpp 75أراد المبرمج التحقق من أن 8 بتات عالية الترتيب من المتغير d تحتوي على وحدات ، ولكن ليس كل البتات دفعة واحدة. بمعنى آخر ، أراد المبرمج التحقق من أن أي قيمة أخرى غير 0x00 و 0xFF في البايتة العالية.لقد اقترب من حل هذه المشكلة بشكل لا داعي له بشكل خلاق. بادئ ذي بدء ، تحقق من أن البتات الأكثر أهمية غير صفرية بكتابة تعبير (د >> 24). هناك ادعاءات لهذا التعبير ، ولكن من المثير للاهتمام تحليل الجانب الأيمن من التعبير: ((د >> 24) +1). يقوم المبرمج بتحويل البتات الثمانية العالية إلى البايت المنخفض. في الوقت نفسه ، يحسب أن بتة الإشارة الأكثر أهمية تتكرر في جميع البتات الأخرى. على سبيل المثال
إذا كان المتغير d هو 0b11111111'00000000'00000000'00000000 ، فحينئذٍ نحصل على القيمة 0b11111111'11111111'11111111'11111111. بإضافة 1 إلى القيمة 0xFFFFFFFF من النوع int ، يخطط المبرمج للحصول على 0. وهذا هو: -1 + 1 = 0. وهكذا ، من خلال التعبير ((d >> 24) +1) ، يتحقق من أن البتات الثمانية العالية ليست كلها تساوي 1. أفهم أن هذا صعب للغاية ، لذا يرجى تخصيص وقتك ومحاولة معرفة كيف وماذا يعمل :).الآن دعونا نرى ما هو الخطأ في هذا الرمز.عند التحول ، فإن البت الأكثر أهمية ليس بالضرورة "تلطيخ". إليك ما يقوله المعيار عن ذلك: "قيمة E1 >> E2 هي مواضع بت E2 ذات إزاحة لليمين E1. إذا كان E1 يحتوي على نوع غير موقع أو إذا كان E1 يحتوي على نوع موقّع وقيمة غير سالبة ، فإن قيمة النتيجة هي جزء لا يتجزأ من حاصل قسمة E1 / 2 ^ E2. إذا كان E1 يحتوي على نوع موقّع وقيمة سالبة ، فإن القيمة الناتجة تكون معرّفة للتنفيذ. "العرض الأخير مهم بالنسبة لنا. لذا ، التقينا بالسلوك المحدد للتنفيذ. تعتمد كيفية عمل هذا الكود على بنية المعالج الدقيق وتطبيق المترجم. بعد التحول ، قد تظهر الأصفار بشكل جيد في البتات الأكثر أهمية ، ثم سيكون التعبير ((د >> 24) +1) مختلفًا دائمًا عن 0 ، أي ستكون دائمًا قيمة حقيقية.ومن هنا الاستنتاج: لا حاجة للحكمة. ستصبح الشفرة أكثر موثوقية ومفهومة إذا كتبت ، على سبيل المثال ، مثل: GGLfixed gglFastDivx(GGLfixed n, GGLfixed d) { uint32_t hibits = static_cast<uint32_t>(d) >> 24; if (hibits != 0x00 && hibits != 0xFF) { n >>= 8; d >>= 8; } return gglMulx(n, gglRecip(d)); }
ربما ، لم أقترح الخيار المثالي ، ولكن في هذا الرمز لا يوجد سلوك محدد بواسطة التطبيق ، وسيكون من الأسهل للقارئ فهم ما يتم فحصه.أنت تستحق كوبًا من القهوة أو الشاي. صرف انتباهك واستمر: نحن في انتظار حالة مثيرة من السلوك غير المحدد.في المقابلة ، كواحد من الأسئلة الأولى لمقدم الطلب ، أسأل ما يلي: "ماذا ستطبع وظيفة printf ولماذا؟" int i = 5; printf("%d,%d", i++, i++)
الجواب الصحيح: هذا سلوك غير محدد. لم يتم تحديد إجراء حساب الوسائط الفعلية عند استدعاء الدالة. من حين لآخر ، حتى أنني أوضح أن هذا الرمز ، الذي تم تجميعه بمساعدة Visual C ++ ، يعرض "6.5" على الشاشة ، مما يربك الوافدين الجدد الضعفاء في المعرفة والروح بتجميد تام :).قد تبدو هذه مشكلة بعيدة المنال. ولكن لا ، يمكن العثور على هذا الرمز في التطبيقات الجادة ، على سبيل المثال ، في Android code. bool ComposerClient::CommandReader::parseSetLayerCursorPosition( uint16_t length) { if (length != CommandWriterBase::kSetLayerCursorPositionLength) { return false; } auto err = mHal.setLayerCursorPosition(mDisplay, mLayer, readSigned(), readSigned()); if (err != Error::NONE) { mWriter.setError(getCommandLoc(), err); } return true; }
تحذير PVS-Studio: V681 CWE-758 لا يحدد معيار اللغة ترتيبًا يتم فيه استدعاء وظائف "readSigned" أثناء تقييم الحجج. ComposerClient.cpp 836نحن مهتمون بهذا السطر من التعليمات البرمجية: mHal.setLayerCursorPosition(...., readSigned(), readSigned());
عن طريق استدعاء الدالة readSigned ، تتم قراءة قيمتين. ولكن من المستحيل التنبؤ بأي ترتيب ستقرأ القيم. هذه حالة كلاسيكية من السلوك غير المحدد.فوائد استخدام محلل رمز ثابت
تروج هذه المقالة بأكملها تحليل الشفرة الثابتة بشكل عام وأداة PVS-Studio بشكل خاص. ومع ذلك ، فإن بعض الأخطاء مثالية فقط لإثبات إمكانات التحليل الثابت. من الصعب التعرف عليها من خلال مراجعات الكود ، ولا يلاحظها سوى برنامج غير متعب. النظر في بضع من هذه الحالات. const std::map<std::string, int32_t> kBootReasonMap = { .... {"watchdog_sdi_apps_reset", 106}, {"smpl", 107}, {"oem_modem_failed_to_powerup", 108}, {"reboot_normal", 109}, {"oem_lpass_cfg", 110},
تحذيرات PVS-Studio:- V766 CWE-462 تمت إضافة عنصر بنفس المفتاح "oem_lpass_cfg" بالفعل. 264
- V766 CWE-462 تم بالفعل إضافة عنصر بنفس المفتاح "" oem_xpu_ns_error ". بوتستات. cpp 265
يتم إدراج قيم مختلفة بنفس المفاتيح في الحاوية النقابية المصنفة ( std :: map ). من حيث تعداد الضعف العام ، هذا هو CWE-462 : مفتاح مكرر في قائمة الارتباطات.يتم اختصار نص البرنامج ويتم تمييز الأخطاء بالتعليقات ، لذلك يبدو الخطأ واضحًا ، ولكن عندما تقرأ هذا الرمز بعينيك ، يكون من الصعب جدًا العثور على مثل هذه الأخطاء.ضع في اعتبارك جزءًا آخر من التعليمات البرمجية يصعب قراءته ، لأنه من نفس النوع وغير مثير للاهتمام. MtpResponseCode MyMtpDatabase::getDevicePropertyValue(....) { .... switch (type) { case MTP_TYPE_INT8: packet.putInt8(longValue); break; case MTP_TYPE_UINT8: packet.putUInt8(longValue); break; case MTP_TYPE_INT16: packet.putInt16(longValue); break; case MTP_TYPE_UINT16: packet.putUInt16(longValue); break; case MTP_TYPE_INT32: packet.putInt32(longValue); break; case MTP_TYPE_UINT32: packet.putUInt32(longValue); break; case MTP_TYPE_INT64: packet.putInt64(longValue); break; case MTP_TYPE_UINT64: packet.putUInt64(longValue); break; case MTP_TYPE_INT128: packet.putInt128(longValue); break; case MTP_TYPE_UINT128: packet.putInt128(longValue);
تحذير PVS-Studio: V525 CWE-682 يحتوي الرمز على مجموعة الكتل المتشابهة. تحقق من العناصر "putInt8" و "putUInt8" و "putInt16" و "putUInt16" و "putInt32" و "putUInt32" و "putInt64" و "putUInt64" و "putInt128" و "putInt128" في السطور 620 و 623 و 626 و 629 ، 632، 635، 638، 641، 644، 647. android_mtp_MtpDatabase.cpp 620إذا MTP_TYPE_UINT128 كان لا بد الناجم عن وظيفة putUInt128 ، وليس putInt128 .والأخير في هذا القسم هو مثال رائع على النسخ واللصق غير الناجح. static void btif_rc_upstreams_evt(....) { .... case AVRC_PDU_REQUEST_CONTINUATION_RSP: { BTIF_TRACE_EVENT( "%s() REQUEST CONTINUATION: target_pdu: 0x%02d", __func__, pavrc_cmd->continu.target_pdu); tAVRC_RESPONSE avrc_rsp; if (p_dev->rc_connected == TRUE) { memset(&(avrc_rsp.continu), 0, sizeof(tAVRC_NEXT_RSP)); avrc_rsp.continu.opcode = opcode_from_pdu(AVRC_PDU_REQUEST_CONTINUATION_RSP); avrc_rsp.continu.pdu = AVRC_PDU_REQUEST_CONTINUATION_RSP; avrc_rsp.continu.status = AVRC_STS_NO_ERROR; avrc_rsp.continu.target_pdu = pavrc_cmd->continu.target_pdu; send_metamsg_rsp(p_dev, -1, label, ctype, &avrc_rsp); } } break; case AVRC_PDU_ABORT_CONTINUATION_RSP: { BTIF_TRACE_EVENT( "%s() ABORT CONTINUATION: target_pdu: 0x%02d", __func__, pavrc_cmd->abort.target_pdu); tAVRC_RESPONSE avrc_rsp; if (p_dev->rc_connected == TRUE) { memset(&(avrc_rsp.abort), 0, sizeof(tAVRC_NEXT_RSP)); avrc_rsp.abort.opcode = opcode_from_pdu(AVRC_PDU_ABORT_CONTINUATION_RSP); avrc_rsp.abort.pdu = AVRC_PDU_ABORT_CONTINUATION_RSP; avrc_rsp.abort.status = AVRC_STS_NO_ERROR; avrc_rsp.abort.target_pdu = pavrc_cmd->continu.target_pdu; send_metamsg_rsp(p_dev, -1, label, ctype, &avrc_rsp); } } break; .... }
قبل قراءة تحذير المحلل والنص الإضافي ، أقترح البحث عن الخطأ بنفسك.حتى لا تقرأ الجواب عن طريق الخطأ على الفور ، ها هي صورة لك لتحويل الانتباه. إذا كنت مهتمًا بما تعنيه بيضة عليها نقش جافا ، فأنت هنا .لذا ، آمل أن تكون قد استمتعت بالبحث عن الأخطاء المطبعية. حان الوقت الآن لإعطاء تحذير المحلل: تم العثور على V778 CWE-682 جزأين من التعليمات البرمجية المماثلة. ربما ، هذا هو خطأ مطبعي ويجب استخدام متغير "إحباط" بدلاً من "استمرار". btif_rc.cc 1554 علىما يبدو ، تم كتابة التعليمات البرمجية باستخدام طريقة النسخ واللصق ، ولا يمكن للشخص ، كما هو الحال دائمًا ، الانتباه في عملية تحرير جزء التعليمات البرمجية المنسوخة. ونتيجة لذلك ، في النهاية ، لم يستبدل " مستمر " بـ " إحباط ".على سبيل المثال
في الكتلة الثانية يجب كتابتها: avrc_rsp.abort.target_pdu = pavrc_cmd->abort.target_pdu;
يندرج هذا الموقف تمامًا تحت تعريف " تأثير السطر الأخير " ، نظرًا لارتكاب خطأ في استبدال الأسماء في النهاية.Facepalm
يرتبط الخطأ المضحك جدًا بالتحويل بين تنسيقات البيانات ذات النهاية الصغيرة والكبيرة (انظر ترتيب البايت ). inline uint32_t bswap32(uint32_t pData) { return (((pData & 0xFF000000) >> 24) | ((pData & 0x00FF0000) >> 8) | ((pData & 0x0000FF00) << 8) | ((pData & 0x000000FF) << 24)); } bool ELFAttribute::merge(....) { .... uint32_t subsection_length = *reinterpret_cast<const uint32_t*>(subsection_data); if (llvm::sys::IsLittleEndianHost != m_Config.targets().isLittleEndian()) bswap32(subsection_length); .... }
تحذير PVS-Studio: V530 CWE-252 يجب استخدام القيمة المرجعة للدالة 'bswap32'. ELFAttribute.cpp 84لا توجد شكاوى حول وظيفة bswap32 . ولكن يتم استخدامه بشكل غير صحيح: bswap32(subsection_length);
اقترح المؤلف أن المتغير يتم تمريره إلى الدالة حسب المرجع ويتم تغييره هناك. ومع ذلك ، يجب عليك استخدام القيمة التي أرجعتها الدالة. ونتيجة لذلك ، لا يحدث تحويل للبيانات.حدد المحلل هذا الخطأ على أنه CWE-252 : قيمة الإرجاع غير المحددة. ولكن ، في الواقع ، من الأنسب استدعاء CWE-198 : استخدام ترتيب بايت غير صحيح هنا. لسوء الحظ ، لا يستطيع المحلل فهم الخطأ من وجهة نظر عالية المستوى. لكن هذا لا يمنعه من تحديد هذا العيب الخطير في المدونة.الرمز الصحيح هو: subsection_length = bswap32(subsection_length);
هناك ثلاثة أماكن أخرى في رمز Android بها نفس الخطأ:- V530 CWE-252 يجب استخدام القيمة المرجعة للدالة 'bswap32'. ELFAttribute.cpp 218
- V530 CWE-252 يجب استخدام القيمة المرجعة للدالة 'bswap32'. 349- د
- V530 CWE-252 يجب استخدام القيمة المرجعة للدالة 'bswap32'. ELFAttribute.cpp 352
لتجنب مثل هذه الأخطاء ، يوصى باستخدام السمة [[nodiscard]] . تُستخدم هذه السمة للإشارة إلى أنه يجب استخدام القيمة المرجعة للدالة عند الاستدعاء. لذلك ، إذا كتبت مثل هذا: [[nodiscard]] inline uint32_t bswap32(uint32_t pData) { ... }
ثم يتم الكشف عن الخطأ حتى في مرحلة تجميع الملف. يمكنك معرفة المزيد عن بعض السمات المفيدة الجديدة من مقال زميلي " C ++ 17 ".رمز لا يمكن الوصول إليه
في البرمجة ونظرية المترجم ، الكود غير القابل للوصول هو جزء من كود البرنامج الذي لا يمكن تنفيذه تحت أي ظرف ، لأنه لا يمكن الوصول إليه في الرسم البياني لسير التحكم.من وجهة نظر تعداد الضعف العام CWE-561 : Dead Code. virtual sp<IEffect> createEffect(....) { .... if (pDesc == NULL) { return effect; if (status != NULL) { *status = BAD_VALUE; } } .... }
تحذير PVS-Studio: تم الكشف عن V779 CWE-561 رمز لا يمكن الوصول إليه. من الممكن أن يكون هناك خطأ. IAudioFlinger.cpp 733مشغل عودة الواضح يجب أن يكون موجودا أدناه.أخطاء أخرى من هذا النوع:- تم الكشف عن V779 CWE-561 رمز لا يمكن الوصول إليه. من الممكن أن يكون هناك خطأ. 612
- تم الكشف عن V779 CWE-561 رمز لا يمكن الوصول إليه. من الممكن أن يكون هناك خطأ. 468
- تم الكشف عن V779 CWE-561 رمز لا يمكن الوصول إليه. من الممكن أن يكون هناك خطأ. AMPEG4AudioAssembler.cpp 187
استراحة
الفاصل المنسي داخل المفتاح خطأ كلاسيكي لمبرمجي C و C ++. لمكافحته ، ظهر تعليق توضيحي مفيد في C ++ 17 كـ [[سقوط]] . يمكنك قراءة المزيد عن هذا الخطأ و [[fallthrough]] في مقالتي " break androughthrough ".ولكن في حين أن العالم مليء بالرمز القديم حيث لا يتم استخدام [[السقوط]] ، فإن PVS-Studio مفيد لك. ضع في اعتبارك بعض الأخطاء الموجودة في Android. وفقًا لتعداد الضعف العام ، يتم تصنيف هذه الأخطاء على أنها CWE-484 : بيان فاصل محذوف في التبديل. bool A2dpCodecConfigLdac::setCodecConfig(....) { .... case BTAV_A2DP_CODEC_SAMPLE_RATE_192000: if (sampleRate & A2DP_LDAC_SAMPLING_FREQ_192000) { result_config_cie.sampleRate = A2DP_LDAC_SAMPLING_FREQ_192000; codec_capability_.sample_rate = codec_user_config_.sample_rate; codec_config_.sample_rate = codec_user_config_.sample_rate; } case BTAV_A2DP_CODEC_SAMPLE_RATE_16000: case BTAV_A2DP_CODEC_SAMPLE_RATE_24000: case BTAV_A2DP_CODEC_SAMPLE_RATE_NONE: codec_capability_.sample_rate = BTAV_A2DP_CODEC_SAMPLE_RATE_NONE; codec_config_.sample_rate = BTAV_A2DP_CODEC_SAMPLE_RATE_NONE; break; .... }
تحذير PVS-Studio: V796 CWE-484 من المحتمل أن تكون عبارة "فاصل" مفقودة في عبارة التبديل. a2dp_vendor_ldac.cc 912أعتقد أن الخطأ لا يحتاج إلى تفسير. ألاحظ أنه غالبًا ما يتم اكتشاف شذوذ في الرمز بأكثر من طريقة. على سبيل المثال ، تم اكتشاف هذا الخطأ أيضًا بواسطة تحذيرات V519:- V519 CWE-563 يتم تعيين قيم المتغير 'codec_capability_.sample_rate' مرتين على التوالي. ربما هذا خطأ. خطوط التحقق: 910 ، 916. a2dp_vendor_ldac.cc 916
- V519 CWE-563 يتم تعيين قيم المتغير 'codec_config_.sample_rate' مرتين على التوالي. ربما هذا خطأ. خطوط التحقق: 911 ، 917. a2dp_vendor_ldac.cc 917
وبعض الأخطاء: Return<void> EffectsFactory::getAllDescriptors(....) { .... switch (status) { case -ENOSYS: {
تحذير PVS-Studio: V796 CWE-484 من المحتمل أن تكون عبارة "فاصل" مفقودة في عبارة التبديل. EffectsFactory.cpp 118 int Reverb_getParameter(....) { .... case REVERB_PARAM_REFLECTIONS_LEVEL: *(uint16_t *)pValue = 0; case REVERB_PARAM_REFLECTIONS_DELAY: *(uint32_t *)pValue = 0; case REVERB_PARAM_REVERB_DELAY: *(uint32_t *)pValue = 0; break; .... }
تحذير PVS-Studio: V796 CWE-484 من المحتمل أن تكون عبارة "فاصل" مفقودة في عبارة التبديل. EffeverReverb.cpp 1847 static SLresult IAndroidConfiguration_GetConfiguration(....) { .... switch (IObjectToObjectID((thiz)->mThis)) { case SL_OBJECTID_AUDIORECORDER: result = android_audioRecorder_getConfig( (CAudioRecorder *) thiz->mThis, configKey, pValueSize, pConfigValue); break; case SL_OBJECTID_AUDIOPLAYER: result = android_audioPlayer_getConfig( (CAudioPlayer *) thiz->mThis, configKey, pValueSize, pConfigValue); default: result = SL_RESULT_FEATURE_UNSUPPORTED; break; } .... }
تحذير PVS-Studio: V796 CWE-484 من المحتمل أن تكون عبارة "فاصل" مفقودة في عبارة التبديل. IAndroidConfiguration.cpp 90إدارة الذاكرة غير صحيحة
لقد قمت بتجميع الأخطاء المتعلقة بإدارة الذاكرة غير الصحيحة. يتم تصنيف هذه التحذيرات ، وفقًا لتعداد الضعف العام ، على النحو التالي:- CWE-401 : تحرير غير صحيح للذاكرة قبل إزالة المرجع الأخير ("تسرب الذاكرة")
- CWE-562 : عودة عنوان متغير المكدس
- CWE-762 : إجراءات إدارة الذاكرة غير المتطابقة
لنبدأ بالوظائف التي ترجع مرجعًا إلى متغير مدمر بالفعل. TransformIterator& operator++(int) { TransformIterator tmp(*this); ++*this; return tmp; } TransformIterator& operator--(int) { TransformIterator tmp(*this); --*this; return tmp; }
تحذيرات PVS-Studio:- تعيد الدالة V558 CWE-562 المرجع إلى كائن محلي مؤقت: tmp. transform_iterator.h 77
- تعيد الدالة V558 CWE-562 المرجع إلى كائن محلي مؤقت: tmp. transform_iterator.h 92
عندما تنتهي الوظائف من تنفيذها ، يتم تدمير متغير tmp ، حيث تم إنشاؤه على المكدس. وبالتالي ، تقوم الدالات بإرجاع مرجع إلى كائن تم تدميره بالفعل (غير موجود).سيكون الحل الصحيح هو إرجاع قيمة: TransformIterator operator++(int) { TransformIterator tmp(*this); ++*this; return tmp; } TransformIterator operator--(int) { TransformIterator tmp(*this); --*this; return tmp; }
فكر في رمز أكثر حزنًا يستحق اهتمامًا وثيقًا. int register_socket_transport( int s, const char* serial, int port, int local) { atransport* t = new atransport(); if (!serial) { char buf[32]; snprintf(buf, sizeof(buf), "T-%p", t); serial = buf; } .... }
تحذير PVS-Studio: يتم تخزين مؤشر V507 CWE-562 إلى الصفيف المحلي 'buf' خارج نطاق هذا الصفيف. سيصبح هذا المؤشر غير صالح. transport.cpp 1030هذا رمز خطير. إذا كانت القيمة الفعلية للوسيطة التسلسلية فارغة ، فيجب استخدام مخزن مؤقت مؤقت على المكدس. عندما ينتهي نص تعليمة if ، يتوقف صفيف buf عن الوجود. يمكن استخدام المكان الذي تم فيه إنشاء المخزن المؤقت لتخزين المتغيرات الأخرى التي تم إنشاؤها على المكدس. ستبدأ فوضى جهنمية في البيانات ، ولا يمكن توقع عواقب مثل هذا الخطأ.تتعلق الأخطاء التالية بالأساليب غير المتوافقة لإنشاء الكائنات وتدميرها. void SensorService::SensorEventConnection::reAllocateCacheLocked(....) { sensors_event_t *eventCache_new; const int new_cache_size = computeMaxCacheSizeLocked(); eventCache_new = new sensors_event_t[new_cache_size]; .... delete mEventCache; mEventCache = eventCache_new; mCacheSize += count; mMaxCacheSize = new_cache_size; }
تحذير PVS-Studio: V611 CWE-762 تم تخصيص الذاكرة باستخدام عامل التشغيل "T الجديد" ولكن تم تحريرها باستخدام عامل التشغيل "حذف". خذ بعين الاعتبار فحص هذا الرمز. من الأفضل استخدام "حذف [] mEventCache ؛". تحقق من الخطوط: 391 ، 384. SensorEventConnection.cpp 391كل شيء بسيط هنا. يتم تخصيص المخزن المؤقت المشار إليه بواسطة عضو من فئة mEventCache باستخدام عامل التشغيل [] الجديد . وقم بتحرير هذه الذاكرة باستخدام عامل الحذف . هذا غير صحيح ويؤدي إلى سلوك برنامج غير محدد.خطأ مماثل: aaudio_result_t AAudioServiceEndpointCapture::open(....) { .... delete mDistributionBuffer; int distributionBufferSizeBytes = getStreamInternal()->getFramesPerBurst() * getStreamInternal()->getBytesPerFrame(); mDistributionBuffer = new uint8_t[distributionBufferSizeBytes]; .... }
تحذير PVS-Studio: V611 CWE-762 تم تخصيص الذاكرة باستخدام عامل التشغيل "T الجديد" ولكن تم تحريرها باستخدام عامل التشغيل "حذف". خذ بعين الاعتبار فحص هذا الرمز. ربما من الأفضل استخدام "حذف [] mDistributionBuffer ؛". AAudioServiceEndpointCapture.cpp 50أعتقد أن الخطأ لا يتطلب أي تفسير.الحالة التالية أكثر إثارة للاهتمام ، ولكن جوهر الخطأ هو نفسه تمامًا. struct HeifFrameInfo { .... void set(....) { .... mIccData.reset(new uint8_t[iccSize]); .... } .... std::unique_ptr<uint8_t> mIccData; };
V554 CWE-762 الاستخدام الخاطئ لـ unique_ptr. سيتم تنظيف الذاكرة المخصصة لـ "new []" باستخدام "delete". HeifDecoderAPI.h 62بشكل افتراضي ، تستدعي فئة المؤشر الذكي std :: unique_ptr عامل الحذف لتدمير كائن . ومع ذلك ، في الوظيفة المحددة ، يتم تخصيص الذاكرة باستخدام عامل التشغيل [] الجديد .الخيار الصحيح: std::unique_ptr<uint8_t[]> mIccData;
أخطاء أخرى:- V554 CWE-762 الاستخدام الخاطئ لـ unique_ptr. سيتم تنظيف الذاكرة المخصصة لـ "new []" باستخدام "delete". 949
- V554 CWE-762 الاستخدام الخاطئ لـ unique_ptr. سيتم تنظيف الذاكرة المخصصة لـ "new []" باستخدام "delete". عوض محمد محمد المطيري 950
- V554 CWE-762 الاستخدام الخاطئ لـ unique_ptr. سيتم تنظيف الذاكرة المخصصة لـ "new []" باستخدام "delete". HeifDecoderImpl.cpp 102
- V554 CWE-762 الاستخدام الخاطئ لـ unique_ptr. سيتم تنظيف الذاكرة المخصصة لـ "new []" باستخدام "delete". 166
- V554 CWE-762 الاستخدام الخاطئ لـ unique_ptr. سيتم تنظيف الذاكرة المخصصة لـ "new []" باستخدام "delete". ColorSpace.cpp 360
ستقوم أخطاء تسرب الذاكرة بإكمال هذا القسم. المفاجأة غير السارة هي أنه كان هناك أكثر من 20 خطأ من هذا القبيل ، ويبدو لي أن هذه العيوب يمكن أن تكون مؤلمة للغاية ، مما يؤدي إلى انخفاض تدريجي في الذاكرة الحرة أثناء التشغيل المستمر المطول لنظام التشغيل. Asset* Asset::createFromUncompressedMap(FileMap* dataMap, AccessMode mode) { _FileAsset* pAsset; status_t result; pAsset = new _FileAsset; result = pAsset->openChunk(dataMap); if (result != NO_ERROR) return NULL; pAsset->mAccessMode = mode; return pAsset; }
تحذير PVS-Studio: V773 CWE-401 تم إنهاء الوظيفة بدون تحرير مؤشر "pAsset". من الممكن حدوث تسرب للذاكرة. Asset.cpp 296إذا لم يكن من الممكن فتح جزء معين ، فستخرج الوظيفة دون تدمير الكائن ، حيث يتم تخزين المؤشر في متغير pAsset . نتيجة لذلك ، سيحدث تسرب للذاكرة.أخطاء أخرى متشابهة ، لذلك لا أرى أي سبب للنظر فيها في المقالة. يمكن للمهتمين رؤية تحذيرات أخرى في الملف: Android_V773.txt .السفر إلى الخارج
هناك عدد كبير من الأنماط الخاطئة التي تؤدي إلى تجاوز الصفيف. في حالة Android ، اكتشفت نمط خطأ واحدًا فقط من النوع التالي: if (i < 0 || i > MAX) return; A[i] = x;
في C و C ++ ، يتم ترقيم خلايا الصفيف من 0 ، لذا يجب أن يكون الحد الأقصى لفهرس عنصر في الصفيف أقل من حجم الصفيف نفسه. يجب أن يكون الفحص الصحيح كما يلي: if (i < 0 || i >= MAX) return; A[i] = x;
يتم تصنيف تجاوز الصفيف ، وفقًا لتعداد الضعف العام ، على أنه CWE-119 : تقييد غير مناسب للعمليات داخل حدود ذاكرة التخزين المؤقت.ألق نظرة على كيفية ظهور هذه الأخطاء في رمز Android. static btif_hf_cb_t btif_hf_cb[BTA_AG_MAX_NUM_CLIENTS]; static bool IsSlcConnected(RawAddress* bd_addr) { if (!bd_addr) { LOG(WARNING) << __func__ << ": bd_addr is null"; return false; } int idx = btif_hf_idx_by_bdaddr(bd_addr); if (idx < 0 || idx > BTA_AG_MAX_NUM_CLIENTS) { LOG(WARNING) << __func__ << ": invalid index " << idx << " for " << *bd_addr; return false; } return btif_hf_cb[idx].state == BTHF_CONNECTION_STATE_SLC_CONNECTED; }
تحذير PVS-Studio: يمكن تجاوز صفيف V557 CWE-119. يمكن أن تصل قيمة مؤشر "idx" إلى 6. btif_hf.cc 277خيار الاختيار الصحيح: if (idx < 0 || idx >= BTA_AG_MAX_NUM_CLIENTS) {
تم العثور على نفس الأخطاء بالضبط شيئين آخرين:- يمكن تجاوز صفيف V557 CWE-119. يمكن أن تصل قيمة مؤشر idx إلى 6. btif_hf.cc 869
- يمكن تجاوز صفيف V557 CWE-119. يمكن أن تصل قيمة مؤشر "الفهرس" إلى 6. btif_rc.cc 374
دورات مكسورة
هناك العديد من الطرق لكتابة دورة معيبة. في رمز Android ، التقيت بأخطاء يمكن تصنيفها وفقًا لتعداد الضعف العام على أنها:- CWE-20 : التحقق من صحة الإدخال
- CWE-670 : تنفيذ تدفق التحكم غير الصحيح دائمًا
- CWE-691 : إدارة تدفق التحكم غير كافية
- CWE-834 : التكرار المفرط
في الوقت نفسه ، بالطبع ، هناك طرق أخرى "لتصويب ساقك" عند كتابة الدورات ، لكن هذه المرة لم يلتقوا بي. int main(int argc, char **argv) { .... char c; printf("%s is already in *.base_fs format, just ..... ", ....); rewind(blk_alloc_file); while ((c = fgetc(blk_alloc_file)) != EOF) { fputc(c, base_fs_file); } .... }
تحذير PVS-Studio: لا يجب مقارنة V739 CWE-20 EOF بقيمة من النوع 'char'. يجب أن يكون "(c = fgetc (blk_alloc_file))" من النوع "int". blk_alloc_to_base_fs.c 61اكتشف المحلل أن ثابت EOF يقارن بمتغير من النوع char . دعونا نرى لماذا هذا الرمز غير صحيح. تعرضالدالة fgetc قيمة int ، وهي: يمكنها إرجاع رقم من 0 إلى 255 أو EOF (-1). يتم وضع قيمة القراءة في متغير من النوع char . وبسبب هذا ، يصبح الحرف بقيمة 0xFF (255) -1 ويتم تفسيره بنفس طريقة نهاية الملف (EOF).بسبب مثل هذه الأخطاء ، يواجه المستخدمون الذين يستخدمون رموز ASCII الموسعة أحيانًا حالة حيث تتم معالجة أحد الأحرف في أبجديتهم بشكل غير صحيح بواسطة البرامج. على سبيل المثال ، الحرف الأخير من الأبجدية الروسية في ترميز Windows-1251 يحتوي فقط على الرمز 0xFF ويتم إدراكه من قبل بعض البرامج على أنه نهاية الملف.تلخيص ، يمكننا القول أن شرط إيقاف الدورة مكتوب بشكل غير صحيح. لإصلاح الموقف ، يجب أن يكون المتغير c من النوع int.نستمر ونأخذ في الاعتبار المزيد من الأخطاء المألوفة عند استخدام العبارة for . status_t AudioPolicyManager::registerPolicyMixes(....) { .... for (size_t i = 0; i < mixes.size(); i++) { .... for (size_t j = 0; i < mHwModules.size(); j++) {
تحذير PVS-Studio: V534 CWE-691 من المحتمل أن تتم مقارنة متغير خاطئ داخل عامل التشغيل "for". ضع في اعتبارك مراجعة "i". AudioPolicyManager.cpp 2489نظرًا لخطأ مطبعي في حلقة متداخلة ، تستخدم الحالة المتغير i ، على الرغم من أنه يجب عليك استخدام المتغير j . ونتيجة لذلك ، يتم زيادة المتغير j بشكل لا يمكن السيطرة عليه ، مما يؤدي بمرور الوقت إلى خروج مجموعة mHwModules من الحدود . من المستحيل التنبؤ بما سيحدث بعد ذلك ، حيث سيكون هناك سلوك برنامج غير محدد.بالمناسبة ، تم نسخ هذا الجزء مع خطأ بالكامل إلى وظيفة أخرى. لذلك ، وجد المحلل الخطأ نفسه بالضبط هنا: AudioPolicyManager.cpp 2586.هناك 3 مقتطفات أخرى من التعليمات البرمجية مريبة للغاية بالنسبة لي. ومع ذلك ، لا أفترض أن أقول إن هذا الرمز خاطئ بالتأكيد ، حيث يوجد منطق معقد هناك. على أي حال ، لا بد لي من الانتباه إلى هذا الرمز حتى يتحقق المؤلف.الجزء الأول: void ce_t3t_handle_check_cmd(....) { .... for (i = 0; i < p_cb->cur_cmd.num_blocks; i++) { .... for (i = 0; i < T3T_MSG_NDEF_ATTR_INFO_SIZE; i++) { checksum += p_temp[i]; } .... } .... }
تحذير PVS-Studio: V535 CWE-691 يتم استخدام المتغير 'i' لهذه الحلقة وللحلقة الخارجية. تحقق من الخطوط: 398 ، 452. ce_t3t.cc 452لاحظ أن المتغير i يستخدم للدورتين الخارجية والداخلية.محفزان آخران مشابهان:- V535 CWE-691 يتم استخدام المتغير 'xx' لهذه الحلقة وللحلقة الخارجية. خطوط التحقق: 801 ، 807. sdp_db.cc 807
- V535 CWE-691 يتم استخدام المتغير 'xx' لهذه الحلقة وللحلقة الخارجية. خطوط التحقق: 424 ، 438. nfa_hci_act.cc 438
ألست متعبًا بعد؟ أقترح إيقاف برنامج PVS-Studio مؤقتًا وتنزيله لمحاولة التحقق من مشروعك.الآن دعنا نواصل. #define NFA_HCI_LAST_PROP_GATE 0xFF tNFA_HCI_DYN_GATE* nfa_hciu_alloc_gate(uint8_t gate_id, tNFA_HANDLE app_handle) { .... for (gate_id = NFA_HCI_FIRST_HOST_SPECIFIC_GENERIC_GATE; gate_id <= NFA_HCI_LAST_PROP_GATE; gate_id++) { if (gate_id == NFA_HCI_CONNECTIVITY_GATE) gate_id++; if (nfa_hciu_find_gate_by_gid(gate_id) == NULL) break; } if (gate_id > NFA_HCI_LAST_PROP_GATE) { LOG(ERROR) << StringPrintf( "nfa_hci_alloc_gate - no free Gate ID: %u " "App Handle: 0x%04x", gate_id, app_handle); return (NULL); } .... }
تحذير PVS-Studio: V654 CWE-834 الشرط 'gate_id <= 0xFF' للحلقة دائمًا. nfa_hci_utils.cc 248لاحظ ما يلي:- الثابت NFA_HCI_LAST_PROP_GATE يساوي 0xFF.
- يتم استخدام متغير من النوع uint8_t كعداد حلقة . لذلك ، فإن نطاق قيمة هذا المتغير هو [0..0xFF].
وتبين أن الشرط gate_id <= NFA_HCI_LAST_PROP_GATE صحيح دائمًا ولا يمكنه إيقاف تنفيذ الحلقة.صنف المحلل هذا الخطأ على أنه CWE-834 ، ولكن يمكن أيضًا تفسيره على أنه CWE-571: التعبير دائمًا صحيح.الخطأ التالي في الحلقة مرتبط بسلوك غير محدد. status_t SimpleDecodingSource::doRead(....) { .... for (int retries = 0; ++retries; ) { .... }
تحذير PVS-Studio: V654 CWE-834 شرط "إعادة المحاولة ++" للحلقة دائمًا. SimpleDecodingSource.cpp 226 علىما يبدو ، أراد المبرمج أن يأخذ متغير إعادة المحاولة جميع القيم الممكنة للمتغير int وعندها تنتهي الحلقة فقط.يجب أن تتوقف الحلقة عند إعادة محاولة التعبير ++ 0. وهذا ممكن فقط إذا تجاوز المتغير. نظرًا لأن المتغير من النوع الموقّع ، فإن تجاوزه يؤدي إلى سلوك غير محدد. لذلك ، هذا الرمز غير صحيح ويمكن أن يؤدي إلى عواقب غير متوقعة. على سبيل المثال ، المترجم لديه الحق الكامل في إزالة الشيك وترك التعليمات فقط لزيادة العداد.والخطأ الأخير في هذا القسم. status_t Check(const std::string& source) { .... int pass = 1; .... do { .... switch(rc) { case 0: SLOGI("Filesystem check completed OK"); return 0; case 2: SLOGE("Filesystem check failed (not a FAT filesystem)"); errno = ENODATA; return -1; case 4: if (pass++ <= 3) { SLOGW("Filesystem modified - rechecking (pass %d)", pass); continue;
تحذير PVS-Studio: V696 CWE-670 سيتم إنهاء عامل التشغيل "استمرار" {do} {/} أثناء التكرار (FALSE) لأن الحالة خاطئة دائمًا. تحقق من الخطوط: 105 ، 121. Vfat.cpp 105أمامنا حلقة من النموذج: do { .... if (x) continue; .... } while (0)
لأداء التكرار المتكرر، يستخدم مبرمج المشغل 'تواصل' . هذا خطأ. لا تستمر عبارة المتابعة في استئناف الحلقة على الفور ، ولكنها تمضي في التحقق من الحالة. نظرًا لأن الشرط دائمًا خاطئ ، فسيتم تنفيذ الدورة على أي حال مرة واحدة فقط.لإصلاح الخطأ ، يمكن إعادة كتابة الرمز ، على سبيل المثال ، على النحو التالي: for (;;) { .... if (x) continue; .... break; }
إعادة تعيين متغيرة
من الأخطاء الشائعة جدًا إعادة الكتابة إلى متغير قبل استخدام القيمة السابقة. في أغلب الأحيان ، تحدث مثل هذه الأخطاء بسبب خطأ مطبعي أو نسخ ولصق غير ناجح. وفقًا لتعداد الضعف العام ، يتم تصنيف هذه الأخطاء على أنها CWE-563 : التعيين إلى متغير بدون استخدام. لا يخلو من مثل هذه الأخطاء في Android. status_t XMLNode::flatten_node(....) const { .... memset(&namespaceExt, 0, sizeof(namespaceExt)); if (mNamespacePrefix.size() > 0) { namespaceExt.prefix.index = htodl(strings.offsetForString(mNamespacePrefix)); } else { namespaceExt.prefix.index = htodl((uint32_t)-1); } namespaceExt.prefix.index = htodl(strings.offsetForString(mNamespacePrefix)); namespaceExt.uri.index = htodl(strings.offsetForString(mNamespaceUri)); .... }
تحذير PVS-Studio: V519 CWE-563 يتم تعيين قيم المتغير "namespaceExt.prefix.index" مرتين على التوالي. ربما هذا خطأ. تحقق من الأسطر: 1535 ، 1539. XMLNode.cpp 1539لتسليط الضوء على جوهر الخطأ ، سأكتب رمزًا زائفًا : if (a > 0) X = 1; else X = 2; X = 1;
بغض النظر عن الحالة ، سيتم دائمًا تعيين قيمة واحدة للمتغير X (في الكود الحالي وهو namespaceExt.prefix.index ). bool AudioFlinger::RecordThread::threadLoop() { .... size_t framesToRead = mBufferSize / mFrameSize; framesToRead = min(mRsmpInFramesOA - rear, mRsmpInFramesP2 / 2); .... }
تحذير PVS-Studio: V519 CWE-563 يتم تعيين قيم المتغير "FramToRead" مرتين على التوالي. ربما هذا خطأ. تحقق من السطور: 6341 ، 6342. Threads.cpp 6342ليس من الواضح لماذا كان من الضروري تهيئة المتغير عند الإعلان ، إذا تم كتابة قيمة أخرى عليه على الفور. هناك خطأ ما هنا. void SchedulingLatencyVisitorARM::VisitArrayGet(....) { .... if (index->IsConstant()) { last_visited_latency_ = kArmMemoryLoadLatency; } else { if (has_intermediate_address) { } else { last_visited_internal_latency_ += kArmIntegerOpLatency; } last_visited_internal_latency_ = kArmMemoryLoadLatency; } .... }
تحذير PVS-Studio: V519 CWE-563 يتم تعيين قيم المتغير "last_visited_internal_latency_" مرتين على التوالي. ربما هذا خطأ. تحقق من الخطوط: 680 ، 682. Scheduler_arm.cc 682كود غريب للغاية ، بلا معنى. أجرؤ على اقتراح أنه كان يجب كتابته: last_visited_internal_latency_ += kArmMemoryLoadLatency;
والخطأ الأخير ، يوضح كيف يعثر المحلل بلا كلل على الأخطاء التي من المرجح أن يتم تخطيها حتى مع مراجعة التعليمات البرمجية بعناية. void multiprecision_fast_mod(uint32_t* c, uint32_t* a) { uint32_t U; uint32_t V; .... c[0] += U; V = c[0] < U; c[1] += V; V = c[1] < V; c[2] += V;
تحذير PVS-Studio: V519 CWE-563 يتم تعيين المتغير "V" مرتين على التوالي. ربما هذا خطأ. تحقق من الخطوط: 307 ، 309. p_256_multprecision.cc 309الشفرة " مزق عينيك" ولا أريد أن أفهمها. هذا واضح للعيان: هناك خطأ مطبعي في الرمز ، والذي أبرزته بالتعليقات.البق الأخرى
هناك أخطاء متفرقة لا معنى لها لفصول منفصلة. ومع ذلك ، فهي مثيرة للاهتمام وغادرة مثل تلك التي تم النظر فيها سابقًا.العمليات ذات الأولوية void TagMonitor::parseTagsToMonitor(String8 tagNames) { std::lock_guard<std::mutex> lock(mMonitorMutex);
تحذير PVS-Studio: V593 CWE-783 ضع في اعتبارك مراجعة تعبير النوع 'A = B! = C'. يتم حساب التعبير كما يلي: 'A = (B! = C)'. TagMonitor.cpp 50تعداد الضعف العام: CWE-783 : خطأ منطق أسبقية عامل التشغيل.تصور المبرمج ما يلي. يتم البحث عن السلسلة الفرعية "3 أ" ويتم كتابة موضع هذه السلسلة الفرعية إلى متغير idx . إذا تم العثور على السلسلة الفرعية (idx! = -1) ، فسيبدأ تشغيل الكود ، والذي يستخدم قيمة متغير idx .لسوء الحظ ، يتم الخلط بين المبرمج حول أولويات العمليات. في الواقع ، يعمل الشيك على النحو التالي: if (ssize_t idx = (tagNames.find("3a") != -1))
أولاً ، يتحقق من وجود سلسلة فرعية "3a" في السلسلة ، ويتم وضع النتيجة false / true في متغير idx . نتيجة لذلك ، يكون للمتغير idx القيمة 0 أو 1.إذا كان الشرط صحيحًا (المتغير idx هو 1) ، فإن المنطق الذي يستخدم متغير idx يبدأ في التنفيذ . سيؤدي متغير يساوي دائمًا 1 إلى سلوك برنامج غير صحيح.يمكنك إصلاح الخطأ إذا قمت بتهيئة المتغير من الشرط: ssize_t idx = tagNames.find("3a"); if (idx != -1)
يتيح لك الإصدار الجديد من C ++ 17 أيضًا كتابة: if (ssize_t idx = tagNames.find("3a"); idx != -1)
مُنشئ غير صالح struct HearingDevice { .... HearingDevice() { HearingDevice(RawAddress::kEmpty, false); } .... };
تحذير PVS-Studio: V603 CWE-665 تم إنشاء الكائن ولكن لم يتم استخدامه. إذا كنت ترغب في استدعاء المُنشئ ، فيجب استخدام 'this-> HearingDevice :: HearingDevice (....)'. جلسة استماع _aid.cc 176تعداد الضعف العام: CWE-665 : التهيئة غير الصحيحة.غالبًا ما يخطئ المبرمجون عند محاولة استدعاء مُنشئ بشكل صريح لتهيئة كائن. هناك نوعان من المنشئين في الفصل. لتقليل حجم شفرة المصدر ، قرر المبرمج استدعاء مُنشئ من آخر. لكن هذا الكود لا يفعل على الإطلاق ما يتوقعه المطور.يحدث ما يلي. يتم إنشاء كائن جديد غير مسمى من النوع HearingDevice ثم تدميره. نتيجة لذلك ، تبقى حقول الصف غير مهيأة.لإصلاح الخطأ ، يمكنك استخدام مُنشئ التفويض (ظهرت هذه الميزة في C ++ 11). الرمز الصحيح هو: HearingDevice() : HearingDevice(RawAddress::kEmpty, false) { }
لا تُرجع الدالة القيمة int NET_RecvFrom(int s, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen) { socklen_t socklen = *fromlen; BLOCKING_IO_RETURN_INT( s, recvfrom(s, buf, len, flags, from, &socklen) ); *fromlen = socklen; }
تحذير PVS-Studio: يجب أن ترجع الدالة V591 CWE-393 قيمة. linux_close.cpp 139تعداد الضعف العام: CWE-393 : عودة كود الحالة الخاطئ.ستُرجع الدالة قيمة عشوائية. خطأ آخر من هذا القبيل: يجب أن ترجع الدالة V591 CWE-393 قيمة. linux_close.cpp 158حساب حجم الهيكل غير صحيح int MtpFfsHandle::handleControlRequest(....) { .... struct mtp_device_status *st = reinterpret_cast<struct mtp_device_status*>(buf.data()); st->wLength = htole16(sizeof(st)); .... }
تحذير PVS-Studio: V568 من الغريب أن يقوم عامل "sizeof ()" بتقييم حجم المؤشر إلى فئة ، ولكن ليس حجم كائن فئة "st". MtpFfsHandle.cpp 251أنا متأكد من أنهم أرادوا وضع حجم الهيكل ، وليس حجم المؤشر ، في متغير عضو wLength . ثم يجب أن يكون الرمز الصحيح كما يلي: st->wLength = htole16(sizeof(*st));
استجابات محلل مماثلة:- V568 من الغريب أن يقوم عامل "sizeof ()" بتقييم حجم المؤشر إلى فئة ، ولكن ليس حجم كائن فئة "cacheinfo". NetlinkEvent.cpp 220
- V568 من الغريب أن يقوم عامل "sizeof ()" بتقييم حجم المؤشر إلى فئة ، ولكن ليس حجم كائن الفئة 'page-> next'. 146 /
- V568 من الغريب أن حجة عامل sizeof () هي التعبير '& session_id'. المرجع- ril.c 1775
عمليات بت لا معنى لها #define EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR 0x00000001 #define EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR 0x00000002 #define EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR 0x00000004 EGLContext eglCreateContext(....) { .... case EGL_CONTEXT_FLAGS_KHR: if ((attrib_val | EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR) || (attrib_val | EGL_CONTEXT_OPENGL_FORWARD_C....) || (attrib_val | EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR)) { context_flags = attrib_val; } else { RETURN_ERROR(EGL_NO_CONTEXT,EGL_BAD_ATTRIBUTE); } .... }
تحذير PVS-Studio: V617 CWE-480 فكر في فحص الحالة. وسيطة "0x00000001" الخاصة بـ "|" تحتوي عملية bitwise على قيمة غير صفرية. egl.cpp 1329تعداد نقاط الضعف الشائعة: CWE-480 : استخدام عامل تشغيل غير صحيح.تعبير عن النموذج (A | 1) || (أ | 2) || (A | 4) لا معنى له ، لأن النتيجة ستكون دائمًا صحيحة. في الواقع ، تحتاج إلى استخدام عامل التشغيل & ، ثم يكون الرمز منطقيًا: if ((attrib_val & EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR) || (attrib_val & EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR) || (attrib_val & EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR))
خطأ مشابه: V617 CWE-480 فكر في فحص الحالة. وسيطة "0x00000001" الخاصة بـ "|" تحتوي عملية bitwise على قيمة غير صفرية. egl.cpp 1338إزاحة البت غير الصحيحة template <typename AddressType> struct RegsInfo { .... uint64_t saved_reg_map = 0; AddressType saved_regs[64]; .... inline AddressType* Save(uint32_t reg) { if (reg > sizeof(saved_regs) / sizeof(AddressType)) { abort(); } saved_reg_map |= 1 << reg; saved_regs[reg] = (*regs)[reg]; return &(*regs)[reg]; } .... }
تحذير PVS-Studio: V629 CWE-190 فكر في فحص تعبير "1 << reg". تحويل البت لقيمة 32 بت مع التوسع اللاحق إلى نوع 64 بت. RegsInfo.h 47تعداد الضعف العام: CWE-190 : تجاوز عدد صحيح أو التفاف.مع تحول 1 << reg ، تقع قيمة المتغير reg في النطاق [0..63]. يعمل التعبير على الحصول على درجات مختلفة من درجتين ، بدءًا من 2 ^ 0 وتنتهي بـ 2 ^ 63.الكود لا يعمل. والحقيقة هي أن الحرف الرقمي 1 لديه نوع int 32 بت . لذلك ، لن يعمل للحصول على قيمة أكبر من 1 ^ 31. يؤدي التحول إلى قيمة أكبر إلى تجاوز المتغير وظهور سلوك غير محدد.الرمز الصحيح هو: saved_reg_map |= static_cast<uint64_t>(1) << reg;
أو: saved_reg_map |= 1ULL << reg;
يتم نسخ السلاسل لأنفسهم. void PCLmGenerator::writeJobTicket() {
تحذيرات PVS-Studio:- V549 CWE-688 الوسيطة الأولى للدالة 'strcpy' تساوي الوسيطة الثانية. GenPCLm.cpp 1181
- V549 CWE-688 الوسيطة الأولى للدالة 'strcpy' تساوي الوسيطة الثانية. GenPCLm.cpp 1182
وفقًا لتصنيف التعداد العام للضعف: CWE-688 : استدعاء دالة مع متغير أو مرجع غير صحيح كوسيطة.يتم نسخ السلاسل لسبب ما لأنفسهم. على الأرجح ، هناك بعض الأخطاء المطبعية هنا.باستخدام متغير غير مهيأ void mca_set_cfg_by_tbl(....) { tMCA_DCB* p_dcb; const tL2CAP_FCR_OPTS* p_opt; tMCA_FCS_OPT fcs = MCA_FCS_NONE; if (p_tbl->tcid == MCA_CTRL_TCID) { p_opt = &mca_l2c_fcr_opts_def; } else { p_dcb = mca_dcb_by_hdl(p_tbl->cb_idx); if (p_dcb) { p_opt = &p_dcb->p_chnl_cfg->fcr_opt; fcs = p_dcb->p_chnl_cfg->fcs; } } memset(p_cfg, 0, sizeof(tL2CAP_CFG_INFO)); p_cfg->mtu_present = true; p_cfg->mtu = p_tbl->my_mtu; p_cfg->fcr_present = true; memcpy(&p_cfg->fcr, p_opt, sizeof(tL2CAP_FCR_OPTS));
تحذير PVS-Studio: V614 CWE-824 يستخدم المؤشر "p_opt" المحتمل أنه غير مهيأ. ضع في اعتبارك التحقق من الوسيطة الفعلية الثانية لوظيفة 'memcpy'. mca_main.cc 252تعداد الضعف العام: CWE-824 : الوصول إلى مؤشر غير مهيأ.إذا كان p_tbl-> tcid! = MCA_CTRL_TCID و p_dcb == nullptr ، فسيظل المؤشر p_opt غير مهيأ.استخدام غريب لمتغير غير مهيأ struct timespec { __time_t tv_sec; long int tv_nsec; }; static inline timespec NsToTimespec(int64_t ns) { timespec t; int32_t remainder; t.tv_sec = ns / kNanosPerSecond; remainder = ns % kNanosPerSecond; if (remainder < 0) { t.tv_nsec--; remainder += kNanosPerSecond; } t.tv_nsec = remainder; return t; }
تحذير PVS-Studio: V614 CWE-457 متغير غير مهيأ يستخدم "t.tv_nsec". clock_ns.h 55تعداد الضعف العام: CWE-457 : استخدام متغير غير مهيأ.في وقت إنقاص المتغير t.tv_nsec ، يكون غير مهيأ. تمت تهيئة المتغير لاحقًا: t.tv_nsec = المتبقي ؛ .
شيء هنا مرتبك بشكل واضح.الإفراط في التعبير void bta_dm_co_ble_io_req(....) { .... *p_auth_req = bte_appl_cfg.ble_auth_req | (bte_appl_cfg.ble_auth_req & 0x04) | ((*p_auth_req) & 0x04); .... }
تحذير PVS-Studio: V578 تم الكشف عن عملية أحادية البتات. ضع في اعتبارك التحقق منها. bta_dm_co.cc 259هذا التعبير فائض. إذا قمت بإزالة التعبير الفرعي (bte_appl_cfg.ble_auth_req & 0x04) ، فلن تتغير نتيجة التعبير. ربما هناك بعض الأخطاء المطبعية هنا.التعامل مع التسرب bool RSReflectionCpp::genEncodedBitCode() { FILE *pfin = fopen(mBitCodeFilePath.c_str(), "rb"); if (pfin == nullptr) { fprintf(stderr, "Error: could not read file %s\n", mBitCodeFilePath.c_str()); return false; } unsigned char buf[16]; int read_length; mOut.indent() << "static const unsigned char __txt[] ="; mOut.startBlock(); while ((read_length = fread(buf, 1, sizeof(buf), pfin)) > 0) { mOut.indent(); for (int i = 0; i < read_length; i++) { char buf2[16]; snprintf(buf2, sizeof(buf2), "0x%02x,", buf[i]); mOut << buf2; } mOut << "\n"; } mOut.endBlock(true); mOut << "\n"; return true; }
تحذير PVS-Studio: V773 CWE-401 تم إنهاء الوظيفة بدون تحرير مقبض "pfin". من الممكن حدوث تسرب في الموارد. slang_rs_reflection_cpp.cpp 448صنف المحلل هذا الخطأ وفقًا لتعداد الضعف العام على أنه: CWE-401 : تحرير غير صحيح للذاكرة قبل إزالة المرجع الأخير ("تسرب الذاكرة"). ومع ذلك ، سيكون من الأصح هنا إصدار CWE-775 : إصدار مفقود من واصف الملف أو مقبض بعد عمر فعال. سأوجه زملائي لتصحيح هذا العيب في PVS-Studio.واصف pfin لم يفرج عنه. نسيت فقط استدعاء دالة fclose في النهاية. خطأ غير سار يمكن أن يستنفد بسرعة كامل المعروض من الواصفات المتاحة ، وبعد ذلك سيكون من المستحيل فتح ملفات جديدة.الخلاصة
كما ترى ، حتى في مشروع معروف ومختبر جيدًا مثل Android ، يجد محلل PVS-Studio بسهولة العديد من الأخطاء ونقاط الضعف المحتملة. لتلخيص نقاط الضعف (نقاط الضعف المحتملة) التي تم العثور عليها:- CWE-14: إزالة المترجم من التعليمات البرمجية لمسح المخازن المؤقتة
- CWE-20: التحقق من صحة الإدخال
- CWE-119: تقييد غير مناسب للعمليات داخل حدود مخزن الذاكرة المؤقت
- CWE-190: تجاوز عدد صحيح أو التفاف
- CWE-198: استخدام ترتيب بايت غير صحيح
- CWE-393: عودة رمز الحالة الخاطئ
- CWE-401: تحرير غير صحيح للذاكرة قبل إزالة المرجع الأخير ("تسرب الذاكرة")
- CWE-457: استخدام متغير غير مهيأ
- CWE-462: مفتاح مكرر في قائمة الترابطات
- CWE-480: استخدام عامل تشغيل غير صحيح
- CWE-484: بيان فاصل محذوف في التبديل
- CWE-561: كود ميت
- CWE-562: عودة عنوان متغير المكدس
- CWE-563: Assignment to Variable without Use
- CWE-570: Expression is Always False
- CWE-571: Expression is Always True
- CWE-476: NULL Pointer Dereference
- CWE-628: Function Call with Incorrectly Specified Arguments
- CWE-665: Improper Initialization
- CWE-670: Always-Incorrect Control Flow Implementation
- CWE-682: Incorrect Calculation
- CWE-688: Function Call With Incorrect Variable or Reference as Argument
- CWE-690: Unchecked Return Value to NULL Pointer Dereference
- CWE-691: Insufficient Control Flow Management
- CWE-758: Reliance on Undefined, Unspecified, or Implementation-Defined Behavior
- CWE-762: Mismatched Memory Management Routines
- CWE-775: Missing Release of File Descriptor or Handle after Effective Lifetime
- CWE-783: Operator Precedence Logic Error
- CWE-824: Access of Uninitialized Pointer
- CWE-834: Excessive Iteration
في المجموع ، وصفت 490 ثغرات محتملة في المقالة. في الواقع ، المحلل قادر على تحديد المزيد منهم ، ولكن ، كما كتبت سابقًا ، لم أجد القوة لدراسة التقرير بعناية أكبر.يبلغ حجم قاعدة الكود المختبرة حوالي 2،168،000 سطر من الكود في لغة C و C ++. 14.4٪ من هذه التعليقات. في المجموع ، نحصل على 1،855،000 سطر من الكود النظيف.وبالتالي ، لدينا 490 CWEs لـ 1،855،000 سطر من الكود.اتضح أن محلل PVS-Studio قادر على اكتشاف أكثر من ضعف (ضعف محتمل) لكل 4000 سطر من التعليمات البرمجية في مشروع Android. نتيجة جيدة لمحلل الكود ، أنا سعيد.شكرا لكم على اهتمامكم!
أتمنى للجميع رمزًا غير مشفر وأقترح القيام بما يلي:- قم بتنزيل PVS-Studio وتحقق من مسودة العمل.
- فقط اسأل: لا تقم بتشغيل المحلل في الاختبارات الاصطناعية: لماذا لا أحب الاختبارات الاصطناعية .
- اشترك لتواكب نشر مقالاتنا الجديدة: twitter ، RSS ، vk.com .

إذا كنت تريد مشاركة هذه المقالة مع جمهور ناطق باللغة الإنجليزية ، فيرجى استخدام رابط الترجمة: Andrey Karpov. لقد تحققنا من رموز مصدر Android بواسطة PVS-Studio أو لا يوجد شيء مثالي