Soru geçmişi
UnwindSafe özelliği, Rust 1.9 ile birlikte std::panic::catch_unwind ile tanıtılmıştır; bu, C++ ve diğer istisna işleme dilleriyle aktarılan istisna güvenliği endişelerini ele almak içindir. Rust'ta, panikler yığın geri sarmasını tetikler ve bu, Drop uygulamalarının çalışmasını garanti eder. Ancak bu, bir panic'in mantıksal bir işlemi kesintiye uğratması durumunda veri yapılarının tutarlı durumda kalmasını otomatik olarak sağlamaz. Bu özellik, bir catch_unwind sınırında aktif bir durumda olmaya tolerans gösteren türleri işaretlemek için tasarlanmıştır; böylece tanımsız davranış veya mantık hataları riski olmadan.
Problem
Bir değişken referans (&mut T) bir catch_unwind sınırını geçtiğinde ve T içsel değişkenlik içerdiğinde (örneğin, RefCell veya Cell gibi), bir panic, T'yi mantıksal olarak tutarsız bir durumda bırakabilir. Örneğin, eğer bir panic, RefCell::borrow_mut ile ortaya çıkan RefMut koruyucusunun gizli düşüşü arasında meydana gelirse, RefCell'ın içsel ödünç alma sayısı artmaya devam eder. catch_unwind panik durumu yakaladığında ve yürütme devam ettiğinde, RefCell hâlâ değişken olarak ödünç alınmış görünür; ancak sayıyı azaltacak koruyucu geri düşürülmüştür. Bu "zehirli" durum, ardışık işlemler sırasında RefCell'da panic geçirme riski taşır veya yanlış davranış sergileyerek program durumunu güvenli kodun tespit edemeyeceği bir şekilde bozabilir.
Çözüm
UnwindSafe, muhafazakar bir işaretleyici özelliktir: çoğu tür için otomatik olarak uygulanır, ancak &mut T ve onu içeren herhangi bir bileşikten açıkça çıkılmıştır. &mut T'nin UnwindSafe uygulamasını yasaklayarak, tür sistemi değişken referansların catch_unwind'a geçirilmesini, programcının bunları açıkça AssertUnwindSafe ile sarmaladıkça engeller. Bu sarmalayıcı, programcının sarılmış türün içsel değişkenliğe sahip olmadığı veya istisna güvenliğini manuel olarak doğruladığı riskini kabul ettiği bir güvensiz sözleşmedir. Bu mimari seçim, potansiyel olarak tehlikeli bir modele açıkça katılım gerektirerek, panic sınırları boyunca değişken, içsel değişkenlik durumunun kazara maruz kalmasını derleme zamanında yakalar.
use std::panic::{catch_unwind, AssertUnwindSafe}; use std::cell::RefCell; fn main() { let shared = RefCell::new(vec![1, 2, 3]); // Bu, &mut RefCell'in UnwindSafe olmaması nedeniyle derlenmeyecek: // let _ = catch_unwind(|| { // let mut borrow = shared.borrow_mut(); // borrow.push(4); // panic!("kesintiye uğradı"); // }); // Güvenli bir kabul ile açıkça katılım: let result = catch_unwind(AssertUnwindSafe(|| { let mut borrow = shared.borrow_mut(); borrow.push(4); panic!("kesintiye uğradı"); })); // Panic'ten sonra, shared geçersiz bir ödünç alma durumunda olabilir, // ancak bu riski AssertUnwindSafe ile açıkça kabul ettik. println!("Kurtarıldı: {:?}", result.is_err()); }
Problem açıklaması
hyper ile inşa edilmiş yüksek performanslı bir HTTP sunucusu, kullanıcı tanımlı istek işleyicilerindeki paniklerin izole edilmesi gerekmektedir; böylece tek bir hatalı istek tüm süreci sonlandırmaz. Sunucu, her iş parçacığı için aktif veritabanı bağlantılarını takip etmek üzere RefCell kullanarak bir bağlantı havuzu sürdürmektedir (tek iş parçacıklı performans için). Mimarisi, panikleri yakalamak ve bunları zarif bir şekilde günlüğe kaydetmek için her istek işleyicisini catch_unwind içine sarmaktadır. Yük testleri sırasında sunucu, bağlantı havuzunun RefCell'inin değişken ödüncünü tutan bir işleyicide bir panic ile karşılaşır. catch_unwind panik durumunu yakaladığında, havuzun iç ödünç alma bayrağı "değişken ödünç alındı" olarak ayarlı kalmakta; çünkü RefMut koruyucusu, yüzden düşüşü gerçekleştirip azaltma mantığını yerine getirmeden geri sarmada düşmüştür. Aynı iş parçacığındaki ardışık talepler, havuzun ödünç alınmasını denediğinde, zaten ödünç alınmış durum nedeniyle çalışma zamanı panic'i tetikler ve böylece iş parçacığı çökmesine ve havuz durumunu kaybetmesine neden olur.
Çözüm 1: catch_unwind'i ortadan kaldırın ve işlem sonlanmasına izin verin
Bu yaklaşım, bir panic durumunda sürecin çökmesine izin vererek istisna güvenliği sorununu tamamen ortadan kaldırır; bu, özellikle bu bağlamda doğruluğun, kullanılabilirlikten ikincil olduğunu kabul eder.
Artılar: Tamamen istisna güvenliği endişelerini ortadan kaldırır; durum bozulması riski yoktur; uygulanması basittir.
Eksiler: Üretim kullanılabilirliği için kabul edilemez; bir kötü niyetli veya hatalı istek tüm hizmeti sonlandırır; güvenilirlik gereksinimlerini ihlal eder.
Çözüm 2: RefCell yerine Mutex ile zehirleme kullanın
RefCell tabanlı havuzu Mutex<Pool> ile değiştirin ve Rust'ın mutex zehirleme tespiti mekanizmasından yararlanın.
Artılar: Mutex, tuttuğu iplerdeki panikleri algılar ve kendini zehirli olarak işaretler; durum bozulmasını PoisonError ile tespit etmeyi sağlar; standart kütüphane yerleşik güvenlik sağlar.
Eksiler: Mutex, tek iş parçacıklı async yürütücüler için gereksiz senkronizasyon yükü getirir; bağlantı havuzunun Send olmasını gerektirir; zehirlenme, havuzun yenilenme mantığını isteyecektir.
Çözüm 3: İşleyicileri AssertUnwindSafe ile sarmalayın ve durum doğrulaması yapın
Performans için RefCell'ı koruyun, ancak işleyiciyi AssertUnwindSafe ile sarın ve bir panic meydana geldiğinde RefCell durumunu sıfırlayan özel bir düşürme koruyucusu uygulayın.
Artılar: RefCell'ın performans avantajlarını korur; panic izolasyonu sağlar; kurtarma mantığı uygulamak mümkündür.
Eksiler: AssertUnwindSafe ile etkileşime girmek için unsafe kod gerektirir; tüm kod yolları için istisna güvenliğini sağlamak son derece zor; durumun bozulduğu kenar vakaları kaçırılması kolaydır.
Seçilen çözüm ve gerekçesi
Ekip, paylaşılan bağlantı havuzu için Çözüm 2 (Zehirleme ile Mutex) ve yalnızca tekrar yeniden başlatılan isteğe bağlı geçici tamponlar için Çözüm 3'ü kullanmayı seçti. Mutex'ın açık zehirlenme mekanizması, her olası panic noktasının unsafe denetimini gerektirmeden bozulmayı tespit etmek için güvenilir ve standart bir yol sağladı. Küçük performans yükü, güvenlik garantisi karşılığında kabul edildi.
Sonuç
Sunucu, istek işleyicilerindeki panikleri izole ederek durum bozulması riskini minimize etti. Havuz kilidini tutarken bir işleyici panic meydana geldiğinde, mutex zehirlenir ve sunucu bunu bir sonraki erişimde algılar, bozulmuş iş parçacığı yerel havuzunu atar ve taze bir tane oluşturur. Bu, tanımsız davranışın gerçekleşmesini engeller ve hizmetin düşmanca girdilere karşı bile kullanılabilirliğini sürdürmesini sağlar.
catch_unwind neden UnwindSafe gerektiriyor; oysa Rust panik durumlarında yıkıcıları çalıştırıyor mu?
Birçok aday, Drop uygulamalarının geri sarılma sırasında çalıştığını varsayarak istisna güvenliğinin garanti edildiğini düşünmektedir. Ancak, UnwindSafe, verilerin mantıksal durumuyla ilgilidir, yalnızca kaynak sızıntılarıyla değil. Bir panic, bir dizi işlemi (örneğin, bağlantılı bir alanı güncellemeyi) kesintiye uğratabilir ve bir nesneyi geçici olarak tutarsız bir durumda bırakabilir. Yıkıcı bu bozuk durumda çalışırsa, bozulmayı yayabilir. UnwindSafe, türün kesilmeye karşı korunamadığından (değişken veriler) ya da programcının riski kabul ettiğinden emin olur. Bu, kendi invarinatlarını ihlal eden nesnelerle yürütmenin yeniden başlanmasını engeller.
UnwindSafe ile Send/Sync otomatik özellikleri arasındaki fark nedir?
Send ve Sync de otomatik özelliklerdir, ancak bunlar olumlu mantık kullanır: &T Send’dir eğer T Sync ise, ve &mut T Send’dir eğer T Send ise. UnwindSafe olumsuz mantık kullanır: &mut T asla UnwindSafe değildir, T’ye bakılmaksızın. Ayrıca, AssertUnwindSafe belirli değerler için bir kaçış mekanizması görevi görür (belirli değerler için unsafe impl benzeri), oysa Send/Sync ihlalleri genellikle tür düzeyinde unsafe impl gerektirir. UnwindSafe ayrıca, paylaşılan referanslar için RefUnwindSafe ile çift özellik sistemi oluşturarak Send/Sync’den benzer ama farklıdır.
RefCell'in ödünç alma bayrağı nasıl panik durumlarında güvensizlik yaratır; ve neden Mutex aynı UnwindSafe sorunlarına sahip değildir?
RefCell, çalışma zamanı ödünç alma bayrağına dayanır. Eğer bir panic, borrow_mut() ile koruyucunun Drop'ı arasında oluşursa, bayrak ayarlı kalır, fakat koruyucu kaybolur. Yürütme yeniden başlar başlamaz, RefCell ödünç alınmış görünür, ancak aslında ödünç yoktur. Bu, gelecekteki ödünç alımlarda yanlışlıkla panic linkine neden olan bir mantık hatasıdır. Mutex bunu zehirleme uygulayarak önler: bir panic, kilit tutulurken meydana gelirse, Mutex kendisini zehirli olarak işaretler. Ardışık lock() çağrıları, bir önceki ipin panic geçirdiğini belirten bir hata döndürür. Bu, bozulmayı açık ve tespit edilebilir hale getirirken, RefCell'nin bozulması sessizdir. Bu nedenle, MutexGuard aslında !UnwindSafe’dir; ancak zehirleme mekanizması, RefCell'de bulunmayan güvenli bir kurtulma yolunu sağlar.