Rust'ın Yakalayamadığı Hatalar
(corrode.dev)- Bellek güvenliği büyük ölçüde iyileşse de, Rust production kodunda da sistem sınırı işleme sorunları aynen kalabiliyor ve zafiyetlere yol açabiliyor
- Aynı yolun birden fazla syscall'da yeniden yorumlanması, oluşturduktan sonra izin değiştirme yaklaşımı ve string tabanlı yol karşılaştırması, TOCTOU ve izin sızıntısı gibi sorunlar üretmeye çok elverişli
- Unix'te yol, ortam değişkeni ve akış verisi ham baytlar olarak dolaştığı için,
Stringmerkezli işleme ya dafrom_utf8_lossy,unwrap,expectkullanımı veri bozulmasına veya DoS'a yol açabiliyor - Hatalar yok sayıldığında başarısızlık başarı gibi görünebilir; GNU coreutils ile olan davranış farkları da shell script'lerde ve privileged araçlarda doğrudan güvenlik sorunlarına dönüşebilir
- Bu denetimde buffer overflow, use-after-free, double-free gibi bellek güvenliği sınıfındaki hatalar görülmedi; geriye kalan temel riskler Rust'ın içinden çok dış dünyayla temas eden sınırlarda yoğunlaşıyordu
Denetimin Ortaya Koyduğu Rust Sınırları
- Canonical'ın açıkladığı uutils'e ait 44 CVE, Rust production kodunda da borrow checker, clippy ve cargo audit'in yakalayamadığı zafiyetlerin kalabileceğini gösteriyor
- Sorunların odağı bellek güvenliğinden çok sistem sınırı işlemeydi
- Yol ile syscall arasında zaman farkı vardı
- Unix bayt verisi ile UTF-8 string'ler arasında uyumsuzluk vardı
- Orijinal araçla davranış farkları vardı
- Eksik hata işleme ve
panic!ile sonlanma vardı
- Bu CVE listesi, Rust sistem kodunda güvenliğin bittiği noktayı yoğun biçimde gösteriyor
Bir Yolu İki Kez Yorumlamak TOCTOU Üretir
- Aynı yolu bir syscall'da kontrol edip sonraki syscall'da yeniden kullanmak, kolayca TOCTOU zafiyetine yol açabilir
- İki çağrı arasında üst dizine yazma izni olan bir saldırgan, yol bileşenini bir symbolic link ile değiştirebilir
- İkinci çağrıda kernel yolu baştan yeniden yorumlarken ayrıcalıklı işlem, saldırganın seçtiği hedefe yönlendirilebilir
- Rust'ın
std::fsAPI'si, varsayılan olarak&Pathtabanlı yeniden yorumlama kullandığı için bu tür hataları yapmayı kolaylaştırıyorfs::metadata,File::create,fs::remove_file,fs::set_permissionsher çağrıda yolu yeniden yorumlar- Yerel saldırganlara karşı koruma gerektiren privileged araçlarda bu varsayılan yol tehlikeli hale gelir
CVE-2026-35355te, bir dosyanın silinmesinin ardından aynı yol üzerinde yeni dosya oluşturulması istismar edildisrc/uu/install/src/install.rsiçindefs::remove_file(to)?sonrasındaFile::create(to)?geliyordu- Silme ile oluşturma arasında
to,/etc/shadowgibi bir hedefi gösteren symbolic link'e çevrilirse ayrıcalıklı süreç o dosyanın üstüne yazabilir
- Düzeltmede
OpenOptions::create_new(true)kullanılarak yalnızca yeni dosya oluşturmaya geçildi- Belgede
create_new, hedef konumda yalnızca mevcut dosyayı değil dangling symlink'i de kabul etmiyor
- Belgede
- Aynı yol üzerinde iki kez işlem yapmak gerekiyorsa, dosya tanımlayıcısına sabitlemek daha güvenlidir
- Yeni dosya oluşturma dışındaki durumlarda, önce üst dizini açıp işlemleri o handle'a göre göreli yolla yapmak daha doğrudur
- Aynı yol üzerinde iki kez işlem yapılıyorsa, aksi kanıtlanana kadar bunun TOCTOU olduğu varsayılmalıdır
İzinler Sonradan Değil, Oluşturma Anında Belirlenmeli
- Dizin ya da dosyayı varsayılan izinlerle oluşturup sonra
chmoduygulamak da kısa bir maruz kalma penceresi yaratırfs::create_dir(&path)?ardındanfs::set_permissions(&path, Permissions::from_mode(0o700))?yazılırsa, aradaki süredepathvarsayılan izinlerle var olur- Diğer kullanıcılar bu aralıkta
open()çağırabilir ve sonradanchmodyapılsa bile önceden alınmış dosya tanımlayıcıları geri alınamaz
- İzinler oluşturma anında birlikte belirtilmelidir
OpenOptions::mode()veDirBuilderExt::mode()kullanılarak hedef izinlerle oluşturulmalıdır- Kernel burada ek olarak
umaskuygular; bunun etkisi önemliyseumaskda açıkça ele alınmalıdır
Yol String'lerini Karşılaştırmak Dosya Sistemi Eşdeğerliği Değildir
chmodiçin ilk--preserve-rootkontrolü yalnızca string karşılaştırması yapıyordurecursive && preserve_root && file == Path::new("/")- Gerçekte kökü gösterdiği halde string olarak
/olmayan/../,/./,/usr/..ya da/i gösteren symbolic link gibi girdiler bu kontrolü aşabiliyordu
- Düzeltme, yolu
fs::canonicalizeile gerçek mutlak yola çözüp sonra karşılaştırma yapacak şekilde değiştirildi- Düzeltme PR'ı
canonicalize,..,.ve symbolic link'leri çözüp gerçek yolu döndürür
--preserve-rootdurumunda/için üst dizin olmadığından bu yaklaşım işe yarar- Genel olarak iki rastgele yolun aynı dosya sistemi nesnesi olup olmadığını karşılaştırmak için string değil
(dev, inode)karşılaştırılmalıdır- GNU coreutils de bunu kullanır
CVE-2026-35363term,.ve..'yi reddederken./ve.///girdilerine izin verdiği için mevcut dizini silebiliyordu- Girdi biçimi farklarını yalnızca string düzeyinde ele almak, kontrollerin kolayca aşılmasına yol açar
Unix Sınırlarında String'den Çok Baytları Öncelemek Gerekir
- Rust'taki
Stringve&strher zaman UTF-8'dir; ama Unix'te yol, ortam değişkeni, argüman ve akış verisi ham baytlar dünyasında yaşar - Bu sınırı geçerken yapılan yanlış seçimler iki tür hataya yol açar
from_utf8_lossygibi kayıplı dönüşümler, geçersiz baytlarıU+FFFDile değiştirip veriyi sessizce bozarunwrapya da?gibi katı dönüşümler, girdiyi reddedebilir veya süreci sonlandırabilir
commiçinCVE-2026-35346, kayıplı dönüşüm yüzünden çıktının bozulduğu bir durumdusrc/uu/comm/src/comm.rsiçinde girdi baytlarıra,rb,String::from_utf8_lossyile çevrilipprint!ile yazdırılıyordu- GNU
comm, ikili dosyalarda da baytları olduğu gibi taşırken uutils geçersiz UTF-8'iU+FFFD'ye dönüştürerek çıktıyı bozuyordu - Düzeltme,
BufWritervewrite_allile ham baytları doğrudanstdouta yazmaktı
print!,Displayüzerinden geçerek UTF-8 gidiş-dönüşünü zorunlu kılar; amaWrite::write_allbunu yapmaz- Unix benzeri sistem kodunda, duruma uygun tipler kullanılmalıdır
- Formatlama kolaylığı için
Stringüzerinden gitmek, veri bozulmasını kod tabanına sızdırmayı kolaylaştırır
Her panic Bir Hizmet Reddi Saldırısına Dönüşebilir
- CLI'da
unwrap,expect, dilim indeksleme, kontrolsüz aritmetik vefrom_utf8, saldırganın girdiyi kontrol edebildiği durumlarda DoS noktası olabilirpanic!, stack'i unwind eder ve süreci durdurur- cron job, CI pipeline ya da shell script içinde çalışıyorsa tüm iş akışı durabilir
- Tekrarlı çalışma ortamlarında crash loop ile tüm sistemi felç etmek bile mümkün olabilir
sort --files0-fromiçinCVE-2026-35348, NUL ile ayrılmış dosya adı listesinde UTF-8 olmayan bir dosya adı görünce duruyordu- Ayrıştırıcı, her ad baytı için
std::str::from_utf8(bytes).expect(...)çağırıyordu - GNU
sort, dosya adlarını kernel gibi ham bayt olarak ele alırken uutils UTF-8'i zorlayıp ilk UTF-8 dışı yolda tüm süreci sonlandırıyordu
- Ayrıştırıcı, her ad baytı için
- Güvenilmeyen girdiyi işleyen kodda
unwrap,expect, indeksleme veascast, potansiyel CVE olarak görülmelidir?,get,checked_*,try_fromkullanılmalı ve gerçek hata çağırana iletilmelidir
- CI'da yakalamak için önerilen clippy kuralları da veriliyor
unwrap_usedexpect_usedpanicindexing_slicingarithmetic_side_effects
- Test kodunda bu uyarılar aşırı gelebileceğinden, bunları
cfg(test)kapsamıyla sınırlamak uygun bir yaklaşım olabilir
Hataları Yok Saymak Başarısızlığı Başarı Gibi Gösterebilir
- Bazı CVE'ler, hataların yok sayılması ya da hata bilgisinin kaybolduğu akışlar yüzünden ortaya çıktı
chmod -Rvechown -R, tüm işlem boyunca yalnızca son dosyanın çıkış kodunu döndürüyordu- Önceki çok sayıdaki dosya işlemi başarısız olsa da son dosya başarılıysa sonuç
0olabiliyordu - Script'ler de tüm işlemin sorunsuz bittiğini sanabiliyordu
- Önceki çok sayıdaki dosya işlemi başarısız olsa da son dosya başarılıysa sonuç
dd,/dev/nullüzerindeki GNU davranışını taklit etmek içinset_len()sonucundaResult::ok()çağırıyordu- Niyet, sınırlı bir durumda hatayı yok saymaktı; ama aynı kod normal dosyalara da uygulanıyordu
- Disk dolu olduğunda bile yarım yazılmış hedef dosya sessizce kalabiliyordu
.ok(),.unwrap_or_default(),let _ =ileResultatıldığında önemli başarısızlık nedenleri kaybolur- İlk hatada hemen durulmasa bile, en ciddi hata kodu hatırlanmalı ve onunla çıkılmalıdır
Resultmutlaka atılacaksa, o hatanın neden güvenle yok sayılabildiği kod içinde belirtilmelidir
Orijinal Araçla Tam Uyumluluk da Bir Güvenlik Özelliğidir
- Birçok CVE, kodun tehlikeli işlem yapmasından değil GNU'dan farklı davranmasından kaynaklandı
- Gerçek shell script'ler orijinal GNU davranışına bağımlı olduğundan, anlam farkı güvenlik sorununa dönüşebiliyor
kill -1içinCVE-2026-35369bunun tipik örneği- GNU,
-1ifadesini signal 1 olarak yorumlar ve PID bekler - uutils ise bunu PID -1'e varsayılan sinyal gönderme olarak yorumladı
- Linux'ta PID -1, görülebilen tüm süreçler anlamına geldiğinden basit bir yazım hatası tüm sistemi kill etmeye dönüşebilir
- GNU,
- Yeniden uygulanan araçlarda bug-for-bug uyumluluk, çıkış kodundan hata mesajına, edge case'lerden seçenek anlamlarına kadar bir güvenlik önlemi haline gelir
- GNU ile farklı davranılan her noktada, shell script'lerin yanlış karar verme olasılığı artar
- uutils artık CI'da upstream GNU coreutils test suite'ini de birlikte çalıştırıyor
- Bu tür farkları önlemek için uygun büyüklükte bir savunma gibi görünüyor
Güven Sınırı Aşılmadan Önce Çözümleme Yapılmalı
CVE-2026-35368,chrootiçinde local root code execution açığıydı- Sorun deseni,
chroot(new_root)?sonrasında saldırganın kontrol ettiği yeni kökün içinde kullanıcı adının çözülmesiydiget_user_by_name(name)?, yeni kök dosya sistemindeki paylaşılan kütüphaneleri okuyarak kullanıcı adını çözmeye çalışıyordu- Saldırgan chroot içine dosya yerleştirebilirse bu, uid 0 kod çalıştırmaya dönüşebiliyordu
- GNU
chroot, kullanıcı çözümlemesinichrootöncesinde yapıyor- Düzeltme de aynı sıraya geçirildi
- Bir kez güven sınırı aşıldıktan sonra, yapılan her kütüphane çağrısı saldırgan kodunu çalıştırabilir
- Statik bağlantı da bu sorunu önlemez
- Çünkü
get_user_by_name, NSS üzerinden çalışma anındalibnss_*modüllerinidlopenile yükleyebilir
- Çünkü
Rust'ın Gerçekten Engellediği Hatalar
- Bu denetimde bulunmayan hata türleri de açıkça ortada
- buffer overflow yoktu
- use-after-free yoktu
- double-free yoktu
- Paylaşılan değişebilir durumdan kaynaklı data race yoktu
- null-pointer dereference yoktu
- uninitialized memory read yoktu
- Araçlarda hatalar olsa bile, bunların keyfi bellek okumasına dönüştürülebilecek türleri denetim sonucunda görülmedi
- GNU coreutils son yıllarda bu tür bellek güvenliği sınıfı CVE'ler üretmeye devam etti
pwddeep path buffer overflownumfmtout-of-bounds readunexpand --tabsheap buffer overflowod --strings -Nheap buffer dışına NUL yazımısortiçin heap buffer öncesinden 1 bayt okumasplit --line-bytesiçin heap overwrite olan CVE-2024-0684b2sum --checkiçin malformed input durumunda ayrılmamış bellekten okumatail -fiçin stack buffer overrun
- Aynı dönem karşılaştırmasında Rust ile yeniden yazılan sürüm, bu kategorilerde 0 hata düzeyini korudu
- Yine de denetimin, bellek güvenliği hatalarının yokluğunu kanıtlamadığı; yalnızca bunları bulamadığı notu da düşülüyor
- Geriye kalan sorunlar, Rust'ın içinden çok dış dünyayla temas eden sınırlarda ortaya çıkıyor
- Yollar
- Baytlar ve string'ler
- syscall'lar
- Zaman farkı ve dosya sistemi durumu değişimleri
Doğru Rust, Aynı Zamanda İdiomatic Rust'tır
- İdiomatic Rust, yalnızca borrow checker'dan geçen ve
clippyuyarısı vermeyen kod demek değildir - Doğruluk da idiomatikliğin bir parçası olmalıdır
- Çünkü gerçek dünyada ayakta kalan kod kalıpları, topluluk deneyimiyle yerleşmiştir
- Sağlam sistemler, gerçeğin dağınıklığını gizlemek yerine olduğu gibi yansıtmalıdır
- Yol yerine dosya tanımlayıcısı
StringyerineOsStrunwrapyerine?- Daha temiz görünen anlamlar yerine orijinalle bug-for-bug uyumluluk
- Tip sistemi çok şeyi ifade edebilir; ama iki syscall arasındaki zaman geçişi gibi denetim dışı koşulları içine alamaz
- İdiomatic Rust'ta kodun tipleri, isimleri ve kontrol akışı çalışma ortamının gerçeğini ortaya koymalıdır
- Tahtada güzel görünen koddan daha az estetik olsa bile, daha dürüst bir biçime ihtiyaç vardır
Kaynaklar
- An update on rust-coreutils: denetim sonuçlarının açıklanması
- Patterns for Defensive Programming in Rust: birlikte okunabilecek savunmacı Rust kalıpları
- Pitfalls of Safe Rust: safe Rust'ta da görülebilen yaygın hatalar
- Sharp Edges In The Rust Standard Library:
stdiçindeki şaşırtıcı davranışlar - uutils/coreutils on GitHub: GNU coreutils'in Rust ile yeniden uygulanmış hali
1 yorum
Hacker News görüşleri
GNU Coreutils bakımcısı olarak yazıyı ilgiyle okudum, ancak biraz kullandığım Rust'ta
std::fsile TOCTOU race oluşturmak fazlasıyla kolaydıUmarım
openatbenzeri bir API en sonunda standart kütüphaneye girerAyrıca yolları karşılaştırmadan önce resolve et kuralına katılmıyorum
Genelde
fstatçağırıpst_devilest_inodeğerlerini karşılaştırmak daha iyi olur ve yazıda da buna bir miktar değinilmiştiDaha az dikkate alınan yan etki ise performans maliyeti
Gerçek bir örnekte çok derin bir dizin yolunda
cp0.010 saniye sürerkenuu_cp12.857 saniye sürdüGerçek hayatta böyle yolları bilerek oluşturmak nadirdir, ama GNU yazılımı keyfi sınırları önlemek için çok ciddi çaba harcar
https://www.gnu.org/prep/standards/standards.html#Semantics
Ayrıca yazıda Rust yeniden yazımında benzer bir zaman aralığında bellek güvenliği hatası sayısının 0 olduğu söyleniyordu, ama bu doğru değil :)
https://github.com/advisories/GHSA-w9vv-q986-vj7x
Evet,
std::fsbir lowest common denominator sorunu taşıyorRust 1.0 için bir şeyler koymak gerekiyordu ve ne yazık ki bu durum uzun süre kalıcılaştı
uutils'in, hata yapması daha zor olan bir std::fs alternatif API tasarlamayı denemek için iyi bir yer olduğunu düşünüyorumKarşı taraftan bu bakış açısını bu kadar özlü anlattığın için teşekkürler
Buradan ne öğrenmemiz gerektiğini sormak istiyorum
Bir internet gönderisi için bilerek biraz saldırgan soruyorum, çünkü karşıtlık olduğunda farkları ve hataları daha net görmek mümkün oluyor
Elbette zamanını ya da zihinsel enerjini harcamak gibi bir yükümlülüğün yok
Neden sürekli hız, performans, race condition ve
st_inobirlikte geliyor, bunu merak ediyorumGecikme, gerçek depolamaya yazma işi, atomiklik, ACID, sonlu bilgi aktarım hızı gibi şeyler sonuçta benzer bir özde birleşiyor gibi görünüyor
Muhasebe gibi güvenilirliği yüksek sistemler sonunda ACID'e gitmek zorunda gibi, düşük güvenilirlikli sistemler ise fazla hızlı unutulduğu için bilgisayarların farkı büyük değilmiş gibi hissedilebiliyor
Ayrıca gündelik uygulamalarda throughput'un gerçekten latency'den daha önemli olup olmadığını da merak ediyorum
Bir de C, Unix türevi işletim sistemleri ve GNU coreutils tarihi nedeniyle inode numaralarına odaklanılmasını anlıyorum,
ama çok temel bir örnek olarak USB belleği dosya depolamak için sadece düzgün çalışır hâle getirme sorununa bakınca ne olur, onu merak ediyorum
libcI/O buffering,fflush, kernel buffering, çok çekirdeklilik, time sharing, aynı anda birden fazla uygulamanın çalışması gibi karmaşıklıklardan kaçmadan tabiiTam bir acemiyim ama neden doğrudan
$(yes a/ | head -n $((32 * 1024)) | tr -d '\n')ilecdyapılmayıpwhiledöngüsü gerektiğini merak ettimDüzenleme: Anladım. Sebep
-bash: cd: a/a/a/....../a/a/: File name too longimişBilmiyorum gördünüz mü ama
wgetgibi GNU yardımcı araçlarını bellek güvenli bir C++ subset'ine otomatik dönüştüren bir demo varhttps://duneroadrunner.github.io/scpp_articles/PoC_autotranslation_of_wget
Tehlikeli C öğelerini davranış olarak karşılık gelen güvenli C++ öğeleriyle neredeyse 1:1 değiştiren bir yöntem olduğu için, yeniden yazımın getirebileceği yeni hatalar ve yeni davranış farklarını sokma ihtimali daha düşük görünüyor
Kaynak kod biraz temizlenirse dönüşüm tamamen otomatikleştirilebilir; böylece derleme aşamasında özgün C kaynağından, biraz daha yavaş ama bellek güvenli bir çalıştırılabilir dosya üretilebilir
Belki biraz aptalca bir soru ama GNU Coreutils tarafında kendi Rust yeniden yazımı üzerinde bir değerlendirme ya da plan var mı diye merak ediyorum
Rust kullanmayı biliyor olabilirlerdi ama Unix API'leriyle, onların anlamlarıyla ve tuzaklarıyla yeterince içli dışlı değillerdi
O hataların çoğu, eski GNU coreutils ya da BSD, Solaris kökenli geliştiricilerin bakış açısından oldukça acemi işi sayılır
Bu tür sorunların büyük kısmı onlarca yıl önce zaten ortaya çıkmış ve ayıklanmıştı; mevcut kod tabanlarında hâlâ uzun kuyruklu düzeltmeler var ama artık genelde düşük hacimde akıyorlar
O Canonical başlığını okuyunca gerçekten dehşete düştüm
Özeti aşağı yukarı şöyleydi: “Rust daha güvenli, güvenlik en büyük öncelik, dolayısıyla coreutils'in tamamının yeniden yazılmış sürümünü dağıtmak acil. Bir şeyler bozulursa da sorun değil, sonra düzeltilir.”
Bu şekilde düşünen insanların yazdığı kodu kendi makinemde çalıştırmak istemiyorum
Ben de Rust yanlısıyım ama Rust'ın daha güvenli olması ancak diğer her şey eşitse geçerlidir
Burada diğer hiçbir şey eşit değil
Yeniden yazım, onlarca yıldır bakımı yapılan koda göre kaçınılmaz olarak çok daha fazla hata ve açık barındıracaktır; bu yüzden güvenlik argümanı uzun vadeli geçiş stratejisi için anlamlı olsa da aceleci bir yaygın dağıtımın gerekçesi olamaz
Dağıtımdan sonra kullanıcı etkisini önemsiz göstermeye çalışmak ya da “hatalar böyle ortaya çıkar”, “mevcut coreutils'in de düzgün testleri yoktu” demek fazlasıyla sorumsuzca
Kullanıcılar kobay değil
Bakımcıların, kullanıcı sistemlerinin güvenilirliğini zedelememe yönünde ahlaki bir sorumluluğu olduğunu düşünüyorum
Daha da temelde, Rust standart kütüphanesi geliştiricileri yanlış soyutlama düzeyindeki temiz API'lere yönlendiriyor gibi görünüyor
Örneğin handle tabanlı dosya işlemleri yerine yol tabanlı işlemlere
Umarım yanılıyorumdur
Bence Rust'ın asıl amacı, en büyük ve düşmesi en kolay tuzaklarla özellikle ilgilenmek zorunda kalmamanızı sağlamak
Bu yazının özü de aslında dosya sistemi API'lerinin bunu yapması gerektiği gibi görünüyor
Birileri buna benzer bir ifade olarak disassembler rage diye bir tabir üretmişti
Yeterince yakından bakarsanız her hata amatörce görünür demek
Bir de yalnızca disassembler'a bakıp, call stack'te 100 frame aşağıdaki bir fonksiyonda neden
switchyerineifkullanıldığını diye yüksek seviye programcıya söven tavrı anlatmak için kullanılıyorŞu anda onların yanlış yaptığı birkaç şeye bakıyoruz; etrafındaki binlerce satırlık doğru yazılmış kodu ise neredeyse hiç görmüyoruz
Bu tür yardımcı araçlarda
panicolması, Rust ölçütlerine göre bile oldukça amatörce bir hataKurtarılamaz alloc hatası gibi bir durum değilse,
expectveunwrapkullanımı, o kod yolunun asla çalışmayacağını garanti eden değişmezler gerçekten çok sıkı değilse zor savunulurKodu yeniden yazarken zor olan şeylerden biri, özgün kodun gerçek üretim ortamlarında ortaya çıkan sorunlara tepki verirken kademeli olarak şekil değiştirmiş olmasıdır
O süreçte çıkarılan dersler sessizce kodun içine siner ve dokümante edilmediyse, eşdeğer seviyeye gelmeden önce yapılması gereken gizli iş miktarı çok büyür
Asıl yazı tam da bu tür bir listeyi iyi gösteriyor
Yine de hemen amatör damgası vurmadan önce, bunun yazılımın en yazılımsal görünen olgularından biri olduğunu da görmek gerek
coreutils için gerçekten iyi teknik dokümantasyon ve bu durumları kapsayan testler vardı da bunlar göz ardı edildi değilse, böyle bir şey neredeyse kaçınılmazdı
Yazıdaki iyi örneklerden biri chroot + NSS CVE
NSS'nin dinamik oluşu ve
chrootiçinde kütüphaneleridlopenile yüklemesi kuralı, göze çarpan bir yerde yazmıyorBu daha çok sistem yöneticilerinin 25 yıldan uzun süredir yaşayarak öğrendiği bir gerçek; clean-room yeniden yazımlar da bunu çoğu zaman yeni bir CVE olarak yeniden öğreniyor
Aynı kodu bir LLM ile taşısanız da durum benzer olur
Fonksiyon imzalarını okuyabilirsiniz ama gerçekten ihtiyaç duyulan şey, o kodun üzerinde kalmış yaralar ve izlerdir
Bunu GPL'den kaçınmak için, özgün kaynağı hiç okumadan yapıyorsanız iş daha da zorlaşıyor
Bence
uutilsGPL olsaydı ve coreutils özgün kaynağından doğrudan ilham alabilseydi çok daha iyi olurduBu dersleri ya da en azından kaçınılmak istenen hataları ve açıkları dokümante etmemek de kötü bir pratik; bunu söylemek lazım
Elbette en baştan iyi yazılmış kodun dolaylı olarak kaçındığı tüm hataları belgelemek zor,
ama gelecekteki okuyucu için “burada
baryerinefookullanmamızın nedeni, ABC koşulundabarkullanılırsa XYZ yüzünden tehlikeli birbazoluşması” gibi açıklamalar bırakmak önemliBiraz zaman ve doküman alanı israfı gibi görünse de bunun daha iyi olduğunu düşünüyorum
Bu yazının işaret ettiği şeylerin önemli bir kısmı, özellikle de GNU coreutils kaynağıyla karşılaştırıldığında, sıradan bir unit test veya manuel incelemede yakalanmış olmalıydı diye hissediyorum
coreutils'i yeniden yazmak korkunç bir fikir gibi görünüyor
https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/
ve önceki yazılımın biriktirdiği bilgi yeterince alınmadan yanlış biçimde ilerlenmiş gibi duruyor
Yeniden yazım yapacaksanız önceki sürümü tamamen anlamalı ve ondan öğrenmelisiniz
Yoksa aynı hataları tekrar edersiniz ve açıkçası bu oldukça utandırıcı olur
Açık olayım, Rust'ı seviyorum, birçok projede kullanıyorum ve harika buluyorum
Ama Rust sizi kötü mühendislikten kurtarmıyor
İlginç olan şu ki
uutils, GNU coreutils test suite'ini kullanıyorEk olarak, GPL kaynak kodunu okuyup yazılmış katkıları kabul etmeyeceğini de açıkça belirtmiş durumda
unity,upstart,snapyapan taraftan geliyorsa bu da tam beklenen türden bir şeyYeni sistem programcılarını şöyle karşılamak gerekiyor galiba
Unix bozuktur ve sonunda çirkin, öğretici bile olmayan dolaşma çözümlerini kendiniz yazmak zorunda kalırsınız; ayrıca ampirik test de yapmanız gerekir
Güvenilir yazılım ve iyi yazılım mühendisliği zaten böyle işler
Neden differential fuzzing bunun gibi hataları yakalayamadı merak ediyorum
https://github.com/uutils/coreutils/tree/main/fuzz/uufuzz
Bir yol üzerinde bir syscall ile kontrol yapıp sonra aynı yola tekrar syscall atarak işlem yapmak, hep aynı soruna yol açar
Üst dizinde yazma yetkisi olan bir saldırgan bu arada yol bileşenlerini sembolik bağlantı ile değiştirebilir ve kernel ikinci çağrıda yolu baştan yeniden resolve ederek ayrıcalıklı işlemi saldırganın seçtiği hedefe yönlendirir
Üst dizinde yazma yetkisi olan bir saldırgan hard link ile de oyun çevirebilir
Yalnızca normal dosyalar üzerinden oynayabilse bile pratikte düzgün bir hafifletme neredeyse yok
Örnek için bkz. https://michael.orlitzky.com/articles/posix_hardlink_heartache.xhtml
Bazı hataların kök nedeni, Unix API'lerinin fazla opak olması gibi görünüyor
Örneğin
get_user_by_namefonksiyonunun yeni kök dosya sistemi içinden shared library yükleyip kullanıcı adını çözmesi ve bu yüzdenchrootiçine dosya yerleştirebilen bir saldırganın uid 0 ile kod çalıştırabilmesi, neredeyse bir bubi tuzağı gibi hissettiriyorKullanıcı verisi alan bir fonksiyonun birden paylaşımlı kütüphane de yüklemesi, ilgi alanlarının birbirine karıştırıldığı bir tasarım gibi duruyor
Kullanıcı verisi sorgulama ile kütüphane yükleme işinin fonksiyon düzeyinde ayrılması ya da en azından bunun addan açıkça anlaşılması gerektiğini düşünüyorum
Kısmen doğru olabilir ama coreutils'i sıfırdan yeniden yazmayı seçtiyseniz POSIX API'lerini anlamak kelimenin tam anlamıyla işin merkezindedir
Ayrıca yolun dosya sistemi kökünü gösterip göstermediğini kontrol eden kod
file == Path::new("/")idiyse, bu bir API sorunu değilBunu yazan kişinin bu projeye katkı verecek yetkinliği neredeyse yok gibi görünüyor
Hatta fonksiyonel güvenli diller kullanmak, ele alınan verilerin de durumsuz olduğu yanılgısını yaratabiliyor olabilir
Oysa işletim sisteminde gerçekten çok fazla şey sürekli değişir
Snapshot sağlayan dosya sistemleri ortaya çıkana kadar her şeyi sürekli yeniden kontrol etmek gerekir
Sonuçta gereken şey, girdi verildiğinde ya başarılı sonuç ya da başarısızlık döndüren API'dir
Başarı, başarısızlık ve hata diye üçlü döndüren API değil
Evet,
musl libctam da böyle bir parçayı kaldırıyorKök nedenin Unix API'lerinin opaklığı değil, root'un kendi kontrol etmediği bir dizine chroot etmesi durumunun iyi düşünülmemesi olduğunu düşünüyorum
chrootyapılan her şey, o chroot'un kurulduğu tarafın kontrolü altındadır; bunu anlamayan birichroot()kullanmamalıget_user_by_nametuzak gibi gelebilir ama aslındanewroot/etc/passwdkullanmaklanewroot/usr/lib/x86_64-linux-gnu/libnss_compat.so,newroot/bin/shgibi şeyleri kullanmak arasında pratikte çok büyük fark yokBu yüzden
/usr/sbin/chroot'un baştan kullanıcı kimliği sorgulaması için bir nedeni olmaması gerektiğini düşünüyorumtoybox chrootzaten bunu yapmıyorSonuçta hata, bir şeyi yanlış yapma biçiminde değil, o şeyi en başta yapmış olmanın kendisindeydi
Unix ve POSIX, neresini keserseniz kesin tuzak çıkan bir fraktal gibi
Rust tarafındakiler Linux deneyimi olmadan coreutils'i yeniden yazmış olsalar bile, Ubuntu'nun bunu nasıl mainline'a aldığı bana daha da anlaşılmaz geliyor
Ubuntu'nun neredeyse her sürümde sistemin temel parçalarından birini özensiz ve tamamlanmamış bir deneyle değiştirme politikası varmış gibi
Buradaki esas mesele “vay canına, Rust kodunda hata varmış” değil, tam olarak bu bence
Özgün sürüm GPL lisanslı, yeniden yazılan sürüm ise MIT lisanslı
“Bu hatalar gerçekten dağıtılan Rust kodunda vardı ve yazarları da ne yaptığını bilen insanlardı” deniyorsa,
özgün araçlarda test harness yoktu da yeniden yazım buna önce onu kurarak başlamadı mı, diye merak ediyorum
Uç durum çok olsa bile, OS ve FS'yi bir dereceye kadar soyutlayıp
rm .//komutunun gerçekten beklendiği gibi geçerli dizini silmemesini doğrulayamaz mıyız diye düşünüyorumBu bana dağınık kod yazımı ya da dil eleştirisinden çok, yine şu eski sistem programlamada test yapılmaz tavrı gibi geliyor
Tersine, özgün araçlarda test vardı da yine de bu kadar boşluk kaldıysa, o zaman özgün test suite'in kendisi de ciddi biçimde yetersiz olabilir
Öyle düşünüyorum
Ama OS ve FS'yi doğrulama yapacak kadar soyutlayabileceğimiz konusunda o kadar emin değilim
İnsanlar ben doğmadan önce de bunu deniyordu ama hâlâ başaramamış gibiler
Mesela sınamak için kaç tane
/ekleneceğine nasıl karar vereceksiniz, orası bile belirsizDaha da ötesi, diyelim ki
rm, bir dosyanın ilk 9 baytıimportantise silmeyi reddediyorO davranışı, o dizgeyi önceden bilmeden yakalayacak testi nasıl düşüneceğinizi hayal etmek zor
Hele o sihirli kelime sözlükte bile olmayan bir şeyse daha da zor
“Sistem programlamada test yapılmaz” sözünü ciddi ciddi kullanan neredeyse kimse görmedim
Ama testlerin insanların beklediği işi her zaman yapmadığını sık sık duydum
Benim anladığım kadarıyla
uutilsgeliştirme sürecinde özgün yardımcı araçlarla geniş kapsamlı davranış karşılaştırma testleri vardı ve hatta hataları bile korumaya çalışıyorlardıWindows'un varsayılan olarak symlink'i devre dışı bırakmasının nedenlerinden biri de bu
Bunu soyutlamayla çözmek yerine, özelliği fiilen ortadan kaldırıyor
Unix türevleri ise onlarca yıldır symlink'e bağımlı yazılımla dolu olduğu için bunu yapamıyor
MacOS'ta da benzer türde bir yaklaşım var
Örneğin
chroot()hatası varsayılan ayarlarda pratikte pek sorun olmaz, çünkü MacOSchroot()'u varsayılan olarak engelliyorKullanmak için system integrity protection'ı kapatmanız gerekiyor
Temel sorun POSIX API'lerinin keskin kenarlarında ve çözüm de bunu soyutlamak değil, neredeyse tamamen ortadan kaldırmak
İnsanların denemeler yapması ve acemice girişimlerde bulunması bence sorun değil
Zaten öğrenme ve gelişme böyle olur
Asıl merak ettiğim, Ubuntu'nun karar alma zincirinde neyin bozulduğu da bunun üretime kadar girmesine yol açtı