- Hubris adalah OS tempat tugas-tugas terisolasi berkomunikasi lewat IPC, dan lewat system call ke-13,
REPLY_FAULT, server dapat mengakhiri permintaan klien yang keliru sebagai fault, bukan dengan nilai error - Dari sisi klien, IPC tampak seperti pemanggilan fungsi, tetapi karena tugas dikompilasi secara terpisah, compiler tidak dapat sepenuhnya mencegah kode operasi yang salah, byte yang tidak dapat ditafsirkan, atau loaned memory yang tidak tepat
- Program Hubris yang normal hampir tidak pernah menemui error semacam ini berkat konfigurasi build dan kode Rust yang dihasilkan, sehingga memaksa
Result<T, IpcError>danunwrap()pada setiap panggilan akan menambah ukuran kode dan biaya runtime - Kernel langsung membunuh tugas yang melanggar prasyarat system call tanpa kode error, dan
REPLY_FAULTmemperluas kebijakan fail-fast yang sama ke respons server - Desain ini dengan cepat mengungkap penggunaan API yang keliru, tetapi fuzz test atau tugas chaos yang mengirim IPC dan system call acak hampir langsung direstart, sehingga pengujian menjadi sulit
Posisi Hubris IPC dan REPLY_FAULT
- Hubris memiliki kernel mandiri aplikasi yang kecil, dan menempatkan sebagian besar kode seperti driver, logika aplikasi, dan network stack dalam tugas terisolasi yang dikompilasi terpisah
- Komunikasi antar-tugas dilakukan lewat system call IPC yang diimplementasikan kernel
RECV: mengambil pesan masuk dengan prioritas tertinggi, atau memblokir sampai pesan datangSEND: menghentikan pemanggil, menyerahkan pesan dan kendali ke tugas penerima, lalu menunggu hingga menerima responsREPLY: mengirimkan respons ke tugas yang sebelumnya melakukanSENDagar dapat berjalan kembali
- Klien dan server di Hubris bukan identitas tetap, melainkan peran yang dijalankan tugas
- Tugas yang memakai
SENDberperan sebagai klien - Tugas yang memakai
RECVdanREPLYberperan sebagai server - Satu tugas dapat menjadi server bagi satu tugas, sekaligus menjadi klien bagi tugas lain
- Tugas yang memakai
Error yang luput dari compiler di batas tugas
- Dalam pemanggilan fungsi biasa, compiler dan linker menjamin tipe dan target pemanggilan dalam tingkat yang cukup besar
- Jika fungsi Rust menerima argumen
String, compiler mencegah pemanggil memberikanbool - Kebingungan target seperti bermaksud memanggil
pet_cattetapi justru memanggilfire_missilesjuga biasanya tidak terjadi
- Jika fungsi Rust menerima argumen
- Hubris IPC melewati batas tugas dan setiap tugas dikompilasi sebagai program terpisah, sehingga compiler tidak dapat memverifikasi seluruh relasi IPC secara langsung
- Secara garis besar, ada tiga jenis error yang dapat dihadapi server IPC
- Kode operasi yang tidak sesuai dengan antarmuka, misalnya “operation number 48” masuk ke antarmuka yang hanya memiliki dua operasi
- Sekumpulan byte yang bukan tipe pesan yang diharapkan dan tidak dapat ditafsirkan, atau pesan yang terlalu pendek atau terlalu panjang
- Loaned memory yang diperlukan tidak ada, atau memori datang sebagai read-only padahal diperlukan memori yang dapat ditulis
Mengapa program normal tidak dipaksa menangani error
- Program Hubris normal dikonfigurasi agar error IPC semacam ini tidak terjadi
- Koneksi tugas disusun melalui konfigurasi sistem build sehingga sulit tertukar satu sama lain
- Klien menyusun dan mengirim IPC dengan kode Rust yang dihasilkan
- Server juga memproses hasil dengan kode Rust terhasil yang terpisah
- Jika semua operasi IPC dibuat mengembalikan
Result<T, IpcError>, program normal harus menambahkanunwrap()untuk error yang sebenarnya tidak mungkin mereka temuiunwrap()membebani dari sisi ukuran kode- Ada juga biaya runtime untuk memeriksa error yang tidak akan terjadi
- Menaruh
unwrap()ataupanic!di dalam kode yang dihasilkan dapat memusatkan lokasi panic sehingga mengurangi dampak pada ukuran kode, tetapi biaya runtime tetap ada - Untuk mendukung kode error universal, semua operasi harus mengikuti aturan encoding error yang sama
- Semua operasi harus dapat mengembalikan error
- Semua operasi harus meng-encode error tersebut dengan cara yang sama
- Operasi yang tidak mungkin gagal pun harus direpresentasikan dalam bentuk yang dapat gagal
- Dalam firmware berbasis Hubris, terus ditemukan operasi yang memang tidak dapat gagal, dan konfigurasi pin GPIO adalah salah satu contohnya
Kebijakan fault agresif kernel Hubris
- Banyak sistem operasi mengembalikan kode error atau memberi kesempatan menangani exception/signal meski prasyarat system call dilanggar
- Di Unix, menutup file descriptor yang belum dibuka dengan
closeakan mengembalikan kode error - Memberikan null pointer ke
openalih-alih pathname juga akan mengembalikan kode error
- Di Unix, menutup file descriptor yang belum dibuka dengan
- Hubris langsung menghancurkan tugas jika prasyarat system call dilanggar
- Tugas tidak dapat lagi menjalankan instruksi
- Tugas itu sendiri tidak punya kesempatan untuk pulih atau melanjutkan
- Tugas supervisor aplikasi menerima pemberitahuan fault, lalu biasanya menghapus tugas tersebut dan me-restart-nya
- Fault yang dibuat kernel adalah synthetic fault
- Mirip dengan hardware fault yang dibuat CPU, seperti dereference null pointer atau pembagian dengan nol
- Hardware fault berasal dari pelanggaran aturan arsitektur prosesor, sedangkan synthetic fault berasal dari pelanggaran aturan kernel
- Misalnya, pada panggilan
SEND, jika indeks tugas penerima berada di luar rentang aplikasi, atau pointer pesan menunjuk ke memori yang tidak memiliki izin akses, synthetic fault akan terjadi - Hubris tidak mengizinkan fault yang dapat dipulihkan atau dapat dilanjutkan
- Baik hardware fault maupun synthetic fault, tugas yang menerima fault masuk ke status mati
- Pilihan ini dibuat untuk menghindari mode kegagalan yang subtil dan menyederhanakan penalaran tentang sistem
Cara server merespons klien dengan fault
REPLY_FAULTadalah system call yang memungkinkan server mengirim fault kepada klien sebagai ganti respons normal- Alur
REPLYbiasa adalah sebagai berikut- Saat klien memakai
SEND, kernel menandai tugas klien sebagai “waiting to send” terhadap tugas penerima - Saat tugas penerima memakai
RECV, klien tersebut menjadi berstatus “waiting for reply” - Saat server memanggil
REPLY, klien kembali ke status runnable
- Saat klien memakai
REPLY_FAULTmirip denganREPLY, tetapi alih-alih mengirim pesan dan menjadikannya dapat berjalan, ia mengirim fault dan membuat tugas masuk status mati- Server tidak bisa membunuh tugas sembarangan
REPLY_FAULThanya dapat digunakan pada tugas yang sudah di-RECVoleh server tersebut dan belum di-REPLY- Ini hanya bekerja terhadap klien yang sedang menunggu respons dari server tertentu
- Hubris memakai
REPLY_FAULTuntuk menangani error berikut- Kode operasi yang salah
- Pesan yang rusak, terpotong, atau tidak bermakna
- Klien tidak mengirim loaned memory dengan jenis yang benar
Error aplikasi dan pengalaman fail-fast
REPLY_FAULTdapat dipakai bukan hanya untuk error format IPC, tetapi juga untuk error spesifik aplikasi- Stack IP Hubris menetapkan port IP ke tugas secara statis
- Jika suatu tugas mencoba mengutak-atik port IP milik tugas lain, stack IP memberi fault pada tugas tersebut
- Cara ini mengurangi penanganan error “teoretis” yang seharusnya tidak terjadi dalam praktik, dan dengan cepat menyingkap penggunaan yang keliru selama pengembangan
REPLY_FAULTmenjadi sarana bagi server untuk memicupanic!lintas-proses terhadap proses klien, mirip dengan model ketika pelanggaran prasyarat pemanggilan fungsi Rust umumnya menimbulkanpanic!- Klien tidak perlu menyertakan kode atau bekerja sama untuk hal ini
Kecenderungan keamanan dan batasan dalam pengujian
- Eliza Weissman menggambarkan Hubris sebagai “secara agresif bermusuhan terhadap program jahat”
- Upaya eksploitasi sering kali pertama muncul sebagai error API atau penyalahgunaan, sehingga sistem yang menghapus status komponen yang berperilaku salah mungkin lebih sulit dieksploitasi
- Hipotesis ini belum diuji
- Ada permintaan untuk menghubungi mereka jika tertarik mencoba exploit Hubris
- Kekurangan yang diamati adalah sistem ini sangat sulit di-fuzz test
- Tugas chaos kecil yang menghasilkan IPC dan system call acak telah diimplementasikan, tetapi hampir apa pun yang dilakukannya segera di-reset
- Agar bisa bekerja secara berguna, setiap kali mulai ia harus mendasarkan keputusannya pada counter uptime sistem yang berubah secara teramati
REPLY_FAULTjuga menyediakan cara bagi server untuk memaksa chaos dengan membunuh klien secara acak, tetapi opsi ini belum dievaluasi sepenuhnya- Tugas Hubris pada umumnya tidak secara dinamis menghasilkan pesan IPC yang sengaja keliru, sehingga biasanya dapat berjalan tanpa menyadari keberadaan
REPLY_FAULT
1 komentar
Opini Hacker News
REPLY_FAULT tampak bagus ketika sistemnya kecil dan rapat, serta aplikasinya juga sebagian besar ditulis oleh orang-orang yang merancang keseluruhan sistem
Namun dari sudut pandang pengembang aplikasi, menghubungkan kode pihak ketiga lewat model IPC di mana layanan lain bisa kapan saja mengembalikan pil kematian instan ke proses saya terasa cukup menakutkan
Saya tidak memercayai pengembang aplikasi lain sampai sejauh itu. Dunia ini penuh dengan pengemudi buruk dan proses latar belakang yang dibuat oleh pengembang yang tertekan oleh manajer; asal bisa pulang sebelum pukul 8, mereka mungkin saja memasukkan banyak REPLY_FAULT default yang bisa jadi tidak semestinya
Tidak semua prasyarat mudah dipahami, dan bisa berbeda tergantung perangkat atau versi OS
Cakupan memang cenderung membesar, tetapi sepertinya orang tidak akan ingin memaksakan pekerjaan yang lebih baik ditangani di host ke dalam task Hubris di embedded controller
Jika server berkata “klien itu salah”, kernel akan membunuh klien tersebut. Intinya adalah keduanya tidak saling memahami
Selain itu, “proses” di sini mungkin berbeda dari yang biasa dibayangkan. Di Hubris, semua thread berbagi ruang alamat yang sama
Apakah REPLY_FAULT berantai? Misalnya A melakukan
SENDke B dan menunggu, lalu B melakukanSENDke C dan menunggu, jika C melakukanREPLY_FAULT, saya penasaran apakah A juga mati bersama BJika tidak, task jahat cukup mendelegasikan eksperimennya ke task pembantu. Sebaliknya jika ya, secara keseluruhan tampaknya cukup rapuh, meski saya tidak terlalu memahami Hubris
Selain itu, jika
SENDbisa bersifat siklik atau timbal balik, sebuah task bisa saja secara tidak sengaja membunuh dirinya sendiri. Dalam kasus seperti B → A → B, itu mungkin menjadi insentif untuk tidak memakai REPLY_FAULTAlasan server bisa membalas balik klien bukanlah keamanan, melainkan keandalan. Anggapannya, kesalahan muncul dari bug, bukan serangan yang disengaja, dan respons ekstrem kernel membantu pengembang menemukan masalah secepat mungkin
Tentu ada bagian yang tumpang tindih dengan keamanan, dan ini bisa menjadi pertahanan cadangan yang berguna ketika proses mencoba melakukan sesuatu yang tidak seharusnya
Hubris dan debugger-nya, Humility, adalah teknologi yang ingin saya dalami kalau ada waktu atau ada tugas yang mengharuskan. Sayangnya saat ini tidak memungkinkan
Menarik bahwa dalam sistem di mana satu tim menulis semua kodenya, pendekatan menembak klien dari orbit hanya karena ia memandang dengan aneh bisa mempercepat iterasi pengembangan
Lucu juga membaca tulisan ini pada pagi hari setelah tertidur saat membaca tentang efek aljabar. Kalau dipelintir sedikit, ini adalah kernel yang memungkinkan server melakukan efek yang tidak bisa ditangani klien
Reuse dan komposisi kode tampaknya akan jauh lebih sulit, tetapi model eksekusinya jauh lebih sederhana. Untuk sistem embedded yang statis, ini jelas kompromi yang tepat. Jika butuh reuse, task selalu bisa di-vendor lalu dimodifikasi
Justru di Unix ada terlalu banyak error yang bisa diabaikan, dan secara pribadi saya pikir cukup banyak di antaranya seharusnya memicu sinyal fatal. Dengan begitu, kualitas perangkat lunak secara keseluruhan mungkin akan cukup meningkat
Misalnya memanggil
close()pada file descriptor yang salah sering diabaikan karena dianggap error yang tidak fatal. Namun pada kenyataannya itu sangat berbahaya, terutama di aplikasi multithread. Kebanyakan penutupan file descriptor yang salah gagal secara harmless, tetapi 1% menutup socket logging, file lock database, atau koneksi IPC yang tidak terkait. Dari situlah lahir perangkat lunak tidak stabil yang dibenci semua orangSaya teringat dialog di Errand of Mercy: “Kalian akan mengetahui bahwa ada berbagai aturan dan regulasi. Semuanya akan diumumkan. Pelanggaran terhadap yang paling kecil sekalipun dihukum mati”
Ini harus dibuat menjadi RFC April Mop untuk HTTP
Saya mengusulkan HTTP 499 “Shame on you.” Klien yang menerima 499, mungkin hanya untuk permintaan yang diawali dengan header tertentu seperti
Strict: true, harus menghentikan task yang menerbitkan permintaan itu dengan cara yang sesuai bahasanyaIni menyeimbangkan dengan sempurna nuansa “apa-apaan ini… tapi sebenarnya, boleh juga?” yang terlihat dalam konteks seperti ini
Saya membacanya dengan sangat tertarik, dan pendekatan single supervisor ini mirip dengan cara kami dulu menyusun aplikasi di startup lama agar semuanya melakukan unwrap
Ini juga mengingatkan saya pada salah satu tulisan favorit saya, https://medium.com/@mattklein123/crash-early-and-crash-often...
Saya penasaran apakah ini benar-benar terlalu agresif
Di Linux, tidak mungkin membuat program lain yang sedang berkomunikasi hanya lewat soket langsung crash, kecuali dengan mengirim data yang salah ke soket
Namun membunuhnya jelas mungkin. Apa pun yang berjalan sebagai root bisa membunuh yang lain, dan juga bisa me-reboot untuk menjatuhkan seluruh sistem
Ini sedikit lebih sulit dan tidak umum, tetapi setidaknya di container, hak akses root itu umum. Tentu ada cgroup sehingga lebih dibatasi, tetapi intinya begitu
Ini juga agak berbeda dari kebijaksanaan umum “bersikap liberal dalam apa yang diterima, konservatif dalam apa yang dikirim”. Namun itu mungkin lebih terkait dengan sistem jaringan
Meski begitu, mungkin memang tidak terhindarkan bahwa sistem harus toleran terhadap apa yang diterimanya. Kalau tidak, bukankah tidak ada cara untuk sedikit mengubah API tanpa merusak program yang sudah ada?
Setahu saya, ia juga tidak mengizinkan jenis proses baru saat runtime. Semua executable yang mungkin harus ditentukan pada waktu kompilasi
Mengenai bagian “Tidak ada cara untuk memperbaiki masalah dan melanjutkan task. Ini adalah pilihan sadar untuk menghindari mode kegagalan yang halus dan menyederhanakan penalaran sistem”, saya teringat kutipan terkenal Einstein: “Buatlah sesederhana mungkin, tetapi jangan lebih sederhana dari itu”
Desain ini tampaknya melanggar syarat bagian belakangnya. Saya tidak tertarik pada lingkungan operasi yang sama sekali tidak tahan terhadap kekacauan dunia nyata, dan saya juga tidak tahu area yang layak secara komersial yang akan menerimanya
Jadi pada akhirnya dikembalikan ke sistem init agar terus mencoba ulang? Tapi dengan mekanisme apa fault yang terjadi bisa dipahami sehingga bisa dicoba ulang dengan cara yang lebih baik?
Bagaimanapun, saya memberi tepuk tangan untuk kemurnian keyakinannya
Bahkan, seperti yang ditulis Cliff secara rinci di blog, REPLY_FAULT pada awalnya adalah fitur yang kami kira mungkin terlalu agresif, tetapi pengalaman membangun, menerapkan, dan terus terang men-debug sistem memberi kami keyakinan bahwa ini akan membuat sistem kami lebih tangguh, bukan merusaknya secara berubah-ubah
Cara berpikir dan wujud nyatanya di sini bisa dilihat lebih lanjut di [0] dan [1]
[0] https://www.mattkeeter.com/blog/2024-03-25-packing/
[1] https://cliffle.com/blog/who-killed-the-network-switch/
Bahkan di proyek hobi, saya sering melihat bus I2C macet ketika satu bit protokol kacau dan menjatuhkan seluruh sistem, jadi menurut saya desain ini cukup inspiratif
Sejauh pemahaman saya, ini membahas kasus kesalahan yang sudah diketahui, yaitu bukan error yang ditangani, melainkan ketidakcocokan protokol dan hal-hal yang sama sekali tidak boleh terjadi
Seperti juga disinggung komentar lain, ini OS yang dibuat khusus untuk tujuan tertentu. Sama seperti kita tidak akan membuat UI dengan Erlang, Hubris juga tampak cocok dengan ruang yang ditempatinya
Penyebabnya bisa bug, serangan, atau hardware yang rusak, dan dalam kasus mana pun, tidak boleh dilanjutkan. Pemanggilnya punya masalah serius dan jika dilanjutkan hanya akan menimbulkan kerusakan lebih besar
Ini terdengar agak mirip dengan filosofi “let it crash” Erlang/OTP. Erlang dipakai di cukup banyak hardware mission-critical dan terkenal akan reliabilitasnya, jadi dalam praktiknya mungkin ini bukan kekurangan yang sebesar itu
Ia ditulis untuk berjalan jauh di bagian dalam rak server 0xide
Pada bagian “Upaya eksploitasi sering kali pertama kali tampak sebagai error API atau penyalahgunaan, jadi sistem yang menghapus status komponen yang berperilaku salah pada setiap misbehavior seharusnya lebih sulit dieksploitasi”, di sini pada dasarnya aplikasi memeriksa apa yang diterimanya dengan sedikit lebih ketat
Jadi ada keuntungan keamanan, tetapi bukan jenis yang Anda bayangkan. Bukan karena menghancurkan progres penyerang lalu memundurkannya, melainkan karena status-status salah tertentu yang dulu bisa disambung menjadi status salah yang lebih diinginkan kini tidak lagi berfungsi
Maka penyerang akan mencari tempat lain alih-alih mencobanya