هذا هو الجزء الثاني من سلسلة مقالات حماية بلا خوف. في البداية تحدثنا عن أمن الذاكرةالتطبيقات الحديثة متعددة الخيوط: بدلاً من تنفيذ المهام بالتسلسل ، يستخدم البرنامج مؤشرات الترابط لأداء عدة مهام في وقت واحد. نلاحظ جميعًا
العمل والتزامن في وقت واحد يوميًا:
- يتم تقديم المواقع من قبل العديد من المستخدمين في نفس الوقت.
- تقوم واجهة المستخدم بعمل خلفية لا يزعج المستخدم (تخيل أنه في كل مرة تكتب فيها شخصية ، يتجمد التطبيق للتحقق من الهجاء).
- يمكن لجهاز الكمبيوتر تشغيل تطبيقات متعددة في نفس الوقت.
تعمل التدفقات المتوازية على تسريع العمل ، ولكن تقدم مجموعة من مشكلات المزامنة ، وهي حالة توقف تام وظروف السباق. من وجهة نظر الأمان ، لماذا نهتم بسلامة الخيط؟ لأن أمان الذاكرة والخيوط لديه مشكلة واحدة ونفس المشكلة: الاستخدام غير المناسب للموارد. الهجمات هنا لها نفس تأثيرات هجمات الذاكرة ، بما في ذلك تصعيد الامتيازات وتنفيذ التعليمات البرمجية التعسفية (ACE) وتجاوز عمليات التحقق من الأمان.
ترتبط أخطاء التزامن ، مثل أخطاء التنفيذ ، ارتباطًا وثيقًا بصحة البرنامج. على الرغم من أن نقاط الضعف في الذاكرة تكون دائمًا خطيرة للغاية ، فإن أخطاء التنفيذ / المنطق لا تشير دائمًا إلى وجود مشكلة أمنية إذا لم تحدث في جزء التعليمات البرمجية المتعلقة بالامتثال لعقود الأمان (على سبيل المثال ، إذن لتجاوز فحص الأمان). ولكن الخلل التزامن لها خصوصية. إذا ظهرت مشاكل أمنية بسبب الأخطاء المنطقية غالبًا بجوار الرمز المقابل ، فغالبًا ما تحدث أخطاء التزامن
في وظائف أخرى ، وليس في الوظيفة التي ارتكب فيها الخطأ مباشرةً ، مما يجعل من الصعب تتبعها والقضاء عليها. هناك صعوبة أخرى تتمثل في تداخل معين بين معالجة الذاكرة غير الصحيحة وأخطاء التزامن ، والتي نراها في سباقات البيانات.
طورت لغات البرمجة استراتيجيات التزامن المختلفة لمساعدة المطورين على إدارة مشكلات الأداء والأمان للتطبيقات متعددة الخيوط.
مشاكل التزامن
من المقبول عمومًا أن البرمجة الموازية أصعب من المعتاد: فدماغنا يتكيف بشكل أفضل مع المنطق المتسلسل. يمكن أن يكون للشفرة المتوازية تفاعلات غير متوقعة وغير مرغوب فيها بين مؤشرات الترابط ، بما في ذلك حالة توقف تام وخلاف وسباقات البيانات.
تحدث حالة
توقف تام عندما تتوقع عدة مؤشرات ترابط أداء بعض الإجراءات لمواصلة العمل. على الرغم من أن هذا السلوك غير المرغوب فيه قد يتسبب في هجوم رفض الخدمة ، إلا أنه لن يتسبب في حدوث ثغرات أمنية مثل ACE.
شرط السباق هو موقف يمكن أن يؤثر فيه زمن أو ترتيب المهام على صحة البرنامج. يحدث سباق البيانات عندما تحاول عدة تدفقات الوصول في نفس الوقت إلى موقع الذاكرة نفسه مع محاولة كتابة واحدة على الأقل. يحدث أن حالة السباق وسباق البيانات
تحدث بشكل مستقل عن بعضها البعض. لكن
سباقات البيانات دائما خطيرة .
الآثار المحتملة لأخطاء التزامن
- طريق مسدود
- فقدان المعلومات: مؤشر ترابط آخر الكتابة فوق المعلومات
- فقدان النزاهة: المعلومات من عدة تدفقات متشابكة
- فقدان الجدوى: مشكلات في الأداء بسبب عدم إمكانية الوصول إلى الموارد المشتركة
يُطلق على أكثر أنواع هجمات التزامن شهرةً اسم
TOCTOU (وقت الاختيار حتى وقت الاستخدام): في الواقع ، تكون حالة السباق بين فحص الشروط (على سبيل المثال ، بيانات اعتماد الأمان) واستخدام النتائج. هجوم TOCTOU يؤدي إلى فقدان النزاهة.
تعتبر الأقفال المتبادلة وفقدان القدرة على البقاء مشكلات في الأداء ، وليست مشاكل أمنية ، في حين أن فقدان المعلومات وفقدان النزاهة من المحتمل أن تكون مرتبطة بالأمن. تبحث
مقالة "الأمن بالون الأحمر" في بعض عمليات الاستغلال المحتملة. مثال على ذلك هو تلف المؤشر متبوعًا بتصعيد الامتيازات أو تنفيذ التعليمات البرمجية عن بُعد. في الاستغلال ، تقوم دالة تقوم بتحميل المكتبة المشتركة ELF (تنسيق قابل للتنفيذ والرابط) ببدء تشغيل إشارة على المكالمة الأولى فقط بشكل صحيح ، ثم تحد بشكل غير صحيح من عدد مؤشرات الترابط ، مما يؤدي إلى تلف ذاكرة kernel. هذا الهجوم هو مثال على فقدان المعلومات.
الجزء الأكثر صعوبة في البرمجة المتزامنة هو الاختبار والتصحيح ، لأنه من الصعب إعادة إنتاج أخطاء التزامن. توقيت الأحداث ، وقرارات نظام التشغيل ، وحركة مرور الشبكة وعوامل أخرى ... كل هذا يغير سلوك البرنامج في كل بداية.
في بعض الأحيان يكون من الأسهل حقًا إزالة البرنامج بالكامل بدلاً من البحث عن خطأ. Heisenbugsلا يتغير السلوك فقط في كل مرة يبدأ فيها ، ولكن حتى إدخال مشغلات الإخراج أو تصحيح الأخطاء يمكن أن يغير السلوك ، مما ينتج عنه "أخطاء Heisenberg" (أخطاء غير محددة ، يصعب إعادة إنتاجها نموذجية من البرمجة المتوازية) التي تنشأ وتختفي بشكل غامض.
البرمجة الموازية صعبة. من الصعب التنبؤ بكيفية تفاعل الكود الموازي مع الكود المتوازي الآخر. عندما تظهر الأخطاء ، يصعب العثور عليها وتصحيحها. بدلاً من الاعتماد على المختبرين ، دعونا نلقي نظرة على طرق تطوير البرامج واستخدام اللغات التي تجعل كتابة التعليمات البرمجية المتوازية أسهل.
أولاً ، نضع مفهوم "أمان الخيط":
"يعتبر نوع البيانات أو الأسلوب الثابت آمنًا في سلسلة الرسائل إذا كان يتصرف بشكل صحيح عند استدعائه من عدة سلاسل ، بغض النظر عن كيفية تنفيذ هذه المواضيع ، ولا يتطلب تنسيقًا إضافيًا من رمز الاتصال." معهد ماساتشوستس للتكنولوجيا
كيف تعمل لغات البرمجة بالتوازي
في اللغات التي لا يوجد بها أمان ثابت للخيط ، يتعين على المبرمجين مراقبة الذاكرة التي يتم مشاركتها مع خيط آخر بشكل مستمر ويمكن أن يتغيروا في أي وقت. في البرمجة المتسلسلة ، يتم تعليمنا تجنب المتغيرات العامة إذا قام جزء آخر من التعليمات البرمجية بتغييرها بهدوء. من المستحيل مطالبة المبرمجين بضمان التغيير الآمن في البيانات المشتركة ، وكذلك الإدارة اليدوية للذاكرة.
"اليقظة المستمرة!"عادة ، تقتصر لغات البرمجة على طريقتين:
- الحد من قابلية التغيير أو تقييد الوصول المشترك
- أمان الخيط اليدوي (مثل الأقفال ، الإشارات)
تضع اللغات ذات قيود سلاسل الرسائل إما حدًا واحدًا لمؤشر الترابط القابل للتغيير ، أو تتطلب أن تكون جميع المتغيرات الشائعة غير قابلة للتغيير. يعالج كلا النهجين المشكلة الأساسية لسباق البيانات - البيانات المشتركة القابلة للتعديل بطريقة غير صحيحة - لكن القيود شديدة للغاية. لحل المشكلة ، جعلت اللغات بدايات التزامن ذات المستوى المنخفض ، مثل المزامنة. يمكن استخدامها لإنشاء بنية بيانات آمنة لمؤشر الترابط.
بيثون وقفل عالمي بواسطة مترجم
يحتوي تطبيق المرجع في Python و Cpython على كائن مزامنة غريب يسمى Global Interpreter Lock (GIL) ، والذي يحظر كافة مؤشرات الترابط الأخرى عندما يصل مؤشر ترابط واحد إلى كائن. بيثون متعدد مؤشرات الترابط سيء السمعة بسبب
عدم كفاءته بسبب الكمون جيل. لذلك ، تعمل معظم برامج Python المتزامنة في عدة عمليات بحيث يكون لكل منها GIL الخاص بها.
استثناءات وقت التشغيل و Java
يدعم
Java البرمجة المتزامنة من خلال نموذج ذاكرة مشترك. كل مؤشر ترابط له مسار التنفيذ الخاص به ، ولكن يمكنه الوصول إلى أي كائن في البرنامج: يجب أن يقوم المبرمج بمزامنة الوصول بين مؤشرات الترابط باستخدام بدائل Java المدمجة.
على الرغم من أن Java تحتوي على كتل إنشاء لإنشاء برامج آمنة لمؤشر الترابط ، إلا أن
أمان مؤشر الترابط غير مضمون من قبل المترجم (على عكس أمان الذاكرة). في حالة حدوث وصول غير متزامن للذاكرة (على سبيل المثال ، سباق البيانات) ، فإن Java ستلقي استثناءًا في وقت التشغيل ، ولكن يجب على المبرمجين استخدام بدائل التزامن بشكل صحيح.
C ++ وعقل مبرمج
في حين تتجنب Python ظروف السباق باستخدام GIL و Java يلقي استثناءات في وقت التشغيل ، تتوقع C ++ من المبرمج مزامنة الوصول إلى الذاكرة يدويًا. قبل الإصدار C ++ 11 ،
لم تتضمن المكتبة القياسية بدائل
التزامن .
توفر معظم اللغات أدوات لكتابة التعليمات البرمجية الآمن لمؤشر الترابط ، وهناك طرق خاصة لاكتشاف سباق البيانات وحالة السباق ؛ لكنه لا يعطي أي ضمانات لسلامة مؤشر الترابط ولا يحمي من سباق البيانات.
كيفية حل مشكلة الصدأ؟
يتخذ Rust نهجًا متعدد الأوجه للتخلص من ظروف السباق باستخدام قواعد الحيازة وأنواع آمنة للحماية تمامًا من ظروف السباق في وقت الترجمة.
في
المقالة الأولى ، قدمنا مفهوم الملكية ، وهذا هو واحد من المفاهيم الأساسية للصدأ. كل متغير له مالك فريد ، ويمكن نقل الملكية أو اقتراضها. إذا أراد خيط آخر تغيير المورد ، فنحن ننقل الملكية عن طريق نقل المتغير إلى خيط جديد.
نقل يستثني استثناء: يمكن أن مؤشرات ترابط متعددة الكتابة إلى نفس الذاكرة ، ولكن أبدا في نفس الوقت. نظرًا لأن المالك دائمًا ما يكون وحيدًا ، فما الذي يحدث إذا اقترض مؤشر ترابط آخر متغيرًا؟
في Rust ، لديك إما استعارة قابلة للتغيير أو عدة استعارات ثابتة. لا يمكن تقديم قروض قابلة للتغيير وغير قابلة للتغيير (أو عدة قروض قابلة للتغيير). في أمان الذاكرة ، من المهم أن يتم تحرير الموارد بشكل صحيح ، ومن المهم في أمان مؤشر الترابط واحد فقط تغيير متغير في أي وقت معين. بالإضافة إلى ذلك ، في مثل هذه الحالة ، لن تشير التدفقات الأخرى إلى الاقتراض المتقادم: إما أن التسجيل أو المشاركة أمر ممكن ، لكن ليس كلاهما.
تم تصميم مفهوم الملكية لمعالجة نقاط الضعف في الذاكرة. اتضح أنه يمنع أيضا سباق البيانات.
على الرغم من أن العديد من اللغات لديها أساليب أمان للذاكرة (مثل حساب الارتباط وجمع البيانات المهملة) ، فإنها تعتمد عادة على المزامنة اليدوية أو الحظر على المشاركة المتزامنة لمنع سباق البيانات. يتناول نهج Rust نوعي الأمن ، ويحاول حل المشكلة الرئيسية المتمثلة في تحديد الاستخدام المقبول للموارد وضمان هذه الصلاحية في وقت الترجمة.
لكن انتظر! هذا ليس كل شيء!
تمنع قواعد الملكية مؤشرات الترابط المتعددة من كتابة البيانات إلى نفس موقع الذاكرة وتحظر تبادل البيانات في وقت واحد بين سلاسل الرسائل وقابلية التحويل ، ولكن هذا لا يوفر بالضرورة هياكل بيانات آمنة لمؤشر الترابط. كل بنية بيانات في Rust هي إما خيط آمن أو لا. يتم تمرير هذا إلى المحول البرمجي باستخدام نظام كتابة.
"البرنامج المكتوب بشكل جيد لا يمكن أن يرتكب خطأ". - روبن ميلنر ، 1978
في لغات البرمجة ، تصف أنظمة الكتابة السلوك المقبول. بمعنى آخر ، فإن البرنامج المكتوب جيدًا محدد جيدًا. طالما أن أنواعنا معبرة بما يكفي لالتقاط المعنى المقصود ، فسوف يتصرف برنامج مكتوب بشكل جيد كما هو مقصود.
الصدأ هي لغة آمنة ، وهنا يقوم المترجم بالتحقق من اتساق جميع الأنواع. على سبيل المثال ، لا يتم ترجمة التعليمات البرمجية التالية:
let mut x = "I am a string"; x = 6;
error[E0308]: mismatched types --> src/main.rs:6:5 | 6 | x = 6;
جميع المتغيرات في الصدأ غالباً ما تكون ضمنية. يمكننا أيضًا تحديد أنواع جديدة ووصف إمكانيات كل نوع باستخدام
نظام السمات . توفر الصفات تجريدًا للواجهة. السمتان المهمتان المضمنتان هما
Send
and
Sync
، والتي يتم توفيرها افتراضيًا بواسطة برنامج التحويل البرمجي لكل نوع:
Send
يشير إلى أنه يمكن نقل الهيكل بأمان بين سلاسل الرسائل (مطلوب لنقل الملكية)
- تشير
Sync
إلى أن مؤشرات الترابط يمكنها استخدام الهيكل بأمان.
المثال أدناه هو نسخة مبسطة من
التعليمات البرمجية من المكتبة القياسية التي تفرز مؤشرات الترابط:
fn spawn<Closure: Fn() + Send>(closure: Closure){ ... } let x = std::rc::Rc::new(6); spawn(|| { x; });
تأخذ وظيفة
spawn
وسيطة واحدة ،
closure
وتتطلب نوعًا للأخير يطبق سمات
Fn
و
Fn
. عند محاولة إنشاء دفق وتمرير قيمة
closure
مع المتغير
x
يلقي المترجم خطأ:
error [E0277]: `std :: rc :: Rc <i32>` لا يمكن إرسالها بين سلاسل الرسائل بأمان
-> src / main.rs: 8: 1
|
8 | تفرخ (move || {x؛})؛
| لا يمكن إرسال ^^^^^ `std :: rc :: Rc <i32>` بين سلاسل الرسائل بأمان
|
= مساعدة: ضمن `[closure@src/main.rs: 8: 7: 8:21 x: std :: rc :: Rc <i32>]` ، لم يتم تنفيذ السمات `std :: marker :: Send` لـ `std :: rc :: Rc <i32>`
= ملاحظة: مطلوب لأنه يظهر داخل النوع `[closure@src/main.rs: 8: 7: 8:21 x: std :: rc :: Rc <i32>]
ملاحظة: مطلوب من قبل "تفرخ"
تسمح
سمات Send
and
Sync
لنظام نوع Rust بفهم البيانات التي يمكن مشاركتها. بتضمين هذه المعلومات في نظام الكتابة ، تصبح سلامة مؤشر الترابط جزءًا من أمان النوع. بدلاً من الوثائق ،
يتم تطبيق أمان مؤشر الترابط بواسطة قانون المحول البرمجي .
يرى المبرمجون بوضوح الكائنات الشائعة بين مؤشرات الترابط ، ويضمن المترجم موثوقية هذا التثبيت.
على الرغم من توفر أدوات البرمجة المتوازية بعدة لغات ، إلا أن منع شروط السباق ليس بالأمر السهل. إذا طلبت من المبرمجين أن يتناوبوا بالتعليمات المعقدة والتفاعل بين سلاسل الرسائل ، فإن الأخطاء لا مفر منها. على الرغم من أن خروقات أمان الخيوط والذاكرة تؤدي إلى عواقب مماثلة ، فإن عمليات حماية الذاكرة التقليدية ، مثل عد الروابط وجمع البيانات المهملة ، لا تمنع ظروف السباق. بالإضافة إلى الضمان الثابت لأمان الذاكرة ، فإن نموذج ملكية Rust يمنع أيضًا تغييرات البيانات غير الآمنة والمشاركة غير الصحيحة للكائنات بين مؤشرات الترابط ، بينما يوفر نظام الكتابة أمان مؤشر الترابط في وقت الترجمة.