Halo, Habr! Saya mempersembahkan kepada Anda terjemahan artikel "Apa itu Rust tidak aman?" penulis Kode Nora.
Saya telah melihat banyak kesalahpahaman tentang arti kata kunci tidak aman untuk kegunaan dan kebenaran bahasa Rust dan promosinya sebagai "bahasa pemrograman sistem yang aman". Sayangnya, kebenarannya jauh lebih rumit daripada yang bisa dijelaskan dalam tweet pendek. Ini adalah bagaimana saya melihatnya.
Secara umum, kata kunci tidak aman tidak mematikan sistem tipe yang membuat kode Rust tetap benar . Itu hanya memungkinkan untuk menggunakan beberapa "kekuatan super," seperti pointer dereferencing. unsafe digunakan untuk mengimplementasikan abstraksi yang aman berdasarkan dunia yang pada dasarnya tidak aman sehingga sebagian besar kode Rust dapat menggunakan abstraksi ini dan menghindari akses memori yang tidak aman.
Jaminan keamanan
Rust menjamin keamanan sebagai salah satu prinsip intinya. Kita dapat mengatakan bahwa ini adalah arti dari keberadaan bahasa. Namun, itu tidak memberikan keamanan dalam arti tradisional, selama pelaksanaan program dan menggunakan pemulung. Sebaliknya, Rust menggunakan sistem tipe yang sangat canggih untuk melacak kapan dan nilai apa yang dapat diakses. Compiler kemudian secara statis menganalisis setiap program Rust untuk memastikan bahwa selalu dalam keadaan yang benar.
Keamanan Python
Mari kita ambil Python sebagai contoh. Kode Python murni tidak dapat merusak memori. Akses ke item daftar memiliki cek untuk melampaui batas; tautan yang dikembalikan oleh fungsi dihitung untuk menghindari munculnya tautan yang menggantung; Tidak ada cara untuk melakukan aritmatika arbitrer dengan pointer.
Ini memiliki dua konsekuensi. Pertama, banyak jenis harus "istimewa." Misalnya, tidak mungkin untuk mengimplementasikan daftar atau kamus yang efektif dengan Python murni. Sebaliknya, juru bahasa CPython memiliki implementasi internal mereka. Kedua, akses ke fungsi eksternal (fungsi tidak diimplementasikan dengan Python), yang disebut antarmuka fungsi eksternal, memerlukan penggunaan modul ctypes khusus dan melanggar jaminan keamanan bahasa.
Dalam arti tertentu, ini berarti bahwa semua yang ditulis dengan Python tidak menjamin akses yang aman ke memori.
Keamanan di Rust
Rust juga menyediakan keamanan, tetapi alih-alih menerapkan struktur yang tidak aman di C, ia menyediakan trik: kata kunci tidak aman. Ini berarti bahwa struktur data mendasar di Rust, seperti Vec, VecDeque, BTreeMap, dan String, diimplementasikan di Rust.
Anda mungkin bertanya: "Tetapi, jika Rust memberikan tipuan terhadap jaminan keamanan kode, dan pustaka standar diimplementasikan menggunakan trik ini, bukankah segala sesuatu di Rust akan dianggap tidak aman?"
Singkatnya, pembaca yang budiman, ya , persis seperti di Python. Mari kita lihat lebih detail.
Apa yang dilarang di Rust yang aman?
Keamanan di Rust didefinisikan dengan baik: kami banyak memikirkannya. Singkatnya, program Rust yang aman tidak bisa:
- Mendereferensi pointer yang menunjuk ke tipe yang berbeda dari yang diketahui kompiler . Ini berarti bahwa tidak ada pointer ke nol (karena mereka tidak menunjuk ke mana pun), tidak ada kesalahan keluar dari batas dan / atau kesalahan segmentasi (kesalahan segmentasi), tidak ada buffer overflow. Tetapi itu juga berarti bahwa tidak ada kegunaan setelah membebaskan memori atau membebaskan kembali memori (karena membebaskan memori dianggap sebagai penereferensi pointer) dan tidak ada pun kata yang dimaksudkan untuk mengetik .
- Memiliki beberapa referensi yang dapat berubah ke suatu objek atau secara bersamaan referensi yang dapat berubah dan tidak dapat diubah ke suatu objek . Yaitu, jika Anda memiliki referensi yang dapat diubah ke suatu objek, Anda hanya dapat memilikinya, dan jika Anda memiliki referensi yang tidak dapat diubah ke objek, itu tidak akan berubah sampai Anda menyimpannya. Ini berarti bahwa Anda tidak dapat memaksa perlombaan data di Safe Rust, yang merupakan jaminan bahwa sebagian besar bahasa aman lainnya tidak dapat menyediakan.
Karat mengkodekan informasi ini dalam sistem tipe atau menggunakan tipe data aljabar , seperti Opsi untuk menunjukkan ada / tidaknya nilai dan Hasil <T, E> untuk menunjukkan kesalahan / keberhasilan, atau referensi dan masa pakainya , misalnya, & T vs & mut T untuk menunjukkan tautan umum (tidak dapat diubah) dan tautan eksklusif (dapat diubah) dan & a T vs & 'b T untuk membedakan tautan yang benar dalam konteks yang berbeda (ini biasanya dihilangkan karena kompiler cukup pintar untuk mengetahuinya sendiri) .
Contohnya
Misalnya, kode berikut ini tidak akan dikompilasi karena mengandung tautan menggantung. Lebih khusus, my_struct tidak cukup hidup . Dengan kata lain, fungsi akan mengembalikan tautan ke sesuatu yang tidak ada lagi, dan oleh karena itu kompiler tidak dapat (dan, pada kenyataannya, bahkan tidak tahu bagaimana) mengkompilasi ini.
fn dangling_reference(v: &u64) -> &MyStruct {
Kode ini melakukan hal yang sama, tetapi mencoba untuk mengatasi masalah ini dengan menempatkan nilai pada heap (Box adalah nama dari smart pointer dasar di Rust).
fn dangling_heap_reference(v: &u64) -> &Box<MyStruct> { let my_struct = MyStruct { value: v };
Kode yang benar dikembalikan oleh Box itu sendiri dan bukan referensi untuk itu. Ini mengkodekan transfer kepemilikan - tanggung jawab untuk membebaskan memori - dalam tanda tangan fungsi. Ketika melihat tanda tangan, menjadi jelas bahwa kode panggilan bertanggung jawab atas apa yang terjadi dengan Box, dan, memang, kompiler memprosesnya secara otomatis.
fn no_dangling_reference(v: &u64) -> Box<MyStruct> { let my_struct = MyStruct { value: v }; let my_box = Box::new(my_struct);
Beberapa hal buruk tidak dilarang di Rust yang aman. Sebagai contoh, itu diizinkan dari sudut pandang kompiler:
- menyebabkan kebuntuan dalam program
- kebocoran sejumlah besar memori yang sewenang-wenang
- gagal menutup pegangan file, koneksi basis data, atau penutup poros rudal
Kekuatan ekosistem Rust adalah bahwa banyak proyek memilih untuk menggunakan sistem tipe untuk memastikan bahwa kode seakurat mungkin, tetapi kompiler tidak memerlukan paksaan seperti itu, kecuali dalam kasus di mana akses memori yang aman disediakan.
Apa yang diizinkan di Rust yang tidak aman?
Kode Karat Tidak Aman adalah kode Karat dengan kata kunci tidak aman. tidak aman dapat diterapkan ke fungsi atau blok kode. Ketika diterapkan pada suatu fungsi, itu berarti "fungsi ini mensyaratkan bahwa kode yang dipanggil secara manual menyediakan invarian yang biasanya disediakan oleh kompiler." Ketika diterapkan pada blok kode, itu berarti "blok kode ini secara manual menyediakan invarian yang diperlukan untuk mencegah akses yang tidak aman ke memori, dan oleh karena itu diperbolehkan untuk melakukan hal-hal yang tidak aman."
Dengan kata lain, tidak aman untuk fungsi berarti "Anda perlu memeriksa semuanya", dan pada blok kode - "Saya sudah memeriksa semuanya."
Seperti dicatat dalam Bahasa Pemrograman Karat , kode dalam blok yang ditandai dengan kata kunci tidak aman dapat:
- Dereferensi penunjuk. Ini adalah "kekuatan super" utama yang memungkinkan Anda untuk mengimplementasikan daftar yang ditautkan ganda, peta hash, dan struktur data mendasar lainnya.
- Panggil fungsi atau metode yang tidak aman. Lebih lanjut tentang ini di bawah ini.
- Akses atau ubah variabel statis yang dapat berubah. Variabel statis yang ruang lingkupnya tidak dikontrol tidak dapat diperiksa secara statis, oleh karena itu penggunaannya tidak aman.
- Terapkan sifat tidak aman. Sifat tidak aman digunakan untuk menandai apakah tipe tertentu menjamin invarian tertentu. Misalnya, Kirim dan Sinkronisasi menentukan apakah suatu jenis dapat dikirim di antara batas utas atau dapat digunakan oleh beberapa utas secara bersamaan.
Ingat pointer menggantung di atas? Tambahkan kata tidak aman, dan kompiler akan bersumpah dua kali lebih banyak karena dia tidak suka menggunakan tidak aman di tempat yang tidak diperlukan.
Alih-alih, kata kunci yang tidak aman digunakan untuk mengimplementasikan abstraksi yang aman berdasarkan operasi penunjuk sewenang-wenang. Misalnya, tipe Vec diimplementasikan menggunakan tidak aman, tetapi aman untuk menggunakannya, karena memeriksa upaya untuk mengakses elemen dan tidak memungkinkan meluap. Meskipun ia menyediakan operasi seperti set_len, yang dapat menyebabkan akses memori tidak aman, mereka ditandai sebagai tidak aman.
Misalnya, kita bisa melakukan hal yang sama seperti pada contoh no_dangling_reference, tetapi dengan penggunaan tidak aman yang tidak masuk akal:
fn manual_heap_reference(v: u64) -> *mut MyStruct { let my_struct = MyStruct { value: v }; let my_box = Box::new(my_struct);
Perhatikan kurangnya kata yang tidak aman. Membuat pointer benar-benar aman. Seperti yang ditulis, ini adalah risiko kebocoran memori, tetapi tidak lebih, dan kebocoran memori aman. Memanggil fungsi ini juga aman. tidak aman hanya diperlukan ketika ada sesuatu yang mencoba mengubah pointer. Sebagai bonus tambahan, dereferencing akan secara otomatis melepaskan memori yang dialokasikan.
fn main() { let my_pointer = manual_heap_reference(1337); let my_boxed_struct = unsafe { Box::from_raw(my_pointer) };
Setelah optimasi, kode ini setara dengan hanya mengembalikan Kotak. Box adalah abstraksi berbasis pointer yang aman karena mencegah distribusi pointer di mana-mana. Misalnya, versi utama berikutnya akan mengarah ke memori bebas ganda (double-free).
fn main() { let my_pointer = manual_heap_reference(1337); let my_boxed_struct_1 = unsafe { Box::from_raw(my_pointer) };
Jadi apa abstraksi yang aman?
Abstraksi yang aman adalah abstraksi yang menggunakan sistem tipe untuk menyediakan API yang tidak dapat digunakan untuk melanggar jaminan keamanan yang disebutkan di atas. Kotak lebih aman * mut T, karena tidak dapat menyebabkan alokasi memori ganda, seperti yang diilustrasikan di atas.
Contoh lain adalah tipe Rc di Rust. Ini adalah penghitung penghitung referensi - referensi yang tidak dapat diubah ke data pada heap. Karena memungkinkan beberapa akses simultan ke satu area memori, itu harus mencegah perubahan agar dianggap aman.
Selain itu, ini bukan thread yang aman. Jika Anda membutuhkan keamanan utas, Anda harus menggunakan tipe Arc (Penghitungan Referensi Atom), yang memiliki penalti kinerja karena penggunaan nilai atom untuk menghitung tautan dan mencegah kemungkinan balapan data di lingkungan multi-utas.
Kompiler tidak akan memungkinkan Anda untuk menggunakan Rc di mana Anda harus menggunakan Arc, karena pembuat seperti Rc tidak menandainya sebagai thread aman. Jika mereka melakukan ini, itu tidak masuk akal: janji keamanan yang salah.
Kapan Rust tidak aman dibutuhkan?
Rust yang Tidak Aman selalu diperlukan ketika perlu untuk melakukan operasi yang melanggar salah satu dari dua aturan yang dijelaskan di atas. Misalnya, dalam daftar yang ditautkan dua kali lipat, tidak adanya tautan yang dapat berubah ke data yang sama (untuk elemen berikutnya dan elemen sebelumnya) sepenuhnya menghilangkan manfaatnya. Dengan tidak aman, pelaksana daftar tertaut ganda dapat menulis kode menggunakan * mut Node pointer dan kemudian merangkumnya dalam abstraksi yang aman.
Contoh lain adalah bekerja dengan sistem tertanam. Seringkali mikrokontroler menggunakan satu set register yang nilainya ditentukan oleh keadaan fisik perangkat. Dunia tidak dapat berhenti saat Anda mengambil & memutus u dari register semacam itu, oleh karena itu tidak aman diperlukan untuk bekerja dengan peti dukungan perangkat. Biasanya, peti ini merangkum keadaan dalam pembungkus yang transparan dan aman yang menyalin data bila memungkinkan, atau menggunakan teknik lain yang memberikan jaminan kompiler.
Kadang-kadang perlu untuk melakukan operasi yang dapat menyebabkan membaca dan menulis secara bersamaan, atau akses tidak aman ke memori, dan ini adalah di mana tidak aman diperlukan. Tetapi selama ada kesempatan untuk memastikan bahwa invarian aman dipertahankan sebelum pengguna menyentuh sesuatu (yaitu, tidak ditandai tidak aman), semuanya baik-baik saja.
Di pundak siapa terletak tanggung jawab ini?
Kami sampai pada pernyataan yang dibuat sebelumnya - ya , kegunaan kode Rust didasarkan pada kode yang tidak aman. Terlepas dari kenyataan bahwa ini dilakukan dengan cara yang sedikit berbeda dari implementasi struktur data dasar yang tidak aman di Python, implementasi Vec, Hashmap, dll., Harus menggunakan manipulasi pointer sampai batas tertentu.
Kami mengatakan bahwa Rust aman, dengan asumsi mendasar bahwa kode tidak aman yang kami gunakan melalui dependensi kami pada pustaka standar atau kode pustaka lain ditulis dengan benar dan dienkapsulasi. Keuntungan mendasar dari Rust adalah bahwa kode tidak aman didorong ke blok tidak aman yang harus diperiksa dengan cermat oleh penulisnya.
Dalam Python, beban memeriksa keamanan manipulasi memori hanya terletak pada pengembang interpreter dan pengguna antarmuka fungsi eksternal. Di C, beban ini ada di tangan setiap programmer.
Di Rust, itu terletak pada pengguna kata kunci yang tidak aman. Ini jelas, karena invarian harus dipelihara secara manual di dalam kode tersebut, dan oleh karena itu perlu untuk berjuang untuk jumlah terkecil dari kode tersebut di perpustakaan atau kode aplikasi. Ketidakamanan terdeteksi, disorot, dan ditunjukkan. Oleh karena itu, jika segfault terjadi dalam kode Rust Anda, maka Anda menemukan kesalahan dalam kompiler atau kesalahan di beberapa baris kode tidak aman Anda.
Ini bukan sistem yang sempurna, tetapi jika Anda membutuhkan kecepatan, keamanan dan multithreading pada saat yang sama, maka ini adalah satu-satunya pilihan.