1 puan yazan GN⁺ 2024-04-28 | 1 yorum | WhatsApp'ta paylaş
  • Hubris, yalıtılmış görevlerin IPC ile iletişim kurduğu bir işletim sistemidir; 13. sistem çağrısı olan REPLY_FAULT, sunucunun hatalı istemci isteklerini hata değeri yerine fault ile sonlandırabilmesini sağlar
  • İstemci açısından IPC bir fonksiyon çağrısı gibi görünür, ancak görevler ayrı derlendiği için yanlış işlem kodu, yorumlanamayan baytlar veya uygunsuz loaned memory durumlarının hepsini derleyici engelleyemez
  • Normal bir Hubris programı, derleme ayarları ve üretilen Rust kodu sayesinde bu hatalarla neredeyse hiç karşılaşmaz; bu yüzden her çağrıda Result<T, IpcError> ve unwrap() zorunlu kılmak kod boyutunu ve çalışma zamanı maliyetini artırır
  • Çekirdek, sistem çağrısı önkoşullarını ihlal eden görevleri hata kodu vermeden anında öldürür; REPLY_FAULT aynı fail-fast politikasını sunucu yanıtlarına genişletir
  • Bu tasarım yanlış API kullanımını hızla ortaya çıkarır, ancak rastgele IPC ve sistem çağrıları gönderen fuzz test veya chaos görevleri neredeyse hemen yeniden başlatıldığı için test yapmayı zorlaştırır

Hubris IPC ve REPLY_FAULTun konumu

  • Hubris küçük, uygulamadan bağımsız bir çekirdeğe sahiptir; sürücüler, uygulama mantığı ve ağ yığını gibi kodların çoğunu ayrı derlenmiş yalıtılmış görevlerde tutar
  • Görevler arası iletişim, çekirdeğin uyguladığı IPC sistem çağrılarıyla yapılır
    • RECV: en yüksek öncelikli gelen mesajı alır veya mesaj gelene kadar bloklanır
    • SEND: çağıranı durdurur, mesajı ve denetimi alıcı göreve devreder, ardından yanıt alana kadar bekler
    • REPLY: daha önce SEND yapan göreve yanıt ileterek yeniden çalışmasını sağlar
  • Hubris’te istemci ve sunucu sabit kimlikler değil, görevlerin üstlendiği rollerdir
    • SEND kullanan görev istemci rolündedir
    • RECV ve REPLY kullanan görev sunucu rolündedir
    • Bir görev, bir göreve karşı sunucu iken başka bir göreve karşı istemci olabilir

Derleyicinin görev sınırlarında kaçırdığı hatalar

  • Sıradan fonksiyon çağrılarında derleyici ve bağlayıcı, türleri ve çağrı hedeflerini büyük ölçüde garanti eder
    • Bir Rust fonksiyonu String argümanı alıyorsa, çağıranın bool geçmesini derleyici engeller
    • pet_cat çağırmak isterken fire_missiles çağırmak gibi hedef karışıklıkları da normalde yaşanmaz
  • Hubris IPC görev sınırlarını aşar ve her görev ayrı bir program olarak derlendiği için derleyici tüm IPC ilişkilerini doğrudan doğrulayamaz
  • Bir IPC sunucusunun karşılaşabileceği hatalar kabaca üçe ayrılır
    • Arayüze uymayan işlem kodu; örneğin yalnızca iki işlem bulunan bir arayüze “operation number 48” gelmesi
    • Beklenen mesaj türü yerine yorumlanamayan bir bayt kümesi gelmesi ya da mesajın çok kısa veya çok uzun olması
    • Gerekli loaned memory’nin olmaması ya da yazılabilir bellek gerekirken salt okunur bellek gelmesi

Normal programlara hata işlemeyi zorunlu kılmama nedeni

  • Normal Hubris programlarında bu tür IPC hataları oluşmayacak şekilde yapılandırma yapılır
    • Görev bağlantıları derleme sistemi ayarlarıyla yapılandırılır, bu yüzden birbirleriyle karıştırılmaları zordur
    • İstemci, üretilen Rust koduyla IPC’yi kurar ve gönderir
    • Sunucu da ayrı üretilmiş Rust koduyla sonucu işler
  • Tüm IPC işlemleri Result<T, IpcError> döndürecek şekilde tasarlanırsa normal programlar pratikte karşılaşamayacakları hatalar için unwrap() koymak zorunda kalır
    • unwrap() kod boyutu açısından yük getirir
    • Çalışma zamanında da hiç gerçekleşmeyecek hataları kontrol etme maliyeti doğar
  • Üretilen kodun içine unwrap() veya panic! koymak panic konumunu merkezîleştirerek kod boyutu etkisini azaltabilir, ancak çalışma zamanı maliyeti aynen kalır
  • Genel hata kodlarını desteklemek için tüm işlemlerin aynı hata kodlama kurallarını izlemesi gerekir
    • Tüm işlemler hata döndürebilmelidir
    • Tüm işlemler bu hatayı aynı şekilde kodlamalıdır
    • Başarısız olamayacak işlemler bile başarısız olabilir biçimde temsil edilmelidir
  • Hubris tabanlı firmware’de gerçekten başarısız olamayacak işlemler sürekli bulundu; GPIO pin ayarı buna bir örnektir

Hubris çekirdeğinin agresif fault politikası

  • Birçok işletim sistemi, sistem çağrısı önkoşulları ihlal edilse bile hata kodu döndürür ya da istisna/sinyal işleme fırsatı verir
    • Unix’te açılmamış bir dosya tanıtıcısı close edilirse hata kodu döner
    • open çağrısına yol adı yerine null pointer verilse bile hata kodu döner
  • Hubris, sistem çağrısı önkoşulları bozulduğunda ilgili görevi anında yok eder
    • Görev artık komut yürütemez
    • Görevin kendisine kurtarma veya devam etme fırsatı verilmez
    • Uygulamanın supervisor görevi fault bildirimi alır ve genellikle görevi silip yeniden başlatır
  • Çekirdeğin ürettiği fault bir synthetic faulttur
    • null pointer dereference veya sıfıra bölme gibi CPU’nun ürettiği donanım fault’larına benzer
    • Donanım fault’u işlemci mimarisi kurallarının ihlalinden, synthetic fault ise çekirdek kurallarının ihlalinden doğar
  • Örneğin bir SEND çağrısında alıcı görev indeksi uygulama aralığının dışındaysa veya mesaj işaretçisi erişim izni olmayan belleği gösteriyorsa synthetic fault oluşur
  • Hubris, kurtarılabilir veya devam edilebilir fault’lara izin vermez
    • Donanım fault’u da olsa synthetic fault da olsa fault alan görev ölü duruma geçer
    • Bu tercih, incelikli arıza modlarından kaçınmak ve sistem üzerine akıl yürütmeyi basitleştirmek içindir

Sunucunun istemciye fault ile yanıt verme biçimi

  • REPLY_FAULT, sunucunun istemciye normal yanıt yerine fault iletmesini sağlayan bir sistem çağrısıdır
  • Tipik REPLY akışı şöyledir
    • İstemci SEND kullandığında çekirdek istemci görevi alıcı görev için “waiting to send” durumunda işaretler
    • Alıcı görev RECV kullandığında ilgili istemci “waiting for reply” durumuna geçer
    • Sunucu REPLY çağırdığında istemci runnable durumuna döner
  • REPLY_FAULT, REPLYye benzer; ancak mesaj iletip çalıştırılabilir duruma getirmek yerine fault ileterek görevi ölü duruma sokar
  • Sunucu keyfi bir görevi öldüremez
    • REPLY_FAULT yalnızca ilgili sunucunun RECV ettiği ve henüz REPLY etmediği görevlerde kullanılabilir
    • Yalnızca belirli bir sunucunun yanıtını bekleyen istemciler üzerinde çalışır
  • Hubris, REPLY_FAULTu şu hata işlemleri için kullanır
    • Yanlış işlem kodu
    • Bozuk, kesilmiş veya anlamsız mesaj
    • İstemcinin doğru türde loaned memory göndermemesi

Uygulama hataları ve fail-fast deneyimi

  • REPLY_FAULT yalnızca IPC biçim hataları için değil, uygulamaya özgü hatalar için de kullanılabilir
  • Hubris IP yığını IP portlarını görevlere statik olarak atar
    • Bir görev başka bir görevin IP portuna dokunmaya çalışırsa IP yığını ilgili göreve fault verir
  • Bu yöntem pratikte oluşmaması gereken “teorik” hata işlemeyi azaltır ve yanlış kullanımı geliştirme sırasında hızla görünür kılar
  • REPLY_FAULT, Rust fonksiyon çağrısı önkoşulları ihlal edildiğinde genellikle panic! oluşan modele benzer biçimde, sunucunun istemci süreci üzerinde süreçler arası panic! tetiklemesine yarayan bir araç olur
  • İstemcinin bunun için kod içermesi veya işbirliği yapması gerekmez

Güvenlik eğilimi ve testteki kısıtlar

  • Eliza Weissman, Hubris’i “kötücül programlara karşı agresif biçimde hasmane” olarak tanımlar
  • İstismar girişimleri çoğu zaman önce API hatası veya yanlış kullanım olarak ortaya çıktığından, hatalı davranan bileşenin durumunu silen bir sistemi istismar etmek daha zor olabilir
    • Bu hipotez henüz test edilmedi
    • Hubris exploit denemeleriyle ilgileniyorsanız iletişime geçmeniz isteniyor
  • Gözlenen dezavantaj, sistemin fuzz test edilmesinin çok zor olmasıdır
    • Rastgele IPC ve sistem çağrıları üreten küçük bir chaos görevi uygulandı, ancak neredeyse ne yaparsa yapsın hemen sıfırlanıyor
    • Yararlı biçimde çalışması için, kararlarını her başlangıçta gözlemlenebilir şekilde değişen sistem uptime counter’a dayandırması gerekiyor
  • REPLY_FAULT, sunucunun istemcileri rastgele öldürerek chaos’u zorlaması için de bir yol sunar; ancak bu seçenek henüz tam olarak değerlendirilmedi
  • Tipik Hubris görevleri kasıtlı olarak hatalı IPC mesajlarını dinamik biçimde üretmediğinden, genellikle REPLY_FAULTun varlığının farkında olmadan çalışabilir

1 yorum

 
GN⁺ 2024-04-28
Hacker News yorumları
  • REPLY_FAULT, sistem küçük ve sıkı tasarlanmışsa, uygulamaları da çoğunlukla tüm sistemi tasarlayan kişiler yazıyorsa iyi görünüyor
    Ama bir uygulama geliştiricisi açısından, başka bir servisin herhangi bir anda benim sürecime anında öldüren bir hap döndürebildiği bir IPC modeliyle üçüncü taraf kodlara bağlanmak epey ürkütücü olurdu
    Diğer uygulama geliştiricilerine o kadar güvenmiyorum. Dünya kötü sürücülerle ve yöneticilerin baskısı altındaki geliştiricilerin yaptığı arka plan süreçleriyle dolu; sırf saat 8'den önce çıkabilmek için uygunsuz olabilecek varsayılan REPLY_FAULT'lardan bolca koymaları çok olası

    • Bu bilinçli bir tasarım gibi görünüyor ve Hubris'in hedeflediği ortam da tam olarak bu yönde
    • Symbian'da gerçekten böyle bir şey vardı. IPC sunucusu istemciyi panic'e düşürebiliyordu ve OS kaynak koduna erişimi olmayan uygulama geliştiricileri açısından bu epey berbattı
      Tüm önkoşulları kolayca anlamak mümkün değildi; cihaza veya OS sürümüne göre değişebiliyordu
    • Sapmaları hızlıca öldürmek, sistemi sıkı tutmanın bir yolu. Tasarlanan kapsamın kendisi zaten muhtemelen küçük kalmasını sağlayacaktır
      Kapsam elbette genişleme eğilimindedir, ama ana makinede ele alınması daha iyi olan işleri ille de gömülü denetleyicinin içindeki Hubris görevlerine itmek isteyeceklerini sanmıyorum
    • Gömülü ortamda, böyle yanlış anlaşılmalar kimin hatası olursa olsun ortaya çıkar çıkmaz çözülse daha iyi gibi görünüyor
      Sunucu “şu istemci hatalı” derse çekirdek o istemciyi öldürüyor. Asıl mesele, ikisinin birbirini anlamamış olması
    • Burada servisi OS arayüzü olarak düşünebilirsiniz. Tek bir çekirdekte hatalı bir çekirdek çağrısı yapıldığında OS'nin o süreci öldürmesi de makul
      Ayrıca “süreç” derken akla gelen şeyden farklı olabilir. Hubris'te iş parçacıklarının hepsi aynı adres alanını paylaşıyor
  • REPLY_FAULT zincirleme yayılıyor mu? Örneğin A, B'ye SEND edip bekliyor; B de C'ye SEND edip bekliyor; C REPLY_FAULT yaparsa A da B ile birlikte ölür mü merak ediyorum
    Öyle değilse kötü niyetli bir görev deneyi yardımcı bir göreve devreder, olur biter. Tersi doğruysa da genel olarak epey kırılgan görünüyor; Hubris'i daha iyi bildiğimden değil
    Üstelik SEND döngüsel veya karşılıklı olabiliyorsa bir görev yanlışlıkla kendi kendini öldürebilir. B → A → B gibi bir durumda bu, REPLY_FAULT kullanmamaya teşvik bile edebilir

    • Hubris'in genel amaçlı bir işletim sistemi olarak tasarlandığını sanmıyorum. Süreçler derleme zamanında tanımlanıyor
      Sunucunun istemciye karşı ateş edebilmesinin nedeni güvenlik değil, güvenilirlik. Hataların kasıtlı saldırılardan değil bug'lardan kaynaklandığı varsayılıyor; çekirdeğin aşırı tepkisi de geliştiricinin sorunu olabildiğince hızlı bulmasını sağlıyor
      Elbette güvenlikle örtüştüğü yerler var; bir sürecin yapmaması gereken bir şeyi yapmaya çalıştığı durumlarda faydalı bir yedek savunma olabilir
    • B fault'a düşerse A muhtemelen sunucunun öldüğünü belirten bir hata alır ve yeni başlatılmış sunucuya aynı mesajı tekrar gönderme fırsatı bulur. Zincirleme çökme değildir gibi
  • Hubris ve hata ayıklayıcısı Humility, zamanım olsa ya da yapmam gereken bir görev çıksa derinlemesine kurcalamak isteyeceğim teknolojiler. Ne yazık ki şu anda mümkün değil

  • Tüm kodu tek bir ekibin yazdığı sistemlerde, istemci sadece tuhaf baktı diye onu yörüngeden vurup yok etme yaklaşımının yinelemeli geliştirme hızını artırabilmesi ilginç
    Cebirsel etkileri okurken uyuyakaldıktan sonra sabah bu yazıyı okumak eğlenceli oldu. Biraz çarpıtarak bakarsak, bu sunucunun, istemcinin işleyemeyeceği bir etkiyi gerçekleştirmesine izin veren bir çekirdek
    Kod yeniden kullanımı ve bileşim çok daha zorlaşacak gibi, ama yürütme modeli çok daha basitleşiyor. Statik gömülü sistemlerde bu kesinlikle doğru bir ödünleşim. Yeniden kullanım gerekiyorsa görevi her zaman vendor'layıp değiştirebilirsiniz

    • Beklenen hatalar, örneğin dosya yok, ile beklenmeyen hata olan geçersiz işlem kodu arasını iyi ayırırsanız, normal programlarda da yeniden kullanılabilirliğin çok kötüleşeceğini sanmıyorum
      Aksine Unix'te görmezden gelinebilen çok fazla hata var ve kişisel olarak bunların önemli bir kısmının ölümcül sinyal üretmesi gerektiğini düşünüyorum. Öyle olsaydı genel yazılım kalitesi epey artardı
      Örneğin geçersiz bir dosya tanımlayıcısına close() çağırmak ölümcül olmayan bir hata olduğu için sıkça yok sayılıyor. Ama gerçekte, özellikle çok iş parçacıklı uygulamalarda, çok tehlikeli. Çoğu zaman yanlış dosya tanımlayıcısını kapatmak harmless biçimde başarısız olur; ama %1'inde bir loglama soketini, veritabanı kilit dosyasını ya da alakasız bir IPC bağlantısını kapatıverir. Herkesin nefret ettiği o kararsız yazılımlar böyle ortaya çıkar
  • Errand of Mercy'deki şu repliği hatırlatıyor: “Çeşitli kurallar ve düzenlemeler olduğunu göreceksiniz. İlan edilecekler. Bunlardan en küçüğünü bile ihlal etmek ölümle cezalandırılır”

  • Bunu HTTP için 1 Nisan RFC'si haline getirmek lazım
    HTTP 499 “Shame on you.” öneriyorum. 499 alan istemci, muhtemelen yalnızca Strict: true gibi belirli bir başlıkla başlayan isteklerde, o isteği yayımlayan görevi dile özgü yöntemle sonlandırmalı
    Bu bağlamda görülen “Bu da ne böyle… ama aslında, fena değil?” dengesini kusursuz yakalıyor

  • Çok keyifli bir okumaydı; bu tek supervisor yaklaşımı, eski startup'ımda uygulamayı her şeyi unwrap edecek şekilde yapılandırmamıza benziyor
    Sevdiğim yazılardan biri olan https://medium.com/@mattklein123/crash-early-and-crash-often... de aklıma geldi

  • Bunun gerçekten fazla agresif olup olmadığını merak ediyorum
    Linux’ta, soket üzerinden iletişim kurduğunuz başka bir programı yalnızca soketle doğrudan çökertmek mümkün değil; sokete hatalı veri gönderme durumu hariç
    Ama öldürmek kesinlikle mümkün. root olarak çalışan herhangi bir şey başka bir şeyi öldürebilir, hatta yeniden başlatıp tüm sistemi de indirebilir
    Biraz daha zor ve yaygın değil, ama en azından konteynerlerde root yetkisi yaygın. Elbette cgroup olduğu için daha da kısıtlanıyor, ama mesele bu
    “Alırken hoşgörülü, gönderirken tutucu ol” şeklindeki yaygın bilgelikten de biraz farklı. Gerçi bu daha çok ağ sistemlerine bağlı bir söz olabilir
    Yine de sistemin kabul ettikleri konusunda hoşgörülü olması kaçınılmaz olabilir. Aksi halde mevcut programları bozmadan API’yi hafifçe değiştirmenin bir yolu kalmaz, değil mi?

    • Hubris genel amaçlı bir OS değil; Oxide sunucu rack içindeki düşük seviye işlemcilerde çalışıyor
      Bildiğim kadarıyla çalışma zamanında yeni tür süreçlere de izin vermiyor. Olası tüm çalıştırılabilir dosyaların derleme zamanında belirlenmiş olması gerekiyor
  • “Sorunu düzeltip görevi sürdürmenin bir yolu yok. Bu, incelikli hata kiplerinden kaçınmak ve sistem hakkında akıl yürütmeyi basitleştirmek için bilinçli bir seçimdi” kısmı bana Einstein’ın ünlü “Mümkün olduğunca basit, ama daha basit değil” sözünü hatırlatıyor
    Bu tasarım sanki ikinci koşulu ihlal ediyor. Gerçek dünyanın karmaşasına hiç dayanıklı olmayan bir işletim ortamıyla ilgilenmiyorum; ticari olarak geçerli alanlar içinde de bunu kabul edecek bir yer pek bilmiyorum
    Sonuçta init sistemine dönüp sürekli yeniden denemesini mi sağlayacağız? Peki hangi mekanizmayla meydana gelen fault’u anlayıp daha iyi bir şekilde tekrar deneyebiliriz?
    Yine de inancın saflığını alkışlıyorum

    • Hubris akademik bir deney değil. Oxide rack’in tüm temel unsurlarının, yani compute sled’lerin, switch’lerin ve power shelf denetleyicilerinin merkezinde çalışıyor; tasarımı da her şeyden önce gerçekten sağladığı faydaya dayanıyor
      Cliff’in blogda ayrıntılı yazdığı gibi, REPLY_FAULT başlangıçta belki fazla agresif olduğunu düşündüğümüz bir özellikti; ama sistemi kurup dağıtırken ve açıkçası debug ederken edindiğimiz deneyim, bunun sistemimizi kaprisli biçimde bozmak yerine daha sağlam hâle getireceğine bizi ikna etti
      Buradaki düşünce tarzını ve pratikte nasıl göründüğünü [0] ve [1]’de daha fazla görebilirsiniz
      [0] https://www.mattkeeter.com/blog/2024-03-25-packing/
      [1] https://cliffle.com/blog/who-killed-the-network-switch/
    • Watchdog timer düzenli olarak dürtülmeyen süreçleri memnuniyetle öldürür ya da yeniden başlatır
      Hobi projelerinde bile I2C bus’ın protokol bitlerinden biri karıştığında sık sık takılıp tüm sistemi indirdiğini gördüğüm için, bu tasarımın epey ilham verici olduğunu düşünüyorum
      Anladığım kadarıyla bu, bilinen hata vakalarından, yani ele alınan hatalardan değil; protokol uyuşmazlıkları ve asla olmaması gereken şeylerle ilgili
      Diğer yorumların da belirttiği gibi, amaca özel bir OS. Erlang ile UI yapmayacağınız gibi, Hubris de kapladığı alana gayet uygun görünüyor
    • Bence bu, açıkça hatalı program durumu sonucu olan sorunlara uygulanmak istenen bir fikir. Bu yüzden makul biçimde kurtarılamaz
      Sebep bir bug, saldırı ya da bozuk donanım olabilir; her durumda devam edilmemeli. Çağıranda ciddi bir sorun vardır ve devam ederse sadece daha büyük zarar verir
      Erlang/OTP’nin “let it crash” felsefesine biraz benziyor. Erlang oldukça fazla sayıda görev kritik donanımda kullanılıyor ve güvenilirliğiyle biliniyor; dolayısıyla pratikte o kadar büyük bir kusur olmayabilir
    • Bu, çalışma zamanında yeni task eklemeyi desteklemeyen 2000 satırlık Rust gömülü sistem kernel’ı
      0xide sunucu rack’inin derinliklerinde çalışmak üzere yazılmış
  • “Kötüye kullanım girişimleri çoğu zaman önce API hatası ya da yanlış kullanım olarak ortaya çıktığından, herhangi bir hatalı davranışta yanlış davranan bileşenin durumunu silen bir sistemin kötüye kullanılması daha zor olmalıdır” kısmında, burada uygulamanın kabul ettiklerini biraz daha sıkı denetlemesi söz konusu
    Bu yüzden güvenlik açısından bir avantaj var, ama düşünülen türden değil. Saldırganın ilerlemesini yok edip onu geri püskürtmek değil; eskiden daha arzu edilen hatalı bir duruma eklemlenebilen belirli hatalı durumların artık işe yaramaması
    O zaman saldırgan bunu denemek yerine başka bir yer arar