Apakah mungkin untuk mengevaluasi keselamatan secara terprogram untuk kode arbitrer?

10

Saya telah banyak memikirkan akhir-akhir ini tentang kode aman. Aman untuk benang. Memori-aman. Safe-to-to-explode-in-your-face-dengan-segfault aman. Tetapi demi kejelasan dalam pertanyaan, mari kita gunakan model keselamatan Rust sebagai definisi kita.

Seringkali, memastikan keamanan adalah sedikit masalah seberapa besar, karena, sebagaimana dibuktikan oleh kebutuhan Rust unsafe, ada beberapa ide pemrograman yang sangat masuk akal, seperti konkurensi, yang tidak dapat diterapkan di Rust tanpa menggunakan unsafekata kunci. . Meskipun konkurensi dapat dibuat sangat aman dengan kunci, mutex, saluran dan isolasi memori atau apa pun yang Anda miliki, ini memerlukan kerja di luar model keselamatan Rust dengan unsafe, dan kemudian secara manual meyakinkan kompiler bahwa, "Ya, saya tahu apa yang saya lakukan Ini terlihat tidak aman, tetapi saya telah membuktikan secara matematis itu sangat aman.

Tapi biasanya, ini turun ke membuat model hal-hal ini secara manual dan membuktikan mereka aman dengan pembuktian teorema . Dari kedua perspektif ilmu komputer (apakah mungkin) dan perspektif kepraktisan (apakah akan mengambil kehidupan alam semesta), apakah masuk akal untuk membayangkan sebuah program yang mengambil kode arbitrer dalam bahasa yang arbitrer dan mengevaluasi apakah itu " Aman dari karat "?

Peringatan :

  • Cara mudah untuk hal ini adalah dengan menunjukkan bahwa program tersebut bisa tidak mematikan dan karena itu masalah penghentian membuat kita gagal. Katakanlah setiap program yang diumpankan ke pembaca dijamin akan berhenti
  • Walaupun "kode arbitrer dalam bahasa arbitrer" adalah tujuannya, tentu saja saya sadar bahwa ini tergantung pada keakraban program dengan bahasa yang dipilih, yang akan kita terima
TheEnvironmentalist
sumber
2
Kode sewenang-wenang? Tidak. Saya membayangkan Anda bahkan tidak dapat membuktikan keamanan kode yang paling berguna karena I / O dan pengecualian perangkat keras.
Telastyn
7
Mengapa Anda mengabaikan Masalah Penghentian? Setiap satu dari contoh yang Anda sebutkan, dan banyak lagi, telah terbukti setara dengan memecahkan Masalah Pemutusan, Masalah Fungsi, Teorema Rice, atau salah satu dari banyak Teorema Undecidability lainnya: keamanan pointer, keamanan memori, thread -safety, exception-safety, purity, I / O-safety, lock-safety, progress progres, dll. Masalah Henti adalah salah satu properti statis yang paling sederhana yang mungkin Anda ingin ketahui, segala sesuatu yang Anda daftarkan jauh lebih sulit .
Jörg W Mittag
3
Jika Anda hanya peduli pada positif palsu, dan bersedia menerima negatif palsu, saya memiliki algoritma yang mengklasifikasikan semuanya: "Apakah aman? Tidak"
Caleth
Anda benar - benar tidak perlu menggunakan unsafeRust untuk menulis kode bersamaan. Ada beberapa mekanisme berbeda yang tersedia, mulai dari sinkronisasi primitif hingga saluran yang terinspirasi aktor.
RubberDuck

Jawaban:

8

Apa yang akhirnya kita bicarakan di sini adalah waktu kompilasi vs runtime.

Kompilasi kesalahan waktu, jika dipikir-pikir, akhirnya berarti kompilator dapat menentukan masalah apa yang Anda miliki dalam program Anda bahkan sebelum dijalankan. Ini jelas bukan kompiler "bahasa arbitrer", tapi saya akan segera kembali ke itu. Kompiler, dengan segala kebijaksanaannya yang tak terbatas, tidak mencantumkan setiap masalah yang dapat ditentukan oleh kompiler. Ini sebagian tergantung pada seberapa baik kompiler ditulis, tetapi alasan utama untuk ini adalah bahwa banyak hal yang baik ditentukan pada saat runtime .

Kesalahan runtime, seperti yang Anda ketahui dengan baik, saya yakin sama seperti saya, adalah segala jenis kesalahan yang terjadi selama pelaksanaan program itu sendiri. Ini termasuk pembagian dengan nol, pengecualian penunjuk nol, masalah perangkat keras, dan banyak faktor lainnya.

Sifat kesalahan runtime berarti Anda tidak dapat mengantisipasi kesalahan tersebut pada waktu kompilasi. Jika Anda bisa, mereka hampir pasti akan diperiksa pada waktu kompilasi. Jika Anda bisa menjamin angka nol pada waktu kompilasi, maka Anda bisa melakukan kesimpulan logis tertentu, seperti membagi angka dengan angka itu akan menghasilkan kesalahan aritmatika yang disebabkan oleh membagi dengan nol.

Dengan demikian, dengan cara yang sangat nyata, musuh pemrograman yang secara terprogram menjamin berfungsinya suatu program sedang melakukan pemeriksaan runtime sebagai lawan dari kompilasi pemeriksaan waktu. Contoh dari ini mungkin melakukan gips dinamis ke tipe lain. Jika ini diizinkan, Anda, programmer, pada dasarnya mengesampingkan kemampuan kompiler untuk mengetahui apakah itu hal yang aman untuk dilakukan. Beberapa bahasa pemrograman telah memutuskan bahwa ini dapat diterima sementara yang lain setidaknya akan memperingatkan Anda pada waktu kompilasi.

Contoh lain yang baik mungkin memungkinkan nulls menjadi bagian dari bahasa, karena pengecualian pointer nol dapat terjadi jika Anda mengizinkan nulls. Beberapa bahasa telah menghilangkan masalah ini sepenuhnya dengan mencegah variabel-variabel yang tidak secara eksplisit dinyatakan mampu menahan nilai-nilai nol untuk dinyatakan tanpa segera diberi nilai (ambil contohnya Kotlin). Meskipun Anda tidak dapat menghilangkan kesalahan runtime pengecualian pointer kosong, Anda dapat mencegahnya terjadi dengan menghapus sifat dinamis bahasa. Di Kotlin, Anda dapat memaksakan kemungkinan memegang nilai nol tentu saja, tetapi tidak perlu dikatakan bahwa ini adalah "pembeli berhati-hatilah" metaforis karena Anda harus secara eksplisit menyatakannya.

Bisakah Anda secara konseptual memiliki kompiler yang dapat memeriksa kesalahan dalam setiap bahasa? Ya, tapi itu mungkin kompiler yang kikuk dan sangat tidak stabil di mana Anda harus menyediakan bahasa yang dikompilasi sebelumnya. Itu juga tidak bisa mengetahui banyak hal tentang program Anda, lebih dari kompiler untuk bahasa tertentu tahu hal-hal tertentu tentang itu, seperti masalah terputusnya seperti yang Anda sebutkan. Ternyata, banyak sekali informasi yang mungkin menarik untuk dipelajari tentang suatu program tidak mungkin diperoleh. Ini sudah terbukti, jadi kemungkinan tidak akan berubah dalam waktu dekat.

Kembali ke poin utama Anda. Metode tidak aman secara otomatis. Ada alasan praktis untuk ini, yaitu metode aman thread juga lebih lambat bahkan ketika thread tidak digunakan. Rust memutuskan bahwa mereka dapat menghilangkan masalah runtime dengan membuat metode thread aman secara default, dan itu adalah pilihan mereka. Itu datang dengan biaya sekalipun.

Dimungkinkan secara matematis untuk membuktikan kebenaran suatu program, tetapi akan dengan peringatan bahwa Anda akan benar-benar memiliki nol fitur runtime dalam bahasa tersebut. Anda akan dapat membaca bahasa ini dan tahu apa fungsinya tanpa ada kejutan. Bahasanya mungkin akan terlihat sangat matematis, dan kemungkinan tidak ada kebetulan di sana. Peringatan kedua adalah bahwa kesalahan runtime masih terjadi, yang mungkin tidak ada hubungannya dengan program itu sendiri. Oleh karena itu, program ini dapat terbukti benar, dengan asumsi serangkaian asumsi tentang komputer itu sedang berjalan di akurat dan tidak berubah, yang tentu saja selalu tidak terjadi pula dan sering.

Neil
sumber
3

Tipe sistem adalah bukti yang dapat diverifikasi secara otomatis dari beberapa aspek kebenaran. Misalnya, sistem tipe Rust dapat membuktikan bahwa referensi tidak hidup lebih lama dari objek yang direferensikan, atau bahwa objek yang direferensikan tidak dimodifikasi oleh utas lainnya.

Tetapi sistem tipe sangat terbatas:

  • Mereka dengan cepat mengalami masalah decidability. Secara khusus, sistem tipe itu sendiri harus dapat ditentukan, namun banyak sistem tipe praktis yang secara tidak sengaja Turing Lengkap (termasuk C ++ karena templat dan Karat karena sifat-sifat). Juga, sifat-sifat tertentu dari program yang mereka verifikasi mungkin tidak dapat diputuskan dalam kasus umum, paling terkenal apakah beberapa program berhenti (atau menyimpang).

  • Selain itu, sistem tipe harus berjalan dengan cepat, idealnya dalam waktu linier. Tidak semua bukti yang mungkin harus ditampilkan dalam sistem tipe. Misalnya, seluruh analisis program biasanya dihindari, dan bukti dicakup dalam modul atau fungsi tunggal.

Karena keterbatasan ini, sistem tipe cenderung hanya memverifikasi properti yang cukup lemah yang mudah dibuktikan, misalnya bahwa suatu fungsi dipanggil dengan nilai tipe yang benar. Namun bahkan yang secara substansial membatasi ekspresif, jadi sudah umum untuk memiliki solusi (seperti interface{}di Go, dynamicdi C #, Objectdi Jawa, void*di C) atau bahkan menggunakan bahasa yang sepenuhnya menghindari pengetikan statis.

Semakin kuat properti yang kami verifikasi, semakin ekspresif bahasa yang didapat. Jika Anda telah menulis Rust, Anda akan mengetahui momen "berjuang dengan kompiler" ini ketika kompiler menolak kode yang tampaknya benar, karena itu tidak dapat membuktikan kebenaran. Dalam beberapa kasus, tidak mungkin untuk mengekspresikan program tertentu di Rust bahkan ketika kami yakin kami dapat membuktikan kebenarannya. The unsafemekanisme Rust atau C # memungkinkan Anda untuk melepaskan diri dari kungkungan sistem jenis. Dalam beberapa kasus, menunda pemeriksaan ke runtime bisa menjadi pilihan lain - tetapi ini berarti kami tidak dapat menolak beberapa program yang tidak valid. Ini adalah masalah definisi. Program Rust yang panik aman sejauh menyangkut sistem tipe, tetapi tidak harus dari perspektif programmer atau pengguna.

Bahasa dirancang bersama dengan sistem tipenya. Jarang bahwa sistem tipe baru dikenakan pada bahasa yang ada (tetapi lihat misalnya MyPy, Flow, atau TypeScript). Bahasa akan mencoba membuatnya mudah untuk menulis kode yang sesuai dengan sistem tipe, misalnya dengan menawarkan anotasi tipe atau dengan memperkenalkan struktur aliran kontrol yang mudah dibuktikan. Bahasa yang berbeda mungkin berakhir dengan solusi yang berbeda. Misalnya Java memiliki konsep finalvariabel yang ditugaskan tepat sekali, mirip dengan mutvariabel non-Rust :

final int x;
if (...) { ... }
else     { ... }
doSomethingWith(x);

Java memiliki tipe aturan sistem untuk menentukan apakah semua jalur menetapkan variabel atau menghentikan fungsi sebelum variabel dapat diakses. Sebaliknya, Rust menyederhanakan bukti ini dengan tidak memiliki variabel yang dideklarasikan tetapi tidak dirancang, tetapi memungkinkan Anda mengembalikan nilai dari pernyataan aliran kontrol:

let x = if ... { ... } else { ... };
do_something_with(x)

Ini terlihat seperti poin yang sangat kecil ketika mencari tahu tugas, tetapi pelingkupan yang jelas sangat penting untuk bukti terkait seumur hidup.

Jika kita menerapkan sistem tipe gaya Rust ke Jawa, kita akan memiliki masalah yang jauh lebih besar dari itu: objek Java tidak dijelaskan dengan masa hidup, jadi kita harus memperlakukannya sebagai &'static SomeClassatau Arc<dyn SomeClass>. Itu akan melemahkan bukti yang dihasilkan. Java juga tidak memiliki konsep tingkat keabadian sehingga kita tidak dapat membedakan &dan &muttipe. Kami harus memperlakukan objek apa pun sebagai Sel atau Mutex, meskipun ini mungkin mengasumsikan jaminan yang lebih kuat daripada yang ditawarkan Java (mengubah bidang Java bukan threadsafe kecuali disinkronkan dan volatile). Akhirnya, Rust tidak memiliki konsep warisan implementasi gaya Java.

TL; DR: sistem tipe adalah pembalik teorema. Tetapi mereka dibatasi oleh masalah decidability dan masalah kinerja. Anda tidak bisa begitu saja mengambil satu jenis sistem dan menerapkannya ke bahasa yang berbeda, karena sintaks bahasa target mungkin tidak memberikan informasi yang diperlukan, dan karena semantik mungkin tidak kompatibel.

amon
sumber
3

Seberapa aman itu aman?

Ya, hampir mungkin untuk menulis verifikasi seperti itu: program Anda hanya perlu mengembalikan UNSAFE yang konstan. Anda akan benar 99% dari waktu

Karena bahkan jika Anda menjalankan program Rust yang aman, seseorang masih dapat menarik steker selama pelaksanaannya: sehingga program Anda mungkin berhenti walaupun secara teori tidak seharusnya.

Dan bahkan jika server Anda berjalan dalam sangkar faraday di bunker, proses tetangga mungkin mengeksekusi eksploitasi rowhammer dan membuat sedikit membalik di salah satu program Rust Anda yang seharusnya aman.

Yang ingin saya katakan adalah bahwa perangkat lunak Anda akan berjalan dalam lingkungan yang tidak deterministik dan banyak faktor luar yang dapat mempengaruhi eksekusi.

Lelucon, verifikasi otomatis

Sudah ada penganalisa kode statis yang dapat melihat konstruksi pemrograman berisiko (variabel tidak diinisialisasi, buffer overflows, dll ...). Ini bekerja dengan membuat grafik program Anda dan menganalisis penyebaran kendala (jenis, rentang nilai, pengurutan).

Analisis semacam ini juga dilakukan oleh beberapa kompiler demi optimasi.

Hal ini tentu mungkin untuk melangkah lebih jauh, dan juga menganalisis konkurensi, dan membuat kesimpulan tentang kendala propagasi di beberapa utas, sinkronisasi dan kondisi balap. Namun, dengan sangat cepat Anda akan mengalami masalah ledakan kombinatorial antara jalur eksekusi, dan banyak yang tidak diketahui (I / O, penjadwalan OS, input pengguna, perilaku program eksternal, interupsi, dll.) Yang akan mengurangi kendala yang diketahui ke telanjang. minimum dan membuatnya sangat sulit untuk membuat kesimpulan otomatis yang berguna pada kode arbitrer.

Christophe
sumber
1

Turing membahas ini pada tahun 1936 dengan makalahnya tentang masalah penghentian. Salah satu hasil adalah bahwa, hanya saja mustahil untuk menulis suatu algoritma yang 100% dari waktu dapat menganalisis kode dan menentukan dengan benar apakah akan berhenti atau tidak, tidak mungkin untuk menulis suatu algoritma yang dapat 100% dari waktu dengan benar tentukan apakah kode memiliki properti tertentu atau tidak, termasuk "keamanan" namun Anda ingin mendefinisikannya.

Namun, hasil Turing tidak menghalangi kemungkinan program yang dapat 100% dari waktu baik (1) benar-benar menentukan kode aman, (2) benar-benar menentukan bahwa kode tidak aman, atau (3) secara antropomorfis mengangkat tangannya dan berkata "Heck, aku tidak tahu." Kompiler Rust, secara umum, termasuk dalam kategori ini.

NovaDenizen
sumber
Jadi, selama Anda memiliki opsi "tidak yakin", ya?
TheEnvironmentalist
1
Kesimpulannya adalah selalu mungkin untuk menulis sebuah program yang mampu membingungkan program penganalisa program. Kesempurnaan tidak mungkin. Kepraktisan dimungkinkan.
NovaDenizen
1

Jika suatu program bersifat total (nama teknis untuk suatu program yang dijamin akan dihentikan), maka secara teori dimungkinkan untuk membuktikan setiap sifat sewenang-wenang atas program yang diberikan sumber daya yang cukup. Anda bisa menjelajahi setiap keadaan potensial yang bisa dimasukkan oleh program, dan memeriksa apakah ada yang melanggar properti Anda. The TLA + bahasa Model pengecekan menggunakan varian dari pendekatan ini, menggunakan teori himpunan untuk memeriksa properti Anda terhadap set negara Program potensial, daripada komputasi semua negara.

Secara teknis, program apa pun yang dijalankan pada perangkat keras fisik praktis adalah total, atau loop yang dapat dibuktikan karena Anda hanya memiliki jumlah penyimpanan terbatas, sehingga hanya ada sejumlah terbatas kondisi komputer dapat berada. (Praktis apa pun komputer sebenarnya mesin negara yang terbatas, bukan Turing lengkap, tetapi ruang negara sangat besar sehingga lebih mudah untuk berpura-pura mereka berputar lengkap).

Masalah dengan pendekatan ini adalah bahwa ia memiliki kompleksitas eksponensial dengan jumlah penyimpanan dan ukuran program, menjadikannya tidak praktis untuk apa pun selain inti dari algoritma, dan mustahil untuk diterapkan ke basis kode signifikan secara keseluruhan.

Jadi sebagian besar penelitian difokuskan pada bukti. Korespondensi Curry-Howard menyatakan bahwa bukti kebenaran dan sistem tipe adalah satu dan hal yang sama, sehingga sebagian besar penelitian praktis berjalan di bawah nama sistem tipe. Yang relevan dengan diskusi ini adalah Coq dan Idriss, selain Karat yang telah Anda sebutkan. Coq mendekati masalah teknik yang mendasarinya dari arah lain. Mengambil bukti kebenaran kode arbitrer dalam bahasa Coq, dapat menghasilkan kode yang mengeksekusi program terbukti. Idriss sementara itu menggunakan sistem tipe dependen untuk membuktikan kode arbitrer dalam bahasa seperti Haskell murni. Apa yang dilakukan kedua bahasa ini adalah mendorong masalah sulit menghasilkan bukti yang bisa diterapkan ke penulis, memungkinkan pemeriksa tipe untuk fokus memeriksa bukti. Memeriksa buktinya adalah masalah yang jauh lebih sederhana, tetapi ini membuat bahasa lebih sulit untuk dikerjakan.

Kedua bahasa ini di mana secara khusus dirancang untuk membuat bukti lebih mudah, menggunakan kemurnian untuk mengontrol keadaan apa yang relevan dengan bagian mana dari program. Untuk banyak bahasa umum, hanya membuktikan bahwa sepotong negara tidak relevan dengan bukti bagian dari program dapat menjadi masalah yang kompleks karena sifat efek samping dan nilai-nilai yang bisa berubah.

pengguna1937198
sumber