- Keamanan memori memang meningkat secara signifikan, tetapi bahkan pada kode Rust production, masalah penanganan batas sistem tetap ada dan bisa berujung pada kerentanan
- Alur yang menafsirkan ulang path yang sama di beberapa syscall, cara mengubah izin setelah membuat file, dan perbandingan path berbasis string mudah menimbulkan masalah seperti TOCTOU dan kebocoran izin
- Di Unix, path, variabel lingkungan, dan data stream berpindah sebagai raw bytes, sehingga pemrosesan yang berpusat pada
String atau penggunaan from_utf8_lossy, unwrap, expect dapat menyebabkan kerusakan data atau DoS
- Jika error dibuang, kegagalan bisa tampak seperti keberhasilan, dan perbedaan perilaku dibanding GNU coreutils juga bisa langsung menjadi masalah keamanan dalam shell script dan alat privileged
- Dalam audit kali ini, tidak ditemukan bug kategori keamanan memori seperti buffer overflow, use-after-free, atau double-free; risiko utama yang tersisa terfokus bukan di dalam Rust, melainkan pada batas yang bersentuhan dengan dunia luar
Batas Rust yang terungkap lewat audit
- 44 CVE uutils yang dipublikasikan Canonical menunjukkan bahwa bahkan pada kode Rust production, masih ada kerentanan yang tidak bisa ditangkap oleh borrow checker, clippy, atau cargo audit
- Pusat masalahnya bukan pada keamanan memori, melainkan pada penanganan batas sistem
- Ada jeda waktu antara path dan syscall
- Data byte Unix dan string UTF-8 tidak selaras
- Ada perbedaan perilaku dengan alat asli
- Ada penanganan error yang terlewat dan penghentian lewat
panic!
- Daftar CVE ini secara padat menunjukkan titik berakhirnya keamanan pada kode sistem Rust
Menafsirkan path dua kali memicu TOCTOU
- Jika path yang sama diperiksa di satu syscall lalu dipakai lagi pada syscall berikutnya, hal itu mudah berujung pada kerentanan TOCTOU
- Di antara dua pemanggilan itu, penyerang yang punya hak tulis ke direktori induk bisa mengubah komponen path menjadi symbolic link
- Saat pemanggilan kedua, kernel menafsirkan ulang path dari awal sehingga operasi privileged bisa diarahkan ke target pilihan penyerang
- API
std::fs Rust pada dasarnya memakai reinterpretasi berbasis &Path, sehingga kesalahan seperti ini mudah terjadi
- Pada
CVE-2026-35355, alur penghapusan file lalu pembuatan file baru di path yang sama dieksploitasi
- Di
src/uu/install/src/install.rs, setelah fs::remove_file(to)? langsung diikuti File::create(to)?
- Jika di antara penghapusan dan pembuatan itu
to diubah menjadi symbolic link yang menunjuk ke target seperti /etc/shadow, proses privileged dapat menimpa file tersebut
- Perbaikannya diubah agar memakai
OpenOptions::create_new(true) sehingga hanya membuat file baru
- Menurut dokumentasinya,
create_new tidak mengizinkan baik file yang sudah ada maupun dangling symlink di lokasi target
- Jika harus melakukan dua operasi pada path yang sama, lebih aman mengikatnya ke file descriptor
- Selain kasus membuat file baru, pendekatan yang tepat adalah membuka direktori induk sekali lalu bekerja dengan path relatif terhadap handle itu
- Jika ada dua operasi pada path yang sama, anggap itu TOCTOU sampai terbukti sebaliknya
Jangan ubah izin setelah dibuat; tentukan saat pembuatan
- Alur membuat direktori atau file dengan izin default lalu menjalankan
chmod belakangan juga menciptakan jendela paparan singkat
- Jika ditulis seperti
fs::create_dir(&path)? lalu fs::set_permissions(&path, Permissions::from_mode(0o700))?, maka selama jeda itu path ada dengan izin default
- Pengguna lain bisa melakukan
open() dalam jendela waktu tersebut, dan meskipun chmod dijalankan setelahnya, file descriptor yang sudah didapat tidak bisa ditarik kembali
- Izin harus ditentukan bersamaan dengan pembuatan
- Gunakan
OpenOptions::mode() dan DirBuilderExt::mode() agar objek lahir dengan izin yang diinginkan
- Kernel tetap akan menerapkan
umask, jadi jika pengaruhnya penting, umask juga perlu ditangani secara eksplisit
Perbandingan string path bukan kesamaan filesystem
- Pemeriksaan awal
--preserve-root pada chmod hanya melakukan perbandingan string
recursive && preserve_root && file == Path::new("/")
- Input yang sebenarnya menunjuk ke root tetapi string-nya bukan
/, seperti /../, /./, /usr/.., atau symbolic link yang mengarah ke /, bisa melewati pemeriksaan ini
- Perbaikannya diubah menjadi membandingkan setelah path diurai ke path absolut sebenarnya dengan
fs::canonicalize
- PR perbaikan
canonicalize mengembalikan path sebenarnya setelah menyelesaikan .., ., dan symbolic link
- Untuk kasus
--preserve-root, cara ini bekerja karena / tidak punya direktori induk
- Untuk membandingkan apakah dua path arbitrer merujuk ke objek filesystem yang sama, secara umum yang harus dibandingkan adalah
(dev, inode), bukan string
- GNU coreutils juga menanganinya dengan cara ini
- Pada
CVE-2026-35363, rm menolak . dan .., tetapi tetap menerima ./ dan .///, sehingga direktori saat ini bisa dihapus
- Jika perbedaan bentuk input hanya ditangani di level string, pemeriksaan mudah disiasati
Di batas Unix, byte lebih penting daripada string
String dan &str di Rust selalu UTF-8, tetapi path, variabel lingkungan, argumen, dan data stream di Unix berada di dunia raw bytes
- Pilihan yang salah saat melintasi batas ini menimbulkan dua jenis bug
- Konversi lossy seperti
from_utf8_lossy diam-diam merusak data dengan mengganti byte yang tidak valid menjadi U+FFFD
- Konversi ketat seperti
unwrap atau ? bisa menolak input atau menghentikan proses
CVE-2026-35346 pada comm adalah kasus keluaran rusak akibat konversi lossy
- Di
src/uu/comm/src/comm.rs, byte input ra, rb diubah menjadi String::from_utf8_lossy lalu dicetak dengan print!
- GNU
comm meneruskan byte apa adanya bahkan untuk file biner, tetapi uutils mengubah UTF-8 yang tidak valid menjadi U+FFFD, sehingga output rusak
- Perbaikannya adalah memakai
BufWriter dan write_all untuk menulis raw bytes langsung ke stdout
print! memaksa round-trip UTF-8 melalui Display, sedangkan Write::write_all tidak
- Pada kode sistem keluarga Unix, tipe harus disesuaikan dengan konteks
- Untuk path file:
Path, PathBuf
- Untuk variabel lingkungan:
OsString
- Untuk isi stream:
Vec<u8> atau &[u8]
- Jika memakai
String sebagai perantara demi kemudahan formatting, kerusakan data mudah menyusup
Setiap panic bisa berujung pada denial of service
- Pada CLI,
unwrap, expect, indexing slice, aritmetika tanpa pemeriksaan, dan from_utf8 dapat menjadi titik DoS ketika penyerang bisa mengendalikan input
panic! akan melakukan stack unwind dan menghentikan proses
- Jika sedang dijalankan oleh cron job, CI pipeline, atau shell script, seluruh pekerjaan bisa terhenti
- Dalam lingkungan eksekusi berulang, crash loop bahkan bisa melumpuhkan seluruh sistem
CVE-2026-35348 pada sort --files0-from berhenti saat menemui nama file non-UTF-8 dalam daftar nama file yang dipisahkan NUL
- Parser memanggil
std::str::from_utf8(bytes).expect(...) untuk setiap byte nama
- GNU
sort memperlakukan nama file sebagai raw bytes seperti kernel, tetapi uutils memaksakan UTF-8 sehingga seluruh proses berhenti pada path non-UTF-8 pertama
- Dalam kode yang memproses input tak tepercaya,
unwrap, expect, indexing, dan cast as harus dipandang sebagai calon CVE
- Gunakan
?, get, checked_*, try_from, dan teruskan error yang nyata ke pemanggil
- Juga diajukan aturan clippy untuk menangkap hal semacam ini di CI
unwrap_used
expect_used
panic
indexing_slicing
arithmetic_side_effects
- Dalam kode pengujian, peringatan seperti ini bisa berlebihan, jadi membatasinya dalam cakupan
cfg(test) adalah pendekatan yang masuk akal
Jika error dibuang, kegagalan bisa terlihat sebagai keberhasilan
- Beberapa CVE muncul karena error diabaikan atau karena informasi error hilang di tengah alur
chmod -R dan chown -R hanya mengembalikan exit code file terakhir dari keseluruhan pekerjaan
- Meski banyak file sebelumnya gagal diproses, jika file terakhir berhasil maka proses bisa berakhir dengan
0
- Script pun salah menyimpulkan bahwa seluruh operasi selesai tanpa masalah
dd memanggil set_len() lalu Result::ok() untuk meniru perilaku GNU pada /dev/null
- Niatnya adalah membuang error pada kondisi terbatas, tetapi kode yang sama juga diterapkan ke file biasa
- Bahkan ketika disk penuh, file tujuan yang hanya setengah tertulis bisa diam-diam tertinggal
- Jika
Result dibuang lewat .ok(), .unwrap_or_default(), atau let _ =, penyebab kegagalan yang penting bisa hilang
- Walaupun tidak berhenti pada kegagalan pertama, program tetap harus mengingat kode error paling serius lalu keluar dengan itu
- Jika memang harus membuang
Result, alasan mengapa aman untuk mengabaikannya harus ditinggalkan di dalam kode
Kompatibilitas yang akurat dengan alat asli juga merupakan fitur keamanan
- Banyak CVE muncul bukan karena kodenya menjalankan operasi berbahaya, tetapi karena berperilaku berbeda dari GNU
- Shell script nyata bergantung pada perilaku GNU asli, sehingga perbedaan makna bisa berkembang menjadi masalah keamanan
CVE-2026-35369 pada kill -1 adalah contoh khas
- GNU membaca
-1 sebagai signal 1 lalu meminta PID
- uutils menafsirkannya sebagai mengirim sinyal default ke PID -1
- Di Linux, PID -1 berarti semua proses yang dapat dilihat, sehingga typo sederhana bisa berubah menjadi kill seluruh sistem
- Pada alat hasil reimplementasi, kompatibilitas bug-for-bug menjadi pengaman yang mencakup exit code, pesan error, edge case, hingga makna opsi
- Di setiap titik tempat perilaku berbeda dari GNU, kemungkinan shell script membuat penilaian yang salah ikut meningkat
- Kini uutils juga menjalankan upstream GNU coreutils test suite di CI
- Itu tampak sebagai tingkat pertahanan yang tepat untuk mencegah jenis perbedaan seperti ini
Harus menafsirkan lebih dulu sebelum melewati batas kepercayaan
CVE-2026-35368 adalah local root code execution pada chroot
- Pola masalahnya adalah setelah
chroot(new_root)?, nama pengguna diurai di dalam root baru yang dikendalikan penyerang
get_user_by_name(name)? akhirnya membaca shared library dari filesystem root baru untuk mengurai nama pengguna
- Jika penyerang menanam file di dalam chroot, hal itu bisa berujung pada eksekusi kode dengan uid 0
- GNU
chroot melakukan penguraian pengguna sebelum chroot
- Perbaikannya juga diubah ke urutan yang sama
- Setelah melewati batas kepercayaan sekali saja, setiap pemanggilan library berpotensi mengeksekusi kode penyerang
- Static linking pun tidak mencegah masalah ini
- Karena
get_user_by_name melewati NSS dan melakukan dlopen modul libnss_* saat runtime
Bug yang benar-benar dicegah Rust
- Ada jenis bug yang jelas-jelas tidak ditemukan dalam audit kali ini
- Tidak ada buffer overflow
- Tidak ada use-after-free
- Tidak ada double-free
- Tidak ada data race pada mutable shared state
- Tidak ada null-pointer dereference
- Tidak ada uninitialized memory read
- Sekalipun alat-alat ini memiliki bug, hasil audit tidak menemukan jenis yang bisa dieksploitasi menjadi pembacaan memori arbitrer
- GNU coreutils dalam beberapa tahun terakhir terus menghasilkan CVE kategori keamanan memori seperti ini
pwd deep path buffer overflow
numfmt out-of-bounds read
unexpand --tabs heap buffer overflow
- penulisan NUL di luar heap buffer pada
od --strings -N
- pembacaan 1 byte sebelum heap buffer pada
sort
- CVE-2024-0684, yaitu heap overwrite pada
split --line-bytes
- unallocated memory read pada input malformed di
b2sum --check
- stack buffer overrun pada
tail -f
- Dalam perbandingan periode yang sama, reimplementasi Rust mempertahankan 0 bug di kategori ini
- Meski begitu, audit tersebut juga memberi catatan bahwa ini bukan bukti tidak adanya bug keamanan memori, hanya saja tidak ditemukan
- Masalah yang tersisa terutama muncul di batas yang bersentuhan dengan dunia luar, bukan di dalam Rust
- path
- byte dan string
- syscall
- jeda waktu dan perubahan status filesystem
Rust yang benar juga merupakan Rust yang idiomatis
- Rust idiomatis bukan sekadar kode yang lolos borrow checker dan membuat
clippy diam
- Ketepatan juga harus menjadi bagian dari idiom
- Karena bentuk kode yang benar-benar bertahan di dunia nyata mengeras lewat pengalaman komunitas
- Sistem yang tangguh harus mencerminkan kekacauan dunia nyata apa adanya, bukan menyembunyikannya
- file descriptor alih-alih path
OsStr alih-alih String
? alih-alih unwrap
- kompatibilitas bug-for-bug dengan alat asli alih-alih makna yang tampak lebih bersih
- Sistem tipe dapat mengekspresikan banyak hal, tetapi tidak bisa menangkap kondisi di luar kendali seperti berlalunya waktu di antara dua syscall
- Rust idiomatis berarti tipe, nama, dan alur kontrol dalam kode harus mengungkapkan kebenaran lingkungan eksekusi
- Bentuk yang mungkin kurang cantik dibanding kode rapi di whiteboard, tetapi lebih jujur, justru dibutuhkan
Referensi
1 komentar
Opini Hacker News
Sebagai maintainer GNU Coreutils, saya membaca artikelnya dengan tertarik, tetapi dari sedikit Rust yang pernah saya pakai, ternyata terlalu mudah membuat TOCTOU race dengan
std::fsSaya berharap API mirip
openatpada akhirnya masuk ke standard libraryDan saya tidak setuju dengan aturan resolve path sebelum membandingkannya
Secara umum, lebih baik memanggil
fstatlalu membandingkanst_devdanst_ino, dan poin itu juga sedikit dibahas di artikelnyaEfek samping yang lebih jarang dipertimbangkan adalah biaya performa
Sebagai contoh nyata, pada path direktori yang sangat dalam,
cpmembutuhkan 0.010 detik sedangkanuu_cpmembutuhkan 12.857 detikMungkin jarang ada orang yang sengaja membuat path seperti itu di dunia nyata, tetapi perangkat lunak GNU berusaha sangat keras untuk menghindari batas yang sewenang-wenang
https://www.gnu.org/prep/standards/standards.html#Semantics
Dan artikel itu mengatakan rewrite Rust memiliki 0 bug keamanan memori selama periode yang kurang lebih sama, tetapi itu tidak benar :)
https://github.com/advisories/GHSA-w9vv-q986-vj7x
Betul,
std::fsmemang punya masalah lowest common denominatorRust 1.0 harus memasukkan sesuatu, dan sayangnya keadaan itu membeku terlalu lama
Saya melihat
uutilssebagai tempat yang bagus untuk mencoba merancang API pengganti std::fs yang lebih sulit disalahgunakanTerima kasih sudah menjelaskan sudut pandang sisi seberang dengan begitu ringkas
Saya ingin bertanya, pelajaran apa yang seharusnya diambil dari sini
Untuk ukuran tulisan internet, saya sengaja bertanya dengan cukup agresif, karena dengan adanya kontras kita bisa melihat perbedaan dan kesalahan dengan lebih jelas
Tentu saja Anda sama sekali tidak berkewajiban menghabiskan waktu atau energi mental untuk menjawab
Saya penasaran kenapa kecepatan, performa, race condition, dan
st_inoterus muncul bersamaTerlihat seolah latensi, penulisan ke media penyimpanan nyata, atomisitas, ACID, dan kecepatan rambat informasi yang terbatas pada akhirnya mengerucut ke hakikat yang mirip
Sistem dengan keandalan tinggi seperti akuntansi tampaknya memang pada akhirnya harus menuju ACID, dan sistem dengan keandalan rendah terasa cepat dilupakan sampai perbedaan antar komputer pun terasa tidak terlalu besar
Saya juga penasaran apakah pada aplikasi sehari-hari throughput benar-benar lebih penting daripada latency
Dan saya paham kenapa, karena sejarah C, OS keluarga Unix, dan GNU coreutils, fokusnya jatuh pada nomor inode,
tetapi saya penasaran bagaimana kalau melihat contoh yang sangat dasar seperti sekadar membuat USB drive benar-benar berfungsi dengan baik untuk menyimpan file
tanpa menghindari kompleksitas seperti buffering I/O
libc,fflush, buffering kernel, multi-core, time-sharing, dan beberapa aplikasi yang berjalan bersamaanSaya benar-benar pemula, jadi saya penasaran kenapa perlu loop
whilealih-alih langsungcddengan$(yes a/ | head -n $((32 * 1024)) | tr -d '\n')Edit: saya mengerti. Karena
-bash: cd: a/a/a/....../a/a/: File name too longEntah Anda sudah melihatnya atau belum, ada demo untuk mengonversi otomatis utilitas GNU seperti
wgetke subset C++ yang aman terhadap memorihttps://duneroadrunner.github.io/scpp_articles/PoC_autotranslation_of_wget
Caranya hampir berupa penggantian 1:1 antara elemen C berbahaya dan elemen C++ aman dengan perilaku yang bersesuaian, jadi tampaknya lebih kecil kemungkinan memasukkan bug baru dan perbedaan perilaku baru dibanding rewrite
Jika kode sumber aslinya dirapikan sedikit, transformasinya bisa sepenuhnya diotomatisasi, sehingga pada tahap build bisa dibuat executable yang sedikit lebih lambat tetapi aman terhadap memori dari sumber C asli
Mungkin ini pertanyaan bodoh, tetapi saya penasaran apakah pihak GNU Coreutils sedang mempertimbangkan atau merencanakan rewrite Rust mereka sendiri
Mereka mungkin tahu cara memakai Rust, tetapi tidak cukup akrab dengan API Unix beserta semantik dan jebakannya
Sebagian besar kesalahan itu, dari sudut pandang pengembang lama GNU coreutils atau BSD, Solaris, tergolong cukup pemula
Banyak masalah semacam itu sudah terungkap dan dibereskan puluhan tahun lalu, dan walau di codebase lama masih ada ekor panjang perbaikan sampai sekarang, kini umumnya hanya sedikit yang terus masuk
Saya benar-benar tercengang membaca thread Canonical itu
Intinya kira-kira, “Rust lebih aman, keamanan adalah prioritas utama, jadi rollout rewrite seluruh coreutils itu mendesak. Kalau ada yang rusak tidak apa-apa, nanti diperbaiki.”
Saya tidak ingin menjalankan kode buatan orang yang berpikir seperti itu di mesin saya
Saya juga pro-Rust, tetapi bahwa Rust lebih aman itu hanya berlaku jika faktor lain setara
Di sini faktor lain sama sekali tidak setara
Rewrite pada akhirnya pasti memiliki jauh lebih banyak bug dan kerentanan dibanding kode yang sudah dikelola puluhan tahun, jadi argumen keamanan mungkin masuk akal untuk strategi transisi jangka panjang, tetapi tidak bisa dijadikan dasar rollout yang terburu-buru
Sikap yang mengecilkan dampak ke pengguna setelah rilis, atau berkata “bug memang akan kelihatan kalau begini”, “coreutils lama juga tidak punya pengujian yang layak”, itu terlalu tidak bertanggung jawab
Pengguna bukan kelinci percobaan
Saya rasa maintainer punya tanggung jawab moral untuk tidak merusak keandalan sistem pengguna
Lebih mendasarnya lagi, standard library Rust tampak mendorong developer ke API yang rapi tetapi berada di tingkat abstraksi yang salah
Misalnya ke operasi file berbasis path alih-alih berbasis handle
Saya berharap saya salah
Menurut saya inti Rust adalah membuat jebakan terbesar dan paling mudah dimasuki jadi tidak perlu terlalu dipikirkan
Tampaknya inti artikel ini pada akhirnya adalah bahwa API filesystem juga harus melakukan hal itu
Seseorang pernah membuat istilah disassembler rage untuk menggambarkan hal seperti ini
Maksudnya, kalau diperhatikan dari cukup dekat, semua kesalahan akan terlihat seperti kesalahan amatir
Istilah itu juga muncul dari sikap menatap disassembler lalu memarahi programmer tingkat tinggi karena memakai
ifalih-alihswitchdi fungsi yang berada 100 frame call stack di bawahSekarang kita hanya melihat beberapa hal yang salah dari mereka, dan hampir tidak melihat ribuan baris kode yang ditulis dengan benar di sekitarnya
Terjadinya
panicpada utilitas seperti ini, bahkan menurut standar Rust sekalipun, adalah kesalahan yang cukup amatirKalau itu error alokasi yang tak bisa ditangani mungkin lain cerita, tetapi
expectdanunwrapsulit dibela kecuali ada invariant yang benar-benar dijaga ketat sehingga jalur kode itu pasti tidak akan pernah dieksekusiSalah satu hal sulit dalam me-rewrite kode adalah bahwa kode asli telah berubah secara bertahap sambil menanggapi masalah yang hanya muncul di lingkungan operasi nyata
Pelajaran yang diperoleh dalam proses itu diam-diam meresap ke dalam kode, dan jika tidak terdokumentasi, ada sangat banyak pekerjaan tersembunyi yang harus dilakukan sebelum mencapai tingkat kesetaraan yang sama
Tulisan aslinya menunjukkan daftar jenis hal seperti itu dengan sangat baik
Namun sebelum langsung menyebutnya amatir, kita juga perlu melihat bahwa ini adalah salah satu gejala paling “software banget” dalam software
Kecuali mereka mengabaikan dokumentasi teknis yang sangat bagus dan test untuk kasus-kasus itu di coreutils, kejadian seperti ini hampir tak terelakkan
Contoh bagus dari artikel itu adalah chroot + NSS CVE
Fakta bahwa NSS itu dinamis, dan bahwa di dalam
chrootia akandlopenlibrary, tidak tertulis secara mencolok di mana punItu lebih mirip pengetahuan yang dipelajari para sysadmin lewat pengalaman pahit selama lebih dari 25 tahun, dan rewrite clean-room biasanya mempelajarinya ulang sebagai CVE baru
Kalau kode yang sama di-porting dengan LLM pun situasinya serupa
Signature fungsi bisa dibaca, tetapi yang benar-benar dibutuhkan adalah luka dan bekas luka yang tertinggal di kode itu
Kalau pekerjaan ini dilakukan sambil menghindari GPL dan tanpa membaca source asli, itu jadi lebih sulit lagi
Menurut saya
uutilsakan jauh lebih baik kalau berlisensi GPL dan boleh mengambil inspirasi langsung dari source asli coreutilsPerlu ditegaskan juga bahwa tidak mendokumentasikan pelajaran seperti ini, atau setidaknya bug dan kerentanan yang berusaha dihindari, adalah praktik yang buruk
Tentu sulit mendokumentasikan semua bug yang secara implisit dihindari hanya karena kodenya ditulis dengan baik sejak awal,
tetapi bagi pembaca di masa depan, penting untuk meninggalkan penjelasan seperti “alasan memakai
foodi sini alih-alihbaradalah karena jikabardipakai dalam kondisi ABC maka akan munculbazberbahaya akibat XYZ”Menurut saya itu lebih baik, meskipun tampak seperti sedikit pemborosan waktu dan ruang dokumentasi
Banyak hal yang ditunjuk artikel ini terasa, terutama jika dibandingkan dengan source GNU coreutils, seharusnya bisa tertangkap oleh unit test atau review manual yang lumayan
Rewrite coreutils tampak seperti ide yang buruk
https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/
dan sepertinya dilakukan dengan cara yang salah, tanpa cukup membawa pengetahuan yang telah dikumpulkan perangkat lunak sebelumnya
Kalau ingin me-rewrite, Anda harus benar-benar memahami dan belajar dari pendahulunya
Kalau tidak, Anda akan mengulang kesalahan yang sama, dan terus terang itu cukup memalukan
Biar jelas, saya suka Rust, memakainya di beberapa proyek, dan Rust itu hebat
Hanya saja Rust tidak akan menyelamatkan rekayasa yang buruk
Menariknya,
uutilsmemang memakai test suite GNU coreutilsTambahan lagi, mereka juga sudah menyatakan bahwa kontribusi yang ditulis setelah membaca source GPL tidak akan diterima
Dari pihak yang membuat
unity,upstart, dansnap, hal seperti ini justru masih dalam kisaran yang bisa diperkirakanMungkin ini sambutan yang perlu diberikan kepada programmer sistem baru
Unix itu rusak, dan pada akhirnya Anda harus menulis solusi jelek yang tidak indah dan tidak mendidik sendiri, serta melakukan pengujian empiris
Begitulah cara kerja perangkat lunak yang dapat dipercaya dan rekayasa perangkat lunak yang baik
Saya penasaran kenapa differential fuzzing tidak menangkap bug-bug seperti ini
https://github.com/uutils/coreutils/tree/main/fuzz/uufuzz
Pola memeriksa sebuah path lewat syscall sekali, lalu melakukan syscall lagi pada path yang sama untuk bekerja dengannya, selalu memanggil masalah yang sama
Penyerang yang punya izin tulis pada direktori induk bisa menukar komponen path itu dengan symbolic link di sela-selanya, dan kernel akan me-resolve ulang path dari awal pada panggilan kedua, sehingga operasi berhak istimewa itu diarahkan ke target yang dipilih penyerang
Penyerang yang punya izin tulis pada direktori induk juga bisa bermain-main dengan hard link
Bahkan kalau Anda hanya bisa menyentuh file reguler, pada praktiknya hampir tidak ada mitigasi yang layak
Untuk contohnya lihat https://michael.orlitzky.com/articles/posix_hardlink_heartache.xhtml
Akar penyebab beberapa bug ini tampaknya adalah API Unix yang terlalu tidak transparan
Misalnya fakta bahwa
get_user_by_namememuat shared library di dalam root filesystem baru untuk menyelesaikan nama pengguna, dan karenanya penyerang yang bisa menanam file di dalamchrootdapat mengeksekusi kode sebagai uid 0, terasa hampir seperti jebakan booby trapFungsi untuk mengambil data pengguna tiba-tiba juga memuat shared library tampak seperti desain yang mencampuradukkan concern
Menurut saya pengambilan data pengguna dan pemuatan library seharusnya dipisahkan di tingkat fungsi, atau setidaknya perilaku semacam itu harus terlihat jelas dari namanya
Sebagiannya mungkin benar, tetapi kalau Anda memutuskan menulis ulang coreutils dari nol, maka memahami API POSIX itu sendiri secara harfiah adalah pekerjaan inti
Lagi pula, kalau kode yang memeriksa apakah suatu path menunjuk ke root filesystem adalah
file == Path::new("/"), itu bukan masalah APIOrang yang menulis seperti itu rasanya nyaris tidak layak ikut proyek ini
Saya malah merasa memakai bahasa aman fungsional bisa membuat orang mengira data yang mereka tangani juga stateless
Padahal dalam sistem operasi, sangat banyak hal yang terus berubah
Sampai filesystem yang menyediakan snapshot menjadi umum, semua hal harus terus diperiksa ulang
Pada akhirnya yang dibutuhkan adalah API yang ketika diberi input hanya mengembalikan hasil sukses atau gagal, bukan API yang memberi tiga kemungkinan: sukses, gagal, atau error
Betul,
musl libcmemang menghilangkan tepat satu bagian seperti ituMenurut saya akar masalahnya bukan ketidaktransparanan API Unix, melainkan tidak benar-benar memikirkan situasi ketika root melakukan chroot ke direktori yang tidak ia kendalikan
Apa pun yang menjadi target
chrootberada di bawah kendali pihak yang membuat chroot itu, dan kalau Anda tidak memahami itu, Anda tidak layak memakaichroot()get_user_by_namemungkin memang terasa seperti jebakan, tetapi sebenarnya hampir tidak ada perbedaan nyata antara memakainewroot/etc/passwddan memakainewroot/usr/lib/x86_64-linux-gnu/libnss_compat.so,newroot/bin/sh, dan semacamnyaKarena itu saya rasa
/usr/sbin/chrootseharusnya sejak awal tidak punya alasan untuk mencari user IDtoybox chrootjuga tidak melakukannyaJadi bug-nya bukan pada cara melakukan sesuatu yang salah, melainkan pada fakta bahwa pekerjaan itu dilakukan sejak awal
Unix dan POSIX itu seperti fraktal; dipotong di mana pun tetap penuh jebakan
Bahkan kalau orang-orang Rust itu menulis ulang coreutils tanpa pengalaman Linux, saya justru lebih tidak paham bagaimana Ubuntu bisa menerimanya ke mainline
Ubuntu seolah punya kebijakan mengganti satu komponen fondasi sistem di hampir setiap rilis dengan eksperimen setengah matang dan belum selesai
Menurut saya intinya di sini bukan “wah, ternyata ada bug di kode Rust”, melainkan justru itu
Yang asli berlisensi GPL, dan versi rewrite berlisensi MIT
Jika pernyataan “bug-bug ini muncul di kode Rust yang benar-benar dirilis, dan penulisnya juga orang-orang yang tahu apa yang mereka lakukan” itu benar,
saya jadi penasaran apakah itu berarti utilitas aslinya juga tidak punya test harness, dan rewrite-nya juga tidak dimulai dengan membuat itu lebih dulu
Walaupun edge case-nya banyak, rasanya OS dan FS masih bisa diabstraksikan sampai tingkat tertentu untuk memverifikasi hal-hal seperti apakah
rm .//benar-benar tidak menghapus direktori saat ini seperti yang diharapkanIni tampak bukan soal coding yang berantakan atau kritik pada bahasa, melainkan sekali lagi sikap lama bahwa pemrograman sistem tidak dites
Sebaliknya, kalau utilitas aslinya memang punya test tetapi tetap ada banyak celah seperti ini, mungkin test suite aslinya sendiri sangat kurang
Saya kira begitu
Tetapi saya tidak terlalu yakin bahwa OS dan FS bisa cukup diabstraksikan untuk diverifikasi sejauh itu
Alasannya, orang-orang sudah mencoba melakukannya sejak sebelum saya lahir dan tampaknya masih belum berhasil
Misalnya, sejak awal saja tidak jelas bagaimana menentukan berapa banyak
/yang perlu ditempelkan untuk diujiLebih jauh lagi, misalkan
rmmenolak menghapus jika 9 byte pertama file adalahimportant,membayangkan bagaimana membuat test yang bisa menemukan perilaku seperti itu tanpa terlebih dahulu mengetahui string tersebut terasa sulit
Bahkan akan lebih sulit lagi kalau kata ajaib itu adalah string yang tidak ada di kamus
Saya hampir tidak pernah melihat orang yang dengan serius berkata “pemrograman sistem tidak dites”
Yang lebih sering saya dengar adalah bahwa test tidak selalu melakukan peran yang orang harapkan
Sejauh yang saya pahami, dalam proses pengembangan
uutilsmemang ada pengujian perbandingan perilaku yang luas terhadap utilitas asli, dan mereka bahkan berusaha mempertahankan bug-bugnya jugaIni salah satu alasan kenapa Windows secara default menonaktifkan symlink
Bukan menyelesaikannya lewat abstraksi, melainkan dengan secara efektif menghapus fitur itu sendiri
Di dunia Unix, sudah terlalu banyak perangkat lunak yang bergantung pada symlink selama puluhan tahun sehingga hal seperti itu tidak bisa dilakukan
MacOS juga punya respons yang mirip
Misalnya bug
chroot()secara default jarang benar-benar jadi masalah, karena MacOS pada dasarnya memblokirchroot()secara defaultUntuk memakainya Anda harus mematikan system integrity protection
Masalah dasarnya ada pada sudut tajam API POSIX, dan solusinya lebih mendekati menghapusnya daripada mengabstraksikannya
Menurut saya tidak masalah kalau orang bereksperimen dan mencoba dengan canggung
Memang begitulah cara belajar dan berkembang
Yang benar-benar membuat saya penasaran adalah bagaimana rantai pengambilan keputusan di Ubuntu bisa rusak sampai hal seperti ini masuk ke produksi