Topik ini membahas dua sumber risiko yang menentukan apakah jaminan keamanan Rust benar-benar berlaku pada sebuah proyek: kode yang keluar dari pengawasan kompilator melalui unsafe dan FFI, serta kode yang masuk dari luar proyek melalui dependensi crate. Pembahasan mencakup makna kontrak soundness pada blok unsafe, pola pembungkusan yang menjaga jaminan tetap utuh, vektor serangan pada rantai pasokan (supply chain) crate, serta kebiasaan praktis untuk menahan keduanya. Setelah membaca topik ini, keputusan memakai unsafe maupun menambah dependensi dapat diambil dengan dasar yang dapat diperiksa ulang.
Dua sumber risiko#
Jaminan keamanan memori pada Rust bukan sifat bawaan binari, melainkan hasil pemeriksaan yang berjalan pada waktu kompilasi atas kode yang dilihat kompilator. Dua pintu dapat melewati pemeriksaan itu. Pintu pertama adalah unsafe beserta FFI: penulis sendiri menyatakan bahwa sebuah operasi dijamin benar, sehingga kompilator berhenti memverifikasinya. Pintu kedua adalah dependensi: baris-baris kode asing ikut terkompilasi ke dalam binari dengan hak akses yang sama seperti kode sendiri. Tabel berikut merangkum wilayah risiko tersebut.
| Wilayah | Yang berhenti diperiksa | Yang memegang kendali |
|---|---|---|
Blok unsafe pada kode sendiri | pemeriksaan batas dan aliasing pada operasi tertentu | kontrak yang ditulis dan diuji penulis |
| FFI ke pustaka C atau C++ | seluruh jaminan memori pada batas ABI | dokumentasi pustaka dan disiplin pemanggil |
| Crate pihak ketiga | kepercayaan bahwa kode asing berperilaku baik | proses pemilihan, penguncian versi, dan audit |
Kedua pintu itu tidak setara derajatnya. Blok unsafe pada umumnya berukuran beberapa baris dan dapat ditinjau satu per satu, sedangkan satu dependensi kecil dapat menarik belasan crate transitif yang tidak pernah dibaca siapa pun. Karena itu pengelolaan keduanya memakai cara yang berbeda: unsafe dikelola dengan kontrak dan pengujian, sedangkan rantai pasokan dikelola dengan kebijakan dependensi. Bagian berikut membahas keduanya secara berurutan.
Batas unsafe dalam praktik#
Blok unsafe tidak mematikan pemeriksaan kompilator secara global. Blok itu hanya membuka segelintir kemampuan yang kode aman tidak boleh miliki, antara lain membaca dan menulis melalui pointer mentah, memanggil fungsi unsafe, mengakses static yang dapat diubah, dan mengimplementasikan trait unsafe. Selebihnya borrow checker, pemeriksaan tipe, dan pemeriksaan batas tetap bekerja seperti biasa, termasuk pada kode lain di dalam blok yang sama. Tanggung jawab tersebut menegakkan soundness, yaitu jaminan bahwa pembungkus unsafe memenuhi setiap kontrak operasinya, sehingga pemanggil pembungkus tetap tidak mungkin memicu undefined behavior.
Program berikut memperlihatkan pola pembungkus yang paling sederhana, yaitu pengambilalihan pemeriksaan batas dari kompilator. Metode get pada slice tetap memeriksa batas secara bawaan, sedangkan fungsi elemen_pada membangun jalur kedua lewat get_unchecked yang melewatkan pemeriksaan itu.
fn main() {
let nilai = [70, 80, 90, 100];
// jalur aman: pemeriksaan batas dilakukan oleh metode get
println!("get(2) : {:?}", nilai.get(2));
println!("get(100) : {:?}", nilai.get(100));
// jalur unsafe: pemeriksaan batas ditulis dengan tangan di dalam fungsi
println!("elemen_pada(2) : {:?}", elemen_pada(&nilai, 2));
println!("elemen_pada(100) : {:?}", elemen_pada(&nilai, 100));
}
/// Mengambil elemen larik pada posisi tertentu.
///
/// Mengembalikan `None` apabila `posisi` tidak lebih kecil daripada panjang `data`.
fn elemen_pada(data: &[i32], posisi: usize) -> Option<i32> {
if posisi < data.len() {
// kontrak get_unchecked terpenuhi sehingga pemanggilannya sah
Some(unsafe { *data.get_unchecked(posisi) })
} else {
None
}
}get(2) : Some(90)
get(100) : None
elemen_pada(2) : Some(90)
elemen_pada(100) : NoneKeempat baris keluaran tersebut identik hasilnya, dan itulah inti polanya: selama kontrak dijaga, jalur unsafe berperilaku sama dengan jalur aman. Hapus baris pemeriksaan posisi < data.len() dan program tetap terkompilasi tanpa satu peringatan, karena yang dilanggar bukan lagi aturan bahasa yang diperiksa kompilator, melainkan kontrak get_unchecked yang hanya dibaca manusia. Fragmen berikut memperlihatkan pelanggaran semacam itu.
// tidak-dijalankan karena membaca di luar batas adalah perilaku tak terdefinisi.
let nilai = [70, 80, 90, 100];
// 100 melebihi panjang larik sehingga kontrak get_unchecked dilanggar
let elemen = unsafe { *nilai.get_unchecked(100) };
println!("{elemen}");Pada praktik yang sehat, setiap fungsi yang membungkus unsafe menuliskan kontraknya sebagai bagian # Safety pada dokumentasi, dan jalur tersebut ditutupi pengujian khusus. Alat Miri turut membantu dengan menjalankan pengujian pada penerjemah yang mendeteksi pelanggaran umum seperti akses di luar batas dan aliasing ilegal, sehingga kesalahan yang tidak menimbulkan gejala pada mesin asli tertangkap sebelum dikirim.
Rantai pasokan crate#
Cargo membuat penambahan kode asing menjadi sangat mudah, dan kemudahan itu mempunyai sisi yang perlu dikelola. Setiap dependensi dijalankan dengan hak yang sama seperti kode sendiri: membaca berkas, membuka jaringan, menjalankan proses. Serangan pada ekosistem paket modern umumnya tidak menyerang kode korban, melainkan jalur pengirimannya, sehingga tiga vektor berikut termasuk yang paling sering dibahas pada ekosistem Rust.
| Vektor | Cara kerja | Pengurangan risiko |
|---|---|---|
| Pemalsuan nama (typosquatting) | nama yang mirip crate populer diunggah ke crates.io | periksa ejaan dan riwayat unduhan sebelum menambah crate |
| Akun perawat yang dibajak | versi berisi muatan jahat diterbitkan dari akun sah | kunci versi lewat Cargo.lock, naik versi secara sadar |
Skrip build.rs | kode dependensi dijalankan pada saat kompilasi | tinjau crate baru dan kecilkan jumlah dependensi |
Alat utama untuk memeriksa dependensi adalah cargo-audit: subcommand Cargo yang membandingkan daftar dependensi proyek dengan basis data advisori RustSec, lalu melaporkan setiap versi yang diketahui bermasalah beserta tingkat keparahannya. Perintahnya sederhana, tetapi pemasangannya membutuhkan unduhan crate eksternal sehingga tidak dijalankan pada lingkungan topik ini.
# tidak-dijalankan karena cargo-audit belum terpasang di lingkungan ini.
cargo install cargo-audit
cargo auditSejumlah alat melengkapi cargo-audit untuk kebijakan yang lebih ketat. Perintah cargo tree menggambar seluruh pohon dependensi transitif sehingga jumlah crate yang sebenarnya ikut terkompilasi tampak jelas. Alat cargo-deny menolak dependensi berdasarkan daftar terlarang dan ketentuan lisensi, sedangkan cargo-vet menuntun pencatatan peninjauan manual antar anggota tim. Ketiganya umumnya dijalankan sebagai gerbang pada pipeline CI bersama pemeriksaan bawaan Cargo.
Kebijakan praktis#
Kebiasaan berikut menahan kedua sumber risiko sekaligus tanpa membutuhkan alat yang rumit. Sebagian diterapkan sekali pada awal proyek, sedangkan sisanya menjadi gerbang yang berjalan pada setiap perubahan.
- Nyatakan niat
unsafesejak awal: crate murni aman memasang#![forbid(unsafe_code)]agar setiap pelanggaran menggagalkan kompilasi, sedangkan crate yang memang membutuhkannya mengumpulkanunsafepada modul sempit dengan kontrak terdokumentasi. - Kecilkan permukaan dependensi: tinjau kebutuhan, riwayat unduhan, dan pemeliharaan setiap crate baru sebelum masuk, karena pohon transitif ikut membawa risikonya.
- Kunci versi lewat berkas
Cargo.lockdan komit berkas itu pada proyek aplikasi, sehingga naik versi menjadi keputusan yang disengaja alih-alih kejutan pada pembangunan ulang. - Jalankan pemeriksaan sebagai gerbang:
cargo fmt --check,cargo clippy, pengujian, dan pemindaian dependensi dijalankan pada setiap perubahan sebelum digabungkan. - Uji jalur
unsafedengan Miri pada pengujian lokal agar pelanggaran kontrak yang tidak menimbulkan gejala tetap tertangkap sebelum pengiriman.
Kesalahan umum#
Menaruh unsafe tanpa kontrak tertulis. Pembungkus yang tidak mendokumentasikan syaratnya memindahkan beban pemeriksaan ke setiap pemanggil, dan satu pemanggil yang lengah cukup untuk merusak memori. Tuliskan bagian # Safety pada dokumentasi fungsi yang membungkus unsafe, lalu jaga agar bloknya sekecil mungkin.
Menganggap Cargo.lock dapat diabaikan pada aplikasi. Kunci yang tidak dikomit membuat mesin CI dan mesin rekan kerja membangun dengan versi dependensi yang berbeda-beda, sehingga hasil pemindaian audit pun tidak menggambarkan binari yang benar-benar dikirim. Komit berkas itu pada proyek aplikasi, dan perlakukan pembaruan versi sebagai perubahan yang ditinjau.
Menyamakan keamanan bahasa dengan keamanan ekosistem. Keamanan memori Rust berlaku atas kode yang diperiksa kompilator, bukan atas niat pembuat crate, sehingga jumlah unduhan yang besar tidak menjamin apa pun. Tetapkan proses pemeriksaan dependensi yang sama ketatnya dengan proses tinjauan kode sendiri.
Ringkasan#
- Jaminan Rust berlaku atas kode yang diperiksa kompilator, dan dua pintu keluarnya adalah
unsafebersama FFI serta dependensi pihak ketiga. - Pembungkus
unsafeyang sehat menjaga kontrak operasinya sehingga pemanggil tetap tidak mungkin memicu undefined behavior, dan kontrak itu ditulis sebagai dokumentasi. - Rantai pasokan crate dikelola dengan meminimalkan dependensi, mengunci versi pada
Cargo.lock, dan memindai advisori melalui cargo-audit. - Gerbang CI yang memuat fmt, clippy, pengujian, dan pemindaian dependensi menjaga kedua sumber risiko tetap terpantau pada setiap perubahan.
Referensi#
- Unsafe Rust — The Rust Programming Language
— Bab resmi tentang kemampuan yang dibuka blok
unsafebeserta tanggung jawabnya. - The Rustonomicon — The Dark Arts of Unsafe Rust
— Pembahasan mendalam tentang soundness dan penulisan kode
unsafeyang benar. - RustSec Advisory Database — Basis data advisori keamanan untuk crate beserta alat pemeriksanya.
- Miri — an undefined behavior detection tool
— Penerjemah untuk mendeteksi pelanggaran kontrak
unsafepada pengujian.