ماذا يعني غير آمنة في الصدأ؟

مرحبا يا هبر! أقدم لكم ترجمة المقال "ما هو الصدأ غير الآمن؟" المؤلف نورا أكواد.


لقد رأيت الكثير من حالات سوء الفهم فيما يتعلق بما تعنيه الكلمة الرئيسية غير الآمنة لفائدة وصحة لغة Rust وترويجها باعتبارها "لغة برمجة نظام آمن". الحقيقة أكثر تعقيدًا مما يمكن وصفه في تغريدة قصيرة ، لسوء الحظ. هذه هي الطريقة التي أراها.


بشكل عام ، لا تقوم الكلمة الأساسية غير الآمنة بإيقاف تشغيل نظام الكتابة الذي يحافظ على رمز Rust الصحيح . إنه يجعل من الممكن فقط استخدام بعض "القوى العظمى" ، مثل مؤشرات إلغاء التسجيل. يستخدم غير آمن لتنفيذ التجريدات الآمنة بناءً على عالم غير آمن بشكل أساسي بحيث يمكن لمعظم كود Rust استخدام هذه التجريدات وتجنب الوصول غير الآمن للذاكرة.


ضمان السلامة


الصدأ يضمن الأمن كأحد مبادئه الأساسية. يمكننا القول أن هذا هو معنى وجود اللغة. ومع ذلك ، فإنه لا يوفر الأمان بالمعنى التقليدي ، أثناء تنفيذ البرنامج واستخدام أداة تجميع مجمعي البيانات المهملة. بدلاً من ذلك ، يستخدم Rust نظام كتابة متقدم للغاية لتتبع متى وما هي القيم التي يمكن الوصول إليها. يقوم المترجم بتحليل كل برنامج Rust بشكل ثابت للتأكد من أنه دائمًا في الحالة الصحيحة.


بيثون الأمن


لنأخذ بيثون كمثال. رمز Python النقي لا يمكن أن يفسد الذاكرة. الوصول إلى عناصر القائمة لديه الشيكات لتجاوز الحدود. يتم حساب الروابط التي يتم إرجاعها بواسطة الوظائف لتجنب ظهور روابط متدلية ؛ لا توجد وسيلة للقيام الحسابي التعسفي مع المؤشرات.


هذا له نتيجتان. أولاً ، يجب أن تكون أنواع كثيرة "مميزة". على سبيل المثال ، لا يمكن تنفيذ قائمة أو قاموس فعال في Python النقي. بدلاً من ذلك ، لدى مترجم CPython تطبيقه الداخلي. ثانياً ، يتطلب الوصول إلى الوظائف الخارجية (الوظائف غير المنفذة في بيثون) ، والتي تسمى واجهة الوظيفة الخارجية ، استخدام وحدة نمطية خاصة وينتهك الضمانات الأمنية للغة.


وهذا يعني إلى حد ما أن كل شيء مكتوب في بيثون لا يضمن الوصول الآمن إلى الذاكرة.


الأمن في الصدأ


يوفر Rust أيضًا الأمان ، ولكن بدلاً من تطبيق الهياكل غير الآمنة في C ، فإنه يوفر خدعة: الكلمة الأساسية غير الآمنة. هذا يعني أن هياكل البيانات الأساسية في Rust ، مثل Vec و VecDeque و BTreeMap و String ، يتم تنفيذها في Rust.


قد تسأل: "لكن ، إذا كانت Rust تقدم خدعة ضد ضمانات أمان الكود ، وتم تنفيذ المكتبة القياسية باستخدام هذه الخدعة ، ألا يعتبر كل شيء في Rust غير آمن؟"


باختصار ، عزيزي القارئ ، نعم ، بالضبط كما كان الحال في بيثون. دعونا ننظر في الأمر بمزيد من التفاصيل.


ما هو ممنوع في الصدأ الآمن؟


الأمن في الصدأ محدد جيدًا: نفكر فيه كثيرًا. باختصار ، لا تستطيع برامج Rust الآمنة:


  • إلغاء مرجع مؤشر يشير إلى نوع مختلف عن المترجم يعرف . هذا يعني أنه لا توجد مؤشرات للإلغاء (لأنها لا تشير إلى أي مكان) ، ولا توجد أخطاء في الخروج عن الحدود و / أو أخطاء تجزئة (أخطاء تجزئة) ، ولا توجد فيضان مؤقت. ولكن هذا يعني أيضًا أنه لا توجد استخدامات بعد تحرير الذاكرة أو إعادة تحرير الذاكرة (لأن تحرير الذاكرة يعتبر إلغاء مرجع المؤشر) وليس التورية مخصصة للكتابة .
  • لديك إشارات متعددة قابلة للتغيير إلى كائن أو إشارات قابلة للتغيير وغير قابلة للتغيير إلى كائن في وقت واحد . أي إذا كانت لديك إشارة قابلة للتغيير إلى كائن ، يمكنك الحصول عليه فقط ، وإذا كان لديك مرجع ثابت للكائن ، فلن يتغير حتى تحتفظ به. هذا يعني أنه لا يمكنك فرض سباق البيانات في Safe Rust ، وهو ضمان لا يمكن لمعظم اللغات الآمنة توفيره.

يقوم Rust بتشفير هذه المعلومات في نظام كتابة أو باستخدام أنواع بيانات جبرية ، مثل الخيار للإشارة إلى وجود / عدم وجود قيمة والنتيجة <T ، E> للإشارة إلى الخطأ / النجاح ، أو المراجع وعمرها ، على سبيل المثال ، & T vs & mut T للإشارة رابط عام (غير قابل للتغيير) ورابط خاص (قابل للتغيير) و & amp؛ & t & t & gt؛ لتمييز الروابط الصحيحة في سياقات مختلفة (عادةً ما يتم حذف هذا لأن المترجم ذكي بما فيه الكفاية لمعرفة ذلك بنفسك) .


أمثلة


على سبيل المثال ، لن يتم تجميع التعليمة البرمجية التالية نظرًا لأنه يحتوي على رابط معلق. بشكل أكثر تحديدًا ، my_struct لا يعيش بما فيه الكفاية . بمعنى آخر ، ستُرجع الدالة ارتباطًا إلى شيء لم يعد موجودًا ، وبالتالي لا يمكن للمترجم (وفي الواقع ، لا يعرف كيف) تجميع هذا.


fn dangling_reference(v: &u64) -> &MyStruct { //     MyStruct   ,  v,   . let my_struct = MyStruct { value: v }; //      my_struct. return &my_struct; //  - my_struct  (  ). } 

يعمل هذا الرمز بنفس الطريقة ، لكنه يحاول حل هذه المشكلة عن طريق وضع القيمة على الكومة (Box هو اسم المؤشر الذكي الأساسي في Rust).


 fn dangling_heap_reference(v: &u64) -> &Box<MyStruct> { let my_struct = MyStruct { value: v }; //    Box         . let my_box = Box::new(my_struct); //      my_box. return &my_box; // my_box   .   "" my_struct       - , //    - MyStruct  . } 

يتم إرجاع الرمز الصحيح بواسطة Box نفسه بدلاً من الإشارة إليه. هذا يشفر نقل الملكية - مسؤولية تحرير الذاكرة - في توقيع الوظيفة. عند النظر إلى التوقيع ، يصبح من الواضح أن رمز الاتصال هو المسؤول عن ما يحدث مع Box ، والواقع أن المترجم يعالجها تلقائيًا.

 fn no_dangling_reference(v: &u64) -> Box<MyStruct> { let my_struct = MyStruct { value: v }; let my_box = Box::new(my_struct); //    my_box  . return my_box; //    .         , //    ;       //  Box<MyStruct>       ,      . } 

لا تحظر بعض الأشياء السيئة في الصدأ الآمن. على سبيل المثال ، يُسمح به من وجهة نظر المترجم:
  • تسبب الجمود في البرنامج
  • تسرب كمية كبيرة بشكل تعسفي من الذاكرة
  • فشل في إغلاق مؤشرات الملفات أو اتصالات قاعدة البيانات أو أغطية عمود الصواريخ


تتمثل قوة النظام البيئي Rust في أن العديد من المشروعات تختار استخدام نظام الكتابة لضمان دقة الكود قدر الإمكان ، لكن المترجم لا يتطلب مثل هذا الإكراه ، إلا في الحالات التي يتم فيها توفير وصول آمن للذاكرة.

ما هو مسموح في الصدأ غير الآمن؟


رمز الصدأ غير الآمن هو رمز الصدأ مع الكلمة الأساسية غير الآمنة. غير آمنة يمكن تطبيقها على وظيفة أو كتلة من التعليمات البرمجية. عند تطبيقها على دالة ، فهذا يعني أن "هذه الوظيفة تتطلب أن توفر الشفرة التي تم استدعاؤها يدويًا المتغير الذي يتم توفيره عادةً بواسطة برنامج التحويل البرمجي." عند تطبيقه على مجموعة من التعليمات البرمجية ، فهذا يعني "توفر هذه المجموعة من التعليمات البرمجية يدويًا المتغير الضروري لمنع الوصول غير الآمن إلى الذاكرة ، وبالتالي يُسمح لها بعمل أشياء غير آمنة."


بمعنى آخر ، تعني كلمة "غير آمنة للوظيفة" أنك "بحاجة إلى التحقق من كل شيء" ، وعلى كتلة التعليمات البرمجية - "لقد راجعت كل شيء بالفعل".


كما هو مذكور في The Rust Programming Language ، فإن الكود الموجود في كتلة تحمل كلمة رئيسية غير آمنة:


  • Dereference مؤشر. هذا هو "القوة العظمى" الرئيسية التي تسمح لك بتنفيذ قوائم مرتبطة مضاعفة ، hashmap ، وغيرها من هياكل البيانات الأساسية.
  • استدعاء وظيفة غير آمنة أو الطريقة. المزيد عن هذا أدناه.
  • الوصول أو تعديل متغير ثابت قابلة للتغيير. لا يمكن التحقق بشكل ثابت من المتغيرات الثابتة التي لا يتم التحكم في نطاقها ، وبالتالي فإن استخدامها غير آمن.
  • تطبيق سمة غير آمنة. يتم استخدام السمات غير الآمنة لتحديد ما إذا كانت أنواع معينة تضمن بعض المتغيرات. على سبيل المثال ، يحدد "إرسال" و "مزامنة" ما إذا كان يمكن إرسال نوع ما بين حدود سلاسل الرسائل أو يمكن استخدامه بواسطة مؤشرات ترابط متعددة في نفس الوقت.

تذكر تلك المؤشرات المعلقة أعلاه؟ أضف الكلمة غير آمنة ، وسوف يقسم المترجم ضعف هذا لأنه لا يحب استخدام غير آمن حيث لا تكون هناك حاجة إليه.


بدلاً من ذلك ، يتم استخدام الكلمة الأساسية غير الآمنة لتنفيذ عمليات تجريد آمنة تستند إلى عمليات تعسفية للمؤشر. على سبيل المثال ، يتم تطبيق نوع Vec باستخدام غير آمن ، ولكنه آمن لاستخدامه ، لأنه يتحقق من محاولات الوصول إلى العناصر ولا يسمح بالفيضانات. على الرغم من أنه يوفر عمليات مثل set_len ، والتي قد تتسبب في الوصول غير الآمن للذاكرة ، إلا أنها وضعت على أنها غير آمنة.


على سبيل المثال ، يمكننا أن نفعل الشيء نفسه كما في المثال no_dangling_reference ، ولكن باستخدام غير معقول للأمن:


 fn manual_heap_reference(v: u64) -> *mut MyStruct { let my_struct = MyStruct { value: v }; let my_box = Box::new(my_struct); //  Box    . let struct_pointer = Box::into_raw(my_box); return struct_pointer; //   ;     . // MyStruct     . } 

لاحظ عدم وجود كلمة غير آمنة. إنشاء مؤشرات آمنة تماما. كما هو مكتوب ، هذا خطر حدوث تسرب للذاكرة ، لكن لا شيء أكثر من ذلك ، وتسريبات الذاكرة آمنة. استدعاء هذه الوظيفة هي أيضا آمنة. غير آمن فقط عند محاولة شيء ما dereference مؤشر. كمكافأة إضافية ، ستقوم عملية إلغاء التسجيل تلقائيًا بتحرير الذاكرة المخصصة.


 fn main() { let my_pointer = manual_heap_reference(1337); let my_boxed_struct = unsafe { Box::from_raw(my_pointer) }; //  "Value: 1337" println!("Value: {}", my_boxed_struct.value); // my_boxed_struct    .       ,  //    - MyStruct } 

بعد التحسين ، يعادل هذا الرمز مجرد إرجاع مربع. Box عبارة عن تجريد آمن يعتمد على المؤشر لأنه يمنع توزيع المؤشرات في كل مكان. على سبيل المثال ، سيؤدي الإصدار التالي من main إلى ذاكرة خالية مزدوجة (خالية مزدوجة).


 fn main() { let my_pointer = manual_heap_reference(1337); let my_boxed_struct_1 = unsafe { Box::from_raw(my_pointer) }; // DOUBLE FREE BUG! let my_boxed_struct_2 = unsafe { Box::from_raw(my_pointer) }; //  "Value: 1337" . println!("Value: {}", my_boxed_struct_1.value); println!("Value: {}", my_boxed_struct_2.value); // my_boxed_struct_2    .     ,  //    - MyStruct. //  my_boxed_struct_1    .      , //      - MyStruct.  double-free bug. } 

إذن ما هو التجريد الآمن؟


التجريد الآمن عبارة عن تجريد يستخدم نظام كتابة لتوفير API لا يمكن استخدامه لانتهاك ضمانات الأمان المذكورة أعلاه. المربع أكثر أمانًا * mut T ، لأنه لا يمكن أن يؤدي إلى إلغاء تخصيص الذاكرة مرتين ، كما هو موضح أعلاه.


مثال آخر هو نوع Rc في Rust. هذا هو مؤشر حساب مرجع - مرجع غير قابل للتغيير إلى البيانات على كومة الذاكرة المؤقتة. نظرًا لأنه يسمح بالوصول المتزامن المتعدد إلى منطقة واحدة من الذاكرة ، يجب أن يمنع التغيير من أجل اعتباره آمنًا.


بالإضافة إلى ذلك ، ليس مؤشر الترابط آمن. إذا كنت بحاجة إلى سلامة مؤشر الترابط ، فسيتعين عليك استخدام نوع القوس (حساب المرجع الذري) ، والذي يحتوي على عقوبة أداء بسبب استخدام القيم الذرية لحساب الارتباط ومنع سباقات البيانات المحتملة في بيئات متعددة الخيوط.


لن يسمح لك المحول البرمجي باستخدام Rc حيث يجب عليك استخدام Arc ، لأن منشئي مثل Rc لم يضعوا علامة على أنه موضوع آمن. إذا فعلوا ذلك ، فسيكون ذلك غير معقول: وعد زائف بالأمن.


متى تكون الصدأ غير آمن؟


يعد الصدأ غير الآمن ضروريًا دائمًا عندما يكون من الضروري إجراء عملية تنتهك إحدى هاتين القاعدتين الموضحتين أعلاه. على سبيل المثال ، في قائمة مزدوجة الارتباط ، يحرمها عدم وجود روابط قابلة للتغيير إلى نفس البيانات (للعنصر التالي والعنصر السابق) من الفائدة. في حالة عدم الأمان ، يمكن لمنفذ القائمة ذي الارتباط المزدوج كتابة التعليمات البرمجية باستخدام مؤشرات * mut Node ثم تغليفها في تجريد آمن.

مثال آخر هو العمل مع الأنظمة المدمجة. غالبًا ما تستخدم المتحكمات الدقيقة مجموعة من السجلات التي يتم تحديد قيمها حسب الحالة المادية للجهاز. لا يمكن للعالم أن يتوقف عند أخذ & mut u8 من هذا السجل ، وبالتالي فإن الأمر غير آمن للعمل مع صناديق دعم الجهاز. عادةً ما تقوم هذه الصناديق بتغليف الحالة في أغلفة شفافة وآمنة تقوم بنسخ البيانات كلما أمكن ذلك ، أو تستخدم تقنيات أخرى توفر ضمانات برنامج التحويل البرمجي.


في بعض الأحيان يكون من الضروري إجراء عملية يمكن أن تؤدي إلى القراءة والكتابة في وقت واحد ، أو الوصول غير الآمن إلى الذاكرة ، وهذا هو المكان الذي تكون هناك حاجة إليه غير آمنة. ولكن ما دامت هناك فرصة للتأكد من الحفاظ على الثوابت الآمنة قبل أن يلمس المستخدم شيئًا (أي أنه غير آمن) ، فكل شيء على ما يرام.


على أكتاف هذه المسؤولية؟


لقد توصلنا إلى بيان تم الإدلاء به مسبقًا - نعم ، تستند فائدة كود Rust إلى رمز غير آمن. على الرغم من حقيقة أن هذا يتم بطريقة مختلفة قليلاً عن التنفيذ غير الآمن لهياكل البيانات الأساسية في بيثون ، فإن تطبيق Vec و Hashmap ، إلخ ، يجب أن يستخدم التلاعب بالمؤشر إلى حد ما.


نقول إن Rust غير آمن ، مع الافتراض الأساسي بأن التعليمات البرمجية غير الآمنة التي نستخدمها من خلال تبعياتنا على المكتبة القياسية أو رمز المكتبات الأخرى مكتوبة ومغلفة بشكل صحيح. الميزة الأساسية لصدأ هي أن الكود غير الآمن مدفوع إلى كتل غير آمنة يجب فحصها بعناية من قبل مؤلفيها.


في Python ، يقع عبء التحقق من أمان التلاعب بالذاكرة فقط على مطوري المترجمين الفوريين ومستخدمي واجهات الوظائف الخارجية. في C ، يقع هذا العبء على كل مبرمج.


في Rust ، تقع على عاتق مستخدمي الكلمة الأساسية غير الآمنة. هذا واضح ، حيث يجب الحفاظ على المتغيرات يدويًا داخل هذا الرمز ، وبالتالي من الضروري السعي للحصول على أقل كمية من هذه الشفرة في المكتبة أو رمز التطبيق. يتم الكشف عن انعدام الأمن ، وتسليط الضوء عليها والإشارة إليها. لذلك ، في حالة حدوث أخطاء في رمز Rust الخاص بك ، فستجد إما خطأ في المحول البرمجي أو خطأ في عدة أسطر من التعليمات البرمجية غير الآمنة.


هذا ليس نظامًا مثاليًا ، ولكن إذا كنت بحاجة إلى السرعة والأمان ومضاعفة مؤشرات الترابط في نفس الوقت ، فهذا هو الخيار الوحيد.

Source: https://habr.com/ru/post/ar460295/


All Articles