iddqd, ya da en zor türden unsafe Rust
(oxide.computer)- iddqd, değerden anahtarı ödünç alan bir Rust map kütüphanesidir; Oxide’ın Omicron kontrol düzleminde disk ve sled envanteri gibi büyük kayıtların bellek içi indekslerini tutar, bu yüzden doğruluk kritik önemdedir
- Standart
BTreeMap, anahtar ve değeri ayrı sakladığı için aktarması zahmetlidir ya da yinelenen anahtarlar birbirinden sapabilir; buna karşılık IdOrdMap, kaydın içindeki bir alandan anahtarı çıkararak sorgu yapar - unsafe Rust, derleyicinin kanıtlayamadığı güvenli programları ifade etmek için bir kaçış kapısıdır; jenerik kod kullanıcı tarafından sağlanan trait implementasyonlarını çağırdığında patolojik güvenli Rust’a bile dayanmak zorundadır
iddqdiçindeki mutable iteration, tüm indekslerin farklı olduğu değişmezine dayanarak ömürleri genişletir; patolojik bir Ord implementasyonu B-tree ile item set’i birbirinden saptırarak aynı öğe için yinelenen indeksler oluşturabiliyordu- Düzeltme, anahtar ve indeksi birlikte karşılaştırıyor ve başarısız olursa kullanıcı kodunu çağırmayan doğrusal taramaya geri dönüyor; yeterli güven için Miri, model tabanlı testler, panic fault injection ve LLM tabanlı düşmanca inceleme birlikte kullanılmalı
iddqd’nin çözdüğü problem
iddqd, anahtarı değerden ödünç alan map yapıları sunan bir Rust kütüphanesidir ve Oxide bunu Omicron kontrol düzleminde yaygın biçimde kullanır- Omicron, Oxide rack’inin merkezinde compute ve storage gibi kaynakları provizyonlayan ve rack’in çalışmayı sürdürmesini sağlayan yazılımdır;
iddqdhatalı çalışırsa kontrol düzlemi öngörülemez ve teşhisi zor biçimlerde yanlış davranabilir - Rust standart kütüphanesindeki
BTreeMap<Email, User>gibi bir yapı, anahtar olan email’i değerden ayrı saklar - Anahtarı ve değeri birlikte geçirmek için
get_key_valueile(email, user)almak gerekir; ayrıca fetch anındaUserRecordgibi ayrı bir struct oluşturmak, kayıt türü çok olduğunda yönetmeyi zorlaştırır - Email’i
Useriçinde de kopyalarsanız, map’in anahtarı ile değerin içindeki email’in birbirinden farklı olma riski ortaya çıkar IdOrdMap,IdOrdItemtrait’i aracılığıyla kayıttan anahtar çıkarmayı sağlar; anahtar tipitype Key<'a> = &'a Emailörneğinde olduğu gibi değerden ödünç alınabilir- Oxide’da bu desen, veritabanı sorgu sonuçları gibi büyük kayıtlar için çok uygundu ve kontrol düzlemi genelinde büyük kayıtların indekslenmesinde faydalı oldu
Ek özellikler
iddqd, birden fazla alandan ödünç alınan karmaşık anahtarları birinci sınıf olarak destekler; böylece dynamic dispatch gibi dolaylı çözümler gerekmezBiHashMapveTriHashMap, tek bir öğeyi sırasıyla iki veya üç ayrı anahtarla indeksler; böylece birden fazla map’i elle senkron tutma ihtiyacını ortadan kaldırır- Serde implementasyonu, map yerine sequence olarak serileştirme yapar; böylece JSON içinde string olmayan anahtarlar da serileştirilebilir ve yinelenen anahtarlar reddedilir
- Geriye dönük uyumluluk için map biçiminde serileştirme de desteklenir
unsafe Rust’ın zorlaştığı yer
- Rust’ta temel risk tanımsız davranıştır (undefined behavior, UB); güvenli kod hiçbir şekilde UB üretemiyorsa sound, aksi durumda unsound kabul edilir
- Güvenli Rust içinde derleyici, UB içeren programları reddeder; ancak statik analizin sınırları nedeniyle UB içermeyen bazı programları da birlikte reddeder
unsafeanahtar sözcüğü bu tür programları ifade etmek için bir kaçış kapısıdır; yazar, soundness sorumluluğunu üstlenir ve derleyiciden güven ister- unsafe kodun uyması gereken Rust kuralları arasında veri yarışı olmaması, başlatılmamış veya serbest bırakılmış belleğin okunmaması, aynı bellek bölgesi için birden fazla
&mutalias olmaması ve immutable verinin değiştirilmemesi bulunur - Bu kuralları özellikle mutable aliasing nedeniyle akıl yürütmek zordur; bu yüzden genellikle unsafe’in güvenli soyutlamaların arkasında kapsüllenmesi gerekir
Güvenli soyutlama doğrulamasının aşamaları
split_at_mut, mutable slice’ı iki parçaya ayıran güvenli bir metottur; ancak güvenli Rust ile bu tür bir bölme ifade edilemediği için unsafe gerekirsplit_at_mut’ın soundness’ı,&mut [T]alınıp alınmadığına,mid <= slice.len()kontrolünün yapılıp yapılmadığına ve kalan uzunluğun doğru hesaplanmasına dair çevresindeki güvenli kod değişmezlerine dayanırMyVec<T>içindekiget,lenalanının doğru olmasına ve aralığın içinde kalmasına bağlıdır; bu koşul aynı modüldeki diğer tüm metotlar tarafından korunmalıdır- Kullanıcı tarafından sağlanan kodu çağıran jenerik unsafe kod en zor kategoridir
- Güvenli Rust kodu ne kadar patolojik ya da düşmanca yazılmış olursa olsun, unsafe kodun UB üretmesine yol açmamalıdır
ExactSizeIteratoriçindekilen()değerine güvenip capacity kadar yazancollect_exactbenzeri kod, iterator sahte bir uzunluk döndürürse ayrılmamış belleğe yazacağı için genellikle unsound’durstd::io::Readde okunan bayt sayısını yanlış döndürebilen bug’lı implementasyonlara açık bir trait’tir; Rust RFC 2930 bu problemi ele alıriddqd, kullanıcı tarafından sağlanan trait implementasyonlarını çağıran jenerik bir veri yapısı olduğu için bu en zor sınıfa girer
iddqd’nin iç yapısı
iddqd, bir öğe listesi olanItemSetile slot indekslerini tutan bir tabloyu birleştirirIdHashMap<T>,ItemSet<T>ileItemIndextutan birhashbrown::HashTablekullanırItemSet<T>,Vec<ItemSlot<T>>içerir veItemSlot<T>,Occupied(T)ya daVacant { next: ItemIndex }biçimindedirfree_head, en son boşaltılmışVacantslot’u ya da boş slot olmadığını gösteren sentinel değeri işaret eder;free_headileVacantslot’lar birlikte free chain’i oluşturur- Yeni öğe eklenirken önce
free_headüzerinden yeniden kullanılabilir slot olup olmadığı kontrol edilir; varsaVacantslotOccupiedile üzerine yazılır vefree_headbir sonraki değere taşınır - Boş slot yoksa sona yeni bir
Occupiedslotpushedilir; ardından anahtar alınır, hash hesaplanır ve yeni indeks hash table’a kaydedilir - Silme işlemi bunun tersidir: hash table’da anahtarla indeks bulunup kaldırılır, ardından ilgili
ItemSlotVacantyapılır ve öncekifree_headdeğerinextolarak bağlanır IdOrdMap, hash table yerine B-tree indeksi kullanır;BiHashMapveTriHashMapise sırasıyla iki ve üç hash table tutar
mutable iteration ve ömür genişletme
IdOrdMap::iter_mut, öğeler üzerinde anahtar sırasına göre mutable iteration yapar- Rust iterator’lerinde döndürülen değer, iterator’ün kendisini ödünç almamalıdır;
collectgibi birleştiriciler, iterator yok olduktan sonra bileVec<&mut T>gibi değerler bırakabilir - Bunun güvenli olması için iterator aynı değeri iki kez döndürmemelidir
- Borrow checker, yalnızca listeye ardışık erişimi görür; tüm indekslerin farklı olduğunu kendiliğinden bilemez
iddqd,std::mem::transmute::<&mut T, &'a mut T>ile lifetime extension kullanır; bu daself.itertarafından döndürülen indekslerin hepsinin farklı olduğu değişmezine dayanır
Patolojik Ord implementasyonunun yarattığı hata
- Anahtarları 0’dan 4’e kadar sıralı beş öğe içeren bir
IdOrdMap’te,Entry APIile anahtar 0 sorgulandığındaOccupiedEntrydahili olarak indeks 0’ı saklar - Daha sonra
KeytürününOrdimplementasyonu, değerden bağımsız olarak her zamanEqualdöndürecek şekilde değiştirilirse,OccupiedEntry::removeB-tree içinde yeniden inerken yalnızca anahtar karşılaştırmasıyla yanlış öğeyi kaldırabilir - Örneğin B-tree’de önce
(key = 2, index = 2)karşılaştırılırsa,Equalsonucu nedeniyle bu entry silinir; buna karşınOccupiedEntryelindeki indeks 0 nedeniyle item set’te öğe 0’ı siler - Bu durumda B-tree içinde indeks 0 kalır, fakat item set içindeki slot 0 boşalmış olur; öğe 2 ise item set’te kalır ama artık B-tree işaretçisi yoktur
- Sonrasında
Ordyeniden normal çalışmaya döner ve anahtar 1000 olan bir öğe eklenirse,free_headtarafından gösterilen slot 0 yeniden kullanılır - Sonuç olarak B-tree içinde aynı slot’u gösteren yinelenen indeksler oluşur ve
IterMutaynı belleğe birden fazla&mutreferans üretebildiği için yapı unsound hale gelir
Düzeltme yaklaşımı ve performans ödünleşimi
- B-tree içinde aşağı inerken artık yalnızca anahtar değil, indeks eşitliği de birlikte doğrulanır
- Bilinen indekse sahip bir anahtar aranırken
(key, index)birlikte karşılaştırılır; böylece patolojikOrd,Equaldöndürse bile(key = 2, index = 2)ile aranan indeks 0 karşılaştırıldığında indeks tie-breaker nedeniyle sonuçLessolur - Bu aramanın başarılı olması için saklanan indeksin gerçekten aranan indeksle aynı olması gerekir
- Tie-breaker, yanlış öğe eşleşmesini engeller; ancak doğru öğeyi her zaman bulmayı garanti etmez
- B-tree anahtara göre sıralıdır, tie-breaker ise indeksi karşılaştırır; bu iki sıralama genel olarak birbirinden bağımsızdır
- Tree search başarısız olursa, kullanıcı kodunu çağırmayan doğrusal tarama ile ilgili indeks B-tree’den kaldırılır
- Bu fallback, silme işlemini logaritmik zaman yerine doğrusal zamana çevirir; ancak yalnızca hatalı kullanıcı kodu varken ulaşılabildiği için kabul edilebilir bir ödünleşim sayılır
Doğrulama katmanları
iddqd, temel veri yapısı olarak kullanıldığı için analitik inceleme ile çeşitli ampirik doğrulama yöntemlerini birlikte kullanır- Tüm unsafe bloklar ve unsafe kalıplar, bir ila üç Rust geliştiricisi ve gözden geçireni tarafından analiz edilir
- Her unsafe bloğun üzerinde
// SAFETY:yorumu ile gerekçelendirme bırakılır ve Clippy’ninundocumented_unsafe_blockslint’i bu yorumların varlığını denetler - Örnek tabanlı testler, map oluşturup işlemler yaptıktan sonra sonucu doğrular;
iddqd, hem iç unit test’lere hem de herkese açık API integration test’lerine sahiptir - Bu testler doctest olarak da bulunur ve aynı zamanda dokümantasyon işlevi görür
- Patolojik testler, bug’lı
Ordve başka trait implementasyonları sağlar - CI ortamında hem normal testler hem de patolojik testler Miri altında çalıştırılarak çeşitli UB türleri saptanır
- Yinelenen indeks olmaması gibi değişmezler, Miri dışındaki normal test ortamlarında da doğrulanabilir
Model tabanlı testler ve fault injection
iddqd, iki katmanlı randomized testing kullanır: doğru kabul edilen bir oracle ile karşılaştıran model-based testing ve bunun üzerinde fault injection- Model tabanlı test veya stateful property-based testing, rastgele işlem dizilerini bir tip örneğine uygular ve sonuçları doğru olduğu bilinen oracle ile karşılaştırır
iddqd, verimsiz ama açıkça doğru olanNaiveMaporacle karşısında kapsamlı model tabanlı testler yürütür- Fault injection, kullanıcı koduna rastgele hata ekleme yöntemidir;
iddqdiçin özellikle panic safety bu eksende çok etkili oldu - Kullanıcı kodu işlemin ortasında panic etse bile map’in değişmezleri bozulmamalıdır
- Her map işlemine rastgele bir panic countdown eklenir; kullanıcı kodu her çağrıldığında sayaç 1 azaltılır ve 0 olduğunda panic tetiklenerek rastgele panic safety testi yapılır
- Bu yöntem,
iddqdiçinde birkaç incelikli hatayı ortaya çıkardı ve bunların bazıları unsoundness’a yol açıyordu - Model tabanlı testler, her işlemden sonra no-duplicate-index gibi dahili değişmezleri de doğrular
- Model tabanlı testler, Miri altında çalıştırmak için fazla yavaştır; bu yüzden soundness ve correctness’ın dayandığı değişmezler ayrıca doğrulanır
LLM tabanlı düşmanca inceleme ve biçimsel yöntemler
- Güncel nesil frontier modeller, map’i bozan çeşitli patolojik kullanıcı kodu implementasyonları bulabildi
- Dikkat çekici bir vakada LLM, custom allocator panic edip unwind olduğunda map değişmezlerinin bozulduğu bir yolu kurguladı
- Bu, mevcut panic safety testlerinin ele aldığı
Ordimplementasyonu gibi genel kullanıcı kodu panic’lerinden farklı bir panic safety problemiydi - LLM’ler kulağa makul gelen ama yanlış soundness iddiaları da üretebilir; bu yüzden Miri’yi oracle olarak kullanan red-green TDD savunma mekanizması olur
- Soundness hatalarında önce Miri altında UB gösteren bir test yazılır, sonra düzeltmeden sonra aynı testin geçtiği yeniden doğrulanır
Kanigibi bir model checker ile map değişmezlerini kanıtlama yaklaşımında,iddqdyeterince karmaşık olduğu için gerekli proof’lar aracın kaldırabileceğinden fazla büyürCreusot, Rust kodunun panic ve diğer hatalardan arınmış olduğunu kanıtlamada yardımcı olabilir; ancak kullanıcı kodu panic etse ya da yanlış davransa bile korunması gereken değişmezleri şu aşamada kanıtlayamazNaiveMap,iddqdiçin en yakın specification işlevini görür ve CI’da model tabanlı testlerin binlerce kez çalıştırılması, implementasyonun specification ile uyumlu olduğuna dair yüksek güven sağlarcargo-anneal, unsafe Rust yanındaki doc comment içine Lean soundness proof’ları yerleştirmeyi amaçlayan geliştirme aşamasındaki bir araç olarak ilgi çekicidiriddqd, açık değişmezleri ve sınırlı ama önemsiz olmayan kapsamı sayesinde biçimsel doğrulama araçları için iyi bir benchmark adayıdır
Sonuç
- unsafe jenerik Rust, rastgele trait implementasyonlarını ve hatta düşmanca implementasyonları hesaba katıp her değişmezi korumak zorunda olduğu için son derece zordur
iddqdhatası, patolojik birOrdimplementasyonunun map’i kandırarak aynı bellek üzerinde mutable alias oluşturabildiğini gösteriyor- Tek bir teknik tüm hataları yakalayamadığı için, insanların satır satır unsafe muhakemesi, örnek tabanlı testler, patolojik testler, randomized testler ve frontier model tabanlı düşmanca inceleme birlikte kullanılır
- Oxide, temel altyapıda bu düzeyde bir titizliğin haklı olduğunu düşünüyor
1 yorum
Lobste.rs görüşleri
Doğru anladıysam (yoldayım, telefondan baktım), bu iş sarmalayıcı tip kullanıp
HashMap/BTreeMapyerineHashSet/BTreeSetkullanılarak çözülebilir gibi görünüyorSarmalayıcı tip kesinlikle gerekli değil ama güvenlik ve sonraki bakım açısından iyi bir tercih olur
Gereken tek şey nesneyi saran 0 boyutlu bir sarmalayıcı; ardından sadece ilgilenilen alanlara bakan özel bir
PartialEq/Hashimplementasyonu eklenebilir. Tüm değeri oluşturmadan arama yapmak için, değer tipi içinAsRefuygulayan ikinci bir tip oluşturulabilirBu yaklaşım,
unsafekullanmadan mevcutHashSet/BTreeSetarayüzünü aynen yeniden kullanmanın bir yolu. Değer/anahtar tipini sarmalamak yerine, bu davranışı sağlayan yeni birHashSet/BTreeSetsarmalayıcısı da yapılabilirEntryAPI’si, değiştirilebilir erişim/yineleme gibi şeyler de var.iddqdiçinde değiştirilebilirlik oldukça dikkatli ele alınıyor ve bazı yerlerde içsel değiştirilebilirliğe izin vermek için atomik değerler kullanılıyor. Böyle bir işlem indeks dolaylı başvurusu olmadan epey zor olurduiddqdiçin biçimsel doğrulama yapacak olsam, ilk olarak Kani gibi bir model denetleyici ile map’in iç değişmezleri bozmadığını kanıtlamaya çalışırdım. Amaiddqd’nin Kani’nin ele alması için biraz fazla karmaşık olduğu ve gereken kanıtların aracın kaldırabileceğinden fazla büyüdüğü kısmı merak ettimBunu biraz daha ayrıntılı paylaşabilir misiniz? Algoritmanın hesaplama karmaşıklığının en kötü durumda dejenere olduğu anlamına mı geliyor diye merak ediyorum
Genel olarak bu, biçimsel yöntemler için ilginç bir vaka çalışması ve bu konuya değinmenize sevindim. Mevcut biçimsel doğrulama araçlarının, büyük projelerdeki başarı örneklerine bakınca, en azından veri yapısının doğruluğunu kanıtlayabileceğini naifçe düşünebiliriz; ama pratikte veri yapısına göre zorluk çok değişiyor ve Rust gibi sınırsız aliasing’e izin veren dillere kıyasla kanıtlamaya daha elverişli sayılan dillerde bile bunun iş ortamında kolay olmadığı ortaya çıkıyor
Konudan biraz sapıyor ama diyagramları nasıl yaptığınızı da merak ettim. Tek seferlik bir betik mi yazdınız, yoksa Oxide blog/web sitesi tasarımına uyarlanmış özel üretimler gibi görünüyor; genel amaçlı bir araç gibi durmuyor
Kapsamı daraltmayı da denedim. Örneğin
hashbrown’un doğru olduğunu varsaydım ama bu da pek ilerleme sağlamadı. O noktada neredeyse vazgeçtim. Düzgün çalışan trait implementasyonları içiniddqd’nin doğru olduğundan oldukça eminim ve biçimsel yöntemlerde benim asıl ilgimi çeken şey, keyfi ve hatalı çalışan implementasyonlar için de geçerli bir kanıttıAma Kani’yi en iyi kullanan kişi ben değilim. Siz ya da başka biri bunu denese gerçekten harika olur
Diyagramları önce Mermaid ile taslak olarak yaptım, sonra da harika tasarımcımız Ben Leonard bunları bizim renk paletimize ve temamıza uygun el yapımı diyagramlara dönüştürdü
Bir nesneyi, o nesnenin alanlarından birini anahtar olarak kullanıp indeksleyen alan tabanlı map deseni, C#’ta da her zaman kullanabilmeyi istediğim bir şey
Bir zamanlar kendim basit bir sürüm yapmaya çalışmıştım ama arayüzü çok temiz olmamıştı, ben de bırakmıştım. Bu yazı bana tekrar deneme isteği verdi
cargo-nextestgibi prodüksiyon kod tabanlarından kişisel projelere kadar her yerde kullanır oldumTek bir alanı anahtar olarak kullanmak en yaygın kullanım olsa da,
iddqdyeterince esnek; alanların, alt alanların veya bir alandan saf ve ucuz biçimde hesaplanabilen fonksiyonların herhangi bir birleşimi kullanılabiliyor. Örneğin https://docs.rs/iddqd/latest/iddqd/ içindeArtifactKeyaratabilirsiniz (Rust sözdizimine aşina değilseniz kusura bakmayın)iddqd’yi tasarlarken, kullanıcıların Rust tip sisteminin tüm gücünü kullanabilmesi gerektiğini güçlü biçimde hissediyordum; bunun için kütüphane yazarı olarak benim ne kadar dolambaçlı yol izlemem gerekirse gereksin.iddqd’nin kullanıcılar, özellikle de iş arkadaşlarım için kullanımı keyifli bir kütüphane olmasını gerçekten istedim