- Oxide'in ağ anahtarı firmware'i, güç sıralaması değişikliği testinden sonra açılmadı; nedenin ise Hubris çekirdeğindeki IPC bellek ödünç verme denetiminin yeni bellek yerleşim yöntemiyle çakışan bir bug olduğu ortaya çıktı
- Hubris, görevleri MPU ile izole eden bir gömülü işletim sistemi ve IPC'de başka bir göreve bellek ödünç verilirken çekirdek bu belleğin gerçekten erişilebilir bir alan olup olmadığını doğruluyor
- Yakın zamanda eklenen task packing, bazı firmware imajlarında RAM'in %30'unu geri kazandırdı; ancak mevcut denetim, ödünç verilen belleğin tek bir MPU bölgesi içinde olduğunu varsaydığı için başarısız oldu
sequencer görevi, 0x801bffd adresini içeren belleği I2C sürücüsüne ödünç vermeye çalışırken synthetic memory fault ile çöktü; humility tasks çıktısında da 115 yeniden başlatma ve mem fault... in syscall durumu kaldı
- Düzeltme, bitişik birden fazla MPU bölgesine yayılan ödünç vermeye izin verecek şekilde denetim algoritmasını değiştirmek oldu; sorunun bulunmasından çekirdek bug'ının düzeltilmesine kadar yaklaşık 3 saat geçti
Açılmayan ağ anahtarı
- Oxide'den Arjen Roodselaar, ağ anahtarı firmware'inde güç sıralaması ve saat ayarı değişikliklerini test ederken, önemsiz görünen bir değişiklikten sonra anahtarın açılmaması sorunuyla karşılaştı
- Firmware'in bir kısmı sorgulara yanıt veriyordu, ancak güç besleme sıralayıcısından sorumlu kritik bölüm durmuş gibi görünüyordu
- Güç sıralaması hataları donanıma gerçekten zarar verebildiği için, önce anahtarın gerçekten öldüğünü mü yoksa sadece yanıt vermediğini mi doğrulamak gerekiyordu
Hubris ve kısıtlı bellek
- Hubris, klavye iç denetleyicisi gibi derin gömülü sistemler için bir işletim sistemi ve Oxide Rack'in büyük işlemcilerini başlatmak için gereken işleri yürütmek amacıyla geliştirildi
- Hubris tabanlı firmware, ayrı ayrı derlenen birden fazla programdan oluşan görevler (task) yapısına sahip
- Her görev, ihtiyaç duyduğu standart kütüphane kodu gibi bileşenleri kendi içinde barındırıyor
- Görevler, birbirlerini çökertmemeleri veya belleği bozmemeleri için donanımsal MPU ile izole ediliyor
- En yaygın kullanılan ARM Cortex-M tabanlı ARMv7-M'de, korumalı bellek bölgelerinin boyutu 2'nin kuvveti olmak zorunda ve bu boyuta göre hizalanmaları gerekiyor
- Örneğin 1024 baytlık bir bölgeye 1 bayt daha gerekirse bölge 1025 değil, 2048 bayt olur
Task packing'in oluşturduğu yeni sınırlar
- İlk dönem Hubris, görev RAM'i için bir bölge, flash için bir bölge kullanan basit bir yaklaşım izliyordu; ancak görevler arasında kullanılamayan boşluklar oluşuyor ve bellek israf ediliyordu
- Matt Keeter, mümkün olduğunda build sisteminin birden fazla power-of-two bölgeyi birleştirerek görev yerleşimi yapmasını sağlayacak şekilde sistemi geliştirdi
- Donanım, görev başına en fazla 8 bölgeye izin veriyor
- Bazı firmware imajlarında RAM'in %30'u geri kazanıldı
- En küçük cihazlar, her seferinde optimizasyona ihtiyaç duyacak kadar sıkışık durumdan bir miktar boş alanı olan hale geldi
- Bu değişiklik, görevlerin flash ve RAM düzeni içinde öngörülmesi zor MPU bölge sınırlarının oluşmasına yol açtı
humility tasks'ın bıraktığı ipucu
- Arjen, arızalı anahtarı Hubris hata ayıklayıcısı Humility ile inceledi ve güç sıralamasından sorumlu servis işlemcisinin hayatta kalıp çalıştığını gördü; bu yüzden donanım arızası olasılığı düşük görünüyordu
humility tasks çıktısında sequencer görevi şu durumu gösteriyordu
mem fault (precise: 0x801bffd) in syscall (was: wait: reply from i2c_driver/gen0)
- Aynı görev 115 kez yeniden başlatılmıştı ve Hubris'te bu neredeyse her zaman bir çökme sonrası verilen tepki anlamına geliyor
- Durum dizgesi şu anlama geliyordu
mem fault: bellek işleme kurallarının ihlali
precise: 0x801bffd: sorunlu adres tam olarak biliniyor
in syscall: görev çalışırken değil, bir sistem çağrısı sırasında hata oldu
was: wait: reply from i2c_driver/gen0: I2C sürücüsüne gönderilen mesaja yanıt bekleniyordu
gen0, i2c_driver görevinin henüz hiç çökmediği anlamına geliyor; sequencer ise generation 115 durumundaydı
Hubris IPC ve bellek ödünç verme
- Hubris görevleri, IPC mesajlarıyla haberleşiyor ve bu mesajlar fonksiyon çağrısı gibi çalışıyor
- Mesajı gönderen görev duruyor
- Alan görev CPU denetimini alıyor
- Sonuç geri dönünce gönderen görev yeniden uyanıyor
- IPC, Rust'ın ownership model yapısına iyi uyacak şekilde tasarlandığı için görevler kendi belleklerinin bir bölümünü IPC mesajıyla birlikte başka bir göreve ödünç verebiliyor
- I2C aygıtlarıyla etkileşen görevler, kendi bellek aralıklarını I2C veri yolu sürücüsüne ödünç veriyor; sürücü de bu aralığı yerinde okuyup yazıyor
- Böylece veri yolu sürücüsünün ayrı bir buffer pool taşıma ihtiyacı azalıyor
- Veri kopyalama sayısı düşüyor
- Hatalı uygulanırsa bu bir güvenlik açığına dönüşebileceğinden, Hubris çekirdeği görevin gerçekten sahip olmadığı veya erişemediği belleği ödünç vermesini yasaklıyor
- Sunucu bir hata kodu alıyor
- İstemci fault alıyor ve her zaman sonlandırılıyor
- Bu, bug, bozulma veya exploit olasılığına işaret eden bir erişim ihlali olarak ele alınıyor
Synthetic fault ve gerçek neden
- Hubris, fault'ları real fault ve synthetic fault olarak ayırıyor
- real fault, null pointer dereference veya kod bölgesine yazma gibi donanım kuralı ihlalleri
- synthetic fault ise IPC ya da bellek ödünç verme gibi Hubris'in eklediği yazılımsal kuralların ihlali
sequencer görevinin aldığı fault, IPC üzerinden I2C sürücüsüne bellek ödünç verme sırasında oluşan bir synthetic fault'tu
- Sorunlu
0x801bffd adresi geçerli bir flash adresiydi; ancak 2'nin kuvveti sınırının 3 bayt altında olduğu için garip bir desen gibi görünüyordu
humility mem çıktısı, sequencer görevine ait iki flash bölgesinin 0x801c000 adresinde birbirine değdiğini gösteriyordu
LOW HIGH SIZE ATTR ID TASK
0x08018000 - 0x0801bfff 16kiB r-x--- 17 sequencer
0x0801c000 - 0x0801dfff 8kiB r-x--- 17 sequencer
- Bu iki bölge aynı göreve ait olduğundan, normal program yürütmesinde donanımsal MPU erişime sorunsuz izin verebiliyordu; ancak çekirdeğin IPC bellek ödünç verme denetimi farklı bir varsayıma dayanıyordu
Eski sadeleştirmenin bug'a dönüştüğü nokta
- Eski çekirdek denetimi, ödünç verilmek istenen bellek diliminin tamamının görevin tek bir bölgesi içinde yer alıp almadığını kontrol ediyordu
self.region_table().iter().any(|region| {
region.covers(slice)
&& region.attributes.contains(desired)
&& !region.attributes.intersects(forbidden)
})
- Bu kod yazıldığı dönemdeki tasarımla, yani görev başına bir RAM bölgesi ve bir flash bölgesi varsayımıyla uyumluydu
- Task packing eklendiğinde, aynı görevin belleği bitişik birden fazla MPU bölgesine bölünebilir hale geldi ve eski varsayım artık geçerli olmadı
- Sıradan bellek erişimleri donanımsal MPU tarafından doğrudan denetlendiği için etkilenmedi; sorun yalnızca bu belleği IPC ile ödünç vermeye çalışırken ortaya çıkıyordu
İki özelliğin birlikte yarattığı arıza
- Task packing fırsatçı biçimde çalışıyor
- Görev başına en fazla 8 bölge sınırı var
- Donanım sürücüsü görevleri, memory-mapped register'lar nedeniyle bazı bölgeleri zaten kullanıyor
- Daha akıllı yerleşim yalnızca boş bölge yuvası kaldığında deneniyor
- Sonuç olarak bölge sınırları, görev geliştiricisinin öngörmesini zor bulacağı yerlerde oluşabiliyor
- Görev A'nın boyutundaki küçük bir değişiklik, ilgisiz görev B'nin MPU bölge sınırlarının yerini değiştirebiliyor
- Sadece hata ayıklama kodu eklemek bile yerleşim kararlarını ve bölge sınırlarını değiştirerek çökmenin ortadan kaybolmasına neden olabiliyordu
- Matt, Arjen'in çalışan bir firmware imajı oluşturabilmesi için build sisteminde task packing'i hemen kapattı; bu sırada çekirdek bug'ı da analiz edilip düzeltildi
Çekirdek düzeltmesinin yöntemi
- Düzeltmenin özü, ödünç verilmek istenen belleğin tam bitişik birden fazla MPU bölgesine yayılmasına da izin verecek şekilde bellek erişim denetimi algoritmasını değiştirmekti
- Yeni algoritma, bölge tablosunu yalnızca bir kez tarayacak şekilde tasarlandı
- Hubris, görevlerin denetleyebildiği zaman karmaşıklığına sahip işlemleri dışa açmaktan kaçınıyor
- Performans, ödünç verilen belleğin boyutuna değil, sabit boyutlu bölge tablosuna bağlı olmalıydı
- Bölge tablosu boyutu sabit olarak 8
- Bunun için build sistemi, görev bölgelerini adres artan sıraya göre sıralayacak şekilde değiştirildi
regions.sort_by_key(|i| region_table.get_index(*i).unwrap().1.base);
- Düzeltme commit'i, çekirdeğin bu sıralama özelliğini kullanarak daha düşük maliyetli bir erişim denetimi yapmasını sağlıyor
- Karmaşıklaşan kod, Hubris çekirdeğinin merkezinden ayrılıp daha taşınabilir bir crate'e alındı ve önemli corner case'ler için unit test'ler eklendi
- Yeni kod sayesinde task packing yeniden etkinleştirilebildi ve görev geliştiricilerinin karşısına öngörülmesi zor çökmeler çıkması engellendi
Arızanın büyümemesinin nedeni
- Tüm süreç, ağ anahtarının açılmamasıyla başladı ve yaklaşık 3 saat içinde çekirdek bug'ının düzeltilmesine kadar ilerledi
- fault isolation sayesinde, 23 izole görevden oluşan anahtar firmware'inde yalnızca
sequencer tekrar tekrar çöküyordu; diğer birçok bileşen çalışmayı sürdürdü
- Firmware güncelleme sistemi
- Yönetim ve denetim arayüzü için IP ağ yığını
echo protokolü uygulamasından rack kontrol düzlemi arayüzüne kadar çeşitli ağ servisleri
- Sensörler, fanlar ve diğer sistem durumu izlemesi için I2C, SMBus, PMBus
- Ön paneldeki 32 adet QSFP 100G transceiver sürücüsü
- Hubris IPC, diğer görevlerin başarısız olabileceği varsayımıyla tasarlandığı için idempotent olarak işaretlenen işlemler şeffaf biçimde yeniden denenebiliyor
- Eski bellek erişim denetimi bug'ı, doğru programların erişimini engelleyen bir türdendi; hatalı ya da kötü niyetli erişime izin veren bir tür değildi, bu yüzden güvenlik etkisi olmadı
sequencer ile I2C sürücüsü belleği fiilen paylaşacakken sequencer çöktü, ancak I2C sürücüsü bozulma riski olmadan çalışmaya devam etti
Hata ayıklama altyapısı ve ekip çalışması
- Humility, Hubris çekirdeğiyle birlikte gelişen bir hata ayıklayıcı ve Arjen birkaç dakika içinde çöken kodun yerini satır numarası düzeyinde belirleyip servis işlemcisinin bağımsız bir anlık görüntüsünü paylaşabildi
- Hubris, çöken görevin sıkıştırılmış core dump'ını RAM'e yazıyor ve bu veri ağ üzerinden geri alınabiliyor
- Yazılabilir kalıcı depolama olmasa da crash dump elde etmek mümkün
- Crash dump işlevi çekirdekte değil, ayrı bir görevde bulunuyor
- Söz konusu işlemciler müşteri iş yükü verilerini değil, yalnızca sistem yönetim trafiğini işliyor ve crash raporları otomatik olarak yüklenmiyor
- Hubris çekirdeğinin mimariden bağımsız kısmı 1.789 satır kod ve 1.192 satır yorumdan oluşuyor; ARMv6-M, ARMv7-M ve ARMv8-M desteği ise buna 1.075 satır kod ve 534 satır yorum daha ekliyor
- Çekirdek kavramları ve IPC basit olduğundan, fault IPC'yi işaret ettiğinde kontrol edilmesi gereken çok fazla yer yoktu
1 yorum
Hacker News yorumları
Hubris gerçekten çok iyi. Kernel kodunu yaklaşık 30 dakika okudum; geçmişte gördüğüm
ifdefmakro yığınları, iki harfli değişken adları ve yorum fakiri C kodundan çok uzak, son derece açık ve iyi yazılmışYatmadan önce okunacak bir şey olarak da güzel; bir göz atmanızı tavsiye ederim: https://github.com/oxidecomputer/hubris/blob/b44e677fb39cde8...
Kaynak kodda disk alanı 40 yıldır büyük bir sorun değil ama insanlar hâlâ değişken adlarında cimri davranıyor
Çünkü yapay zeka belirli bir kodlayıcının zevkini ve alışkanlıklarını tam olarak öğrenmiştir
Güzel bir yazı ama aşağıdaki yorumun konumu biraz talihsiz
regions.sort_by_key(|i| region_table.get_index(*i).unwrap().1.base);satırının üstündeki “adres artan sırada sıralanmalı ve kernel bu özelliği erişim kontrollerini ucuza yapmak için kullanır” yorumu, bu fonksiyonun bir detayı olmaktan çok tüm yazarların koruması ve tüm okuyucuların yararlanabilmesi gereken bir alan değişmeziBu yüzden bunu
TaskDesc::regionsdokümantasyon dizgesine koymak daha doğru olurdu: https://github.com/oxidecomputer/hubris/commit/b44e677fb39cd...Muhtemelen en iyi yaklaşım, bölgeleri sıralayan bir kurucu metot ekleyerek değişmezi zorunlu kılmak olurdu. Kodun zamanla daha karmaşık hâle geldiği görülüyor; bu noktada karmaşıklığı metotların içine kapsüllemeye biraz zaman harcamak değerli görünüyor
Şimdiye kadar gördüğüm en iyi iş ilanlarından biri. Doğal biçimde kültür konusuna geçip sonunda “bu arada işe alım yapıyoruz” demesi hoş
Gerçekten mükemmel bir postmortem yazısı ve ben bir uygulama katmanı geliştiricisi olmama rağmen takip edebildim. Zaten şu sıralar Rust in Action okuduğum için bu tür içeriklere daha hazırlıklıydım
Koda bol yorum yazan insanları görmek de her zaman keyifli. Edebi programlama işe yarıyor
Önceki bölüm sanırım burada bulunabiliyor
“Ekibin sıkı, hiyerarşik olmayan entegrasyonu” kısmı dikkatimi çekti. Hubris’in doğrudan bir özelliği değil ama Hubris’i onu yapan ekipten ayırmak zor ve Oxide mühendislik ekibinde fiilen iç silo olmadığı açıklaması etkileyiciydi
Açıklık, merak, iletişim teşvik edilirken savunmacılık, imparatorluk kurma ve gatekeeping’in neden ve tam olarak nasıl bastırıldığını daha çok duymak isterim. Böyle bir kültürü bir organizasyonda büyütmenin dezavantajları olup olmadığını da merak ediyorum
Daha katı hiyerarşik sistemleri seçen yerler de var; organizasyon şemasının stratejik olarak belirlenmesi gerekebileceğinden, trade-off’ları tam göremiyorum
Bu yapı açıkça tanımlı değilse daha az şeffaf olur, bilinçli olarak seçilmiş olmaz ve özellikle sosyal etkileşimlerde çok rahat olmayan insanlar için anlaşılması daha zor hâle gelir. Bu yüzden gölge gibi yapısı, daha patolojik davranışlara alan açabilir; çok kötü olmasa bile koordinasyonu ciddi biçimde zorlaştırabilir
Bunu birkaç şirkette yaşadım. Büyük bir danışmanlık şirketinde resmî güç yapısı vardı ama pratikte pek uygulanmıyordu; projelere girmenin yolu da resmî kanallardan çok satış ve yönetim tarafındaki insanlarla yakın olmaktan geçiyordu. Gerekli sosyal ağı kurabiliyorsanız iyiydi ama kuramıyorsanız iyi işlemiyordu
Benzer bir örnek olarak “The Tyranny of Structurelessness” var. Hiyerarşiyi ataerkil diye görüp reddeden organizasyonlarda aynı şeyin yaşandığını gözlemleyen bir feministin konuşması; iç yapısı net olmayan Valve için de benzer tartışmalar var. Açık kaynak projeleri de aynı sorunu yaşayabiliyor ve Rust tarafındaki bazı çatışmaların da benzer problemlerden kaynaklandığını düşünüyorum
Ama açık güç yapılarının mutlaka hiyerarşik olması gerekmez. Geleneksel iş organizasyonları hiyerarşiktir ama Oxide’ın yapısı açıkça tanımlı olup yine de hiyerarşik olmayabilir. Bu tür modeller genelde daha küçük ölçekte daha iyi çalışır ve sözünü ettiğim danışmanlık şirketi, daha serbest biçimde işleyen şirketler arasında bildiğim en büyük örnekti ama yine de onu destekleyen belli bir iskelet vardı
Bu bir ikilik değil, bir spektrum. Kâğıt üstünde en katı güç yapısında bile altında daha karmaşık, örtük bir yapı vardır; insan gruplarının doğası budur
Açık yapıların her zaman örtük yapılardan daha iyi olduğuna inanmıyorum. Sadece daha az açık organizasyonlarda gözlenen dezavantajlardan söz ediyorum; daha açık güç yapılarının da kendine özgü sorunları var. Bununla ilgili olarak “seeing like a state” ve okunabilirlik meseleleri de var
Karmaşık bir problemi debug etme sürecini derinlemesine gösteren harika bir yazı. Sistemin geri kalanının kararlı kalmış olması, Oxide ekibinin mühendislik kalitesini çok iyi gösteriyor
Kişisel olarak da oldukça ilham vericiydi ve benzer teknikleri günlük işime uygulamayı düşünüyorum
O donanım yazılımla doldurulan TLB gibi ele alınırsa 8’den fazla bölge de desteklenebilir
Son çare olmadıkça bunu asla yapmazdım. Sanal sayfalama dağınık bir şey ve şüphe bırakmak istemezsiniz
Oxide’ın yaptığı şeyler gerçekten etkileyici
Oxide ekibinin yaptığı her şeyi seviyorum, bu da onlardan biri
İşletim sistemine Hubris adını mı vermişler? Ah, bu… bunu söylemeye dilim varmıyor
Aklı başında hangi insan bugünlerde yeni bir işletim sistemi kullanır ki? Cevap, tüm işletim sistemlerinin görmezden geldiği bir sorunu çözmeye çalışan kişidir: işletim sisteminin kontrol etmediği ve edemeyeceği anakart ve genişleme kartı denetleyicileri sorunu