- EdgeDB, ağ I/O'sunu Python'dan Rust'a taşırken
reqwesttabanlı yeni HTTP fetch testinin yalnızca ARM64 CI'da takılmış gibi göründüğünü, ama aslında çöktüğünü fark etti - Çekirdek dökümü analizi, sorunun yeni HTTP kodunda değil
libciçindekigetenv()çağrısında olduğunu gösterdi; ortam değişkeni dizisini dolaşırken hatalı0x220işaretçisini okumaya çalışıp başarısız oluyordu - Başka bir thread,
openssl-probeyolundaSSL_CERT_FILEveSSL_CERT_DIRayarlarkensetenv()environdizisini yeniden tahsis etti; aynı anda Python hata işleme yolugetenv()çağırınca yarış durumu oluştu - Rust kodunda açık bir
unsafeyoktu, ancakstd::env::set_var()global ortamı değiştirirken bu işlem Rust içi kilitle, diğer runtime'larla veya doğrudanlibcçağrılarıyla senkronize edilmiyordu - Çözüm, Linux'ta
reqwestinrust-native-tls/opensslbackend'i yerinerustlskullanmaya geçmek oldu; Rust 2024 edition ve glibc de bu tür sorunları azaltacak yönde değişiyor
Yalnızca ARM64 CI'da ortaya çıkan çöküş
- EdgeDB, ağ I/O kodunun önemli bir bölümünü Python'dan Rust'a taşırken yeni bir HTTP fetch özelliği geliştiriyordu
- HTTP istemci kütüphanesi olarak
reqwestkullanıldı; özellik yerelde ve x86_64 CI runner'larında geçmesine rağmen ARM64 CI runner üzerinde aralıklı olarak başarısız oluyordu - Başta test runner'ın süresiz takıldığı sanıldı; CI loglarında hata olmadan tek bir test saatlerce çalışıyor görünüyor ve sonunda timeout'a düşüyordu
- İlk hipotez, Intel ile ARM64 arasındaki bellek modeli farkları idi
- Intel nispeten daha katı bir bellek modeline sahiptir ve bellek yazmaları için tüm işlemcilerin uzlaştığı küresel bir sıra vardır
- ARM ise daha zayıf sıralı bir bellek modeline sahiptir; yazmalar farklı thread'lere farklı sıralarda görünebilir
Docker CI ortamında çekirdek dökümünü izleme
- Gece CI makineleri Amazon AWS üzerinde çalıştığından, konteyner dışındaki gerçek root kullanıcıyla bağlanıp
dmesgve sistem logları görülebiliyordu - Konteynerin içinde ve dışında takılmış gibi görünen PID arandı ama ilgili süreç bulunamadı; böylece bunun deadlock değil çöküş olduğu anlaşıldı
- Docker konteyneri süreç namespace'i kullandığından çekirdek dökümü Docker host'una iletildi ve
journalctliçindepython3sürecine ait çekirdek dökümü görüldü - Başta çekirdek
gdbile açıldığında, konteyner içindeki.sodosyaları olmadığından backtrace kullanışsızdı - Konteynerden
/lib,/usrvb. kopyalanıpgdbiçindesolib-absolute-prefixayarlandıktan sonra, çöküş noktasınınlibc.so.6içindekigetenv()olduğu doğrulandı
getenv() tarafından okunan bozuk ortam değişkeni işaretçisi
- Tam backtrace şu akıştaydı:
getenv()→__dcigettext()→strerror_r()→strerror()→PyErr_SetFromErrnoWithFilenameObjects() - Yeni HTTP kodu doğrudan çökmemişti; Python, errno tabanlı bir istisna üretirken gettext ile ilgili bir yol üzerinden
getenv()çağırıyordu - GLIBC 2.17'deki
getenv()uygulaması, POSIXenvironyapısını birchar **listesi olarak dolaşır ve sonNULLişaretçisine kadar ortam değişkeni string işaretçilerini inceler - Disassembly ve register durumuna göre
getenv(),x19 = 0x220adresinden bayt okumaya çalışırken çöktü0x220açıkça geçersiz bir bellek adresiydienviron'ın kendisi incelendiğinde mevcut ortam değişkeni listesi tutarlı görünüyordu
Neden: setenv() ile getenv() arasında yarış durumu
setenv()çok thread'li ortamlarda güvenle çağrılabilecek bir fonksiyon değildir;libciçindekigetenv()tarafında garip çöküşlere yol açtığı daha önce de defalarca yeniden keşfedildi- Disassembly ile C kodu karşılaştırıldığında,
x20'ninenvirondizisini gezenepişaretçisine karşılık geldiği görüldü - Çöküş anında
x20değeri0x248b5000idi; o sıradakienvironise0x28655750adresindeydi, yani yaklaşık 60 MB ilerideydi - Eski ortam dizisinin çevresindeki bellek ile güncel
environkarşılaştırılınca son farkınSSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crtveSSL_CERT_DIR=/etc/ssl/certsgirdilerinde olduğu görüldü - Başka bir thread
setenv()çağrısı sırasındaenviron'ı taşımıştı vegetenv()eski ortam dizisini okumaya devam ederek bir use-after-free durumuna düşmüştü
openssl-probe ve TLS backend bağlantısı
rust-native-tlsile ilgili eski bir issue'da,openssl-probe'un sistem sertifikalarını bulmak içinSSL_CERT_FILEveSSL_CERT_DIRortam değişkenlerini ayarladığına dair ipucu bulundu- Linux'ta
rust-native-tlsinopensslbackend'i kullanıldığında bu yol çağrılıyordu - Sorunlu
openssl-probekodu, açık birunsafeolmadanenv::set_var()ile şu iki ortam değişkenini ayarlıyorduSSL_CERT_FILESSL_CERT_DIR
- Bu birleşim yüzünden unsafe içermeyen Rust kodu, aynı süreç içindeki
libckullanımıyla kötü etkileşime girerek çöküş üretti
Neden ARM64 Linux'ta yeniden üretilebildi
- Bu çöküşün oluşması için
setenv()'inreallocile ortam dizisini taşıdığı anda başka bir thread'ingetenv()çağırması gerekiyordu - Yeniden üretim için aynı anda birçok koşulun sağlanması gerekiyordu
- Ortam değişkeni sayısı
realloctetikleyecek düzeyde olmalıydı - İlgisiz bir I/O hatası
asynciotarafından yakalanmalıydı - Python hata işleme yolu,
LANGUAGEortam değişkenini almak için tam o andagetenv()çağırmalıydı
- Ortam değişkeni sayısı
- Hatalı
0x220değeri, 64 bit word cinsinden eski ortam boyutuna yakındı0x220 / 8 = 68- Bu değer eski ortam bloğunun sonundaki
NULLyerine yazılmıştı ve sistem malloc'unun free block boyutunu göstermek için kullandığı bir değer gibi görünüyordu
- Bu kadar çok önkoşul gerektiği için, tek bir platformda oldukça yeniden üretilebilir olması bile şanslı bir durumdu
ARM64 disassembly'de görülen ipucu
getenv()disassembly'sindex20'nin değiştiği nokta ilk başta açık görünmediği için kafa karıştırıcıydı- Kilit nokta, AArch64'ün pre-index addressing mode özelliğiydi
ldr x19, [x20, #8]!şu şekilde çalışırx19 = *(x20 + 8)x20 = x20 + 8
- Bu adresleme modu yüzünden
x20, solda açıkça yazılmasa da ortam değişkeni işaretçi dizisini dolaşıyordu
Uygulanan düzeltme ve ilgili projelerdeki değişiklikler
- Sonunda Linux'ta
reqwestinrust-native-tls/opensslbackend'i yerinerustlskullanmaya geçme kararı alındı - Başta native TLS backend'in seçilme nedeni, Python kodu Rust'a taşınırken iki TLS motorunu aynı anda paketlemekten kaçınmaktı
- Bu sorundan sonra, kısa vadede iki TLS motorunu birlikte taşımak kabul edilebilir bulundu
- Alternatif olarak
try_init_ssl_cert_env_vars()çağrısının ilk kez Python'un GIL'i elde tutulurken yapılması da mümkündü- Rust'ta, Rust kodunun kendi arasında ortam okuma ve yazma yarışlarını önleyen dahili bir kilit var
- Ancak bu kilit, başka dillerin doğrudan
libckullanmasını engellemez - GIL tutulursa en azından Python thread'leriyle yarış engellenebilir
- Rust projesi bu sorunun zaten farkında ve 2024 edition'da ortam setter fonksiyonlarını
unsafeyapmayı planlıyor - glibc projesi de yakın zamanda
reallockullanımından kaçınıp eski ortam dizilerini sızdırarakgetenv()in thread safety düzeyini artıran değişiklikler ekledi
4 yorum
Setenv thread-safe değil ve C bunu düzeltmek istemiyor
Setenv fonksiyonu yine sorun çıkarıyor.
Ben başlığı şöyle yazardım: 'C stdlib'in thread-unsafe olması, o güvenli denilen Rust'u bile kurtaramaz'. :)
Kesinlikle anladım.
Hacker News yorumları
Buradaki en büyük nokta, Rust’ın bir sonraki edition’ında ortam değişkeni ayarlama fonksiyonunun unsafe olacağı.
Şansımız yaver giderse, bu tür çökmelere yol açan crate’lere kadar etkisi iner gibi görünüyor; bu arada upstream’de şu issue açıldı: https://github.com/alexcrichton/openssl-probe/issues/30
getenvilesetenvveyaunsetenv’in farklı thread’lerden güvenli biçimde çağrılamaması gerçekten düzeltilmiş olmuyor.Güvenilir tek çözüm, bu fonksiyonların mutlaka bir mutex alacak şekilde değiştirilmesi gibi görünüyor.
Ne yazık ki ekosistem öyle değil: https://github.com/seanmonstar/reqwest/blob/master/Cargo.tom...
____UNSAFE_payattention__nevermindthatthisappears50timesinthisfile___gibi blokları ve önekleri görmezden gelecek şekilde eğitilmiş durumda.Web framework’lerinde de benzer; Vue’da
v-htmlyönergesi, React’te isedangerouslySetInnerHTMLvar. Bu açıdan Vue’nun kesinlikle daha iyi olduğunu düşünüyorum.Rust standart kütüphanesindeki
set_varveremove_var, bir sonraki edition olan 2024 edition’da artık düzgün biçimdeunsafe {}bloğu gerektirecek.Belgeler de artık güvenlik sorunundan bahsediyor; ama en başta bu fonksiyonları safe yapmak bir hataydı ve daha yüksek seviyeli dillerin de yaptığı bir hataydı.
https://doc.rust-lang.org/stable/std/env/fn.set_var.html
glibc’de, ortamın değiştirildiği daha fazla durumda
getenv’i güvenli hale getiren bir patch var; ancak C hâlâenviron’a doğrudan erişebildiği için, değişiklik yapılan durumlarda tamamen güvenli olamaz: https://github.com/bminor/glibc/commit/7a61e7f557a97ab597d6f...Aktif ortam değişkeni başına amorti edilmiş olarak sabit büyüklükte bir bellek sızıntısı oluşuyor; ama değişkenin kendisinde de zaten böyle bir sızıntı var, hatta uzunluğa bağlı olarak değişiyor ve artık kullanılmayan değerleri de içerdiği söyleniyor.
API açısından doğru programlarda bile bunun yüzünden belleğin sınırsız büyüdüğü patolojik kullanım senaryoları mutlaka vardır gibi geliyor.
API’yi birden fazla thread’den kullanarak kuralları ihlal eden programları düzeltmek için, API’ye uyan programlara sınırsız bellek artışı gibi bir bug sokuluyor olması ilginç ama rahatsız edici. Dogmatizmden çok pragmatizm hissi veriyor.
unsafegerektirmesi gerektiğini merak ediyorum.C standart kütüphanesi bakımcıları
setenv’i çok thread’li kullanıma güvenli hale getirmeye karşı çıksa bile, en azından POSIX içinde ya da önce fiilî bir standart oluşturup POSIX’in sonradan kabul etmesini sağlayarak yeni bir thread-safe API tanımlanmalı.Hiçbir şey yapılamamasının nedenlerini açıklamaya harcanan zamanla bu sorun düzeltilmiş olsaydı, eski
setenv’in yerini alabilecek ve birçok yazılım projesinde kullanımdan kaldırılıp kaldırılabilecek bir çözüm olurdu.glibc’nin bu sorunu fiilen ortadan kaldırmaya yönelik değişiklik yaptığını görünce, Musl bakımcısının Musl içinde düzeltilemeyeceği yönündeki sözü de pek ikna edici gelmiyor.
extern char **environ;’ın var olması.environherkese açık biçimde erişilebilir olduğu sürecesetenvvegetenv’in kullanıldığının bile garantisi yok; çünkü ikisi zorunlu değil.environkaldırılabilseydisetenvvegetenv’i thread-safe yapmak epey basit olurdu. Kaldırılamıyorsa imkânsız; yine de tam bir çözüm olmasa bilesetenvvegetenv’i thread-safe yapmanın bir iyileştirme olduğu savunulabilir.PATHiçinde arama yapan tümexec()fonksiyonlarının da kilit gerektireceği gibi görünüyor.Linux’ta ortam değişkenleriyle ilgili bir bug’a denk gelmek bir tür geçiş ritüeli gibi; diğer Unix’lerde ise tuhaf biçimde daha az sorun oluyor
Linus ve kernel, POSIX bug’larını pratik şekilde, gerçekten patlamayacak hâle getirerek düzeltiyor; glibc’nin ise insanların sorunu biraz olsun hafifletmeye çalışmasının üzerinden onlarca yıl geçmişken hâlâ geride kalması biraz komik
TZgibi türlü baş ağrıları olduğu doğru, amagetenv_r()sağlanıpsetenv()ile senkronize edilse vegetenv()kullanıldığında derleme/bağlama aşamasında sadece uyarı verilse bile sorunların önemli bir kısmı ortadan kalkardıDaha da ileri gidip ortam işaretçisini salt okunur tutan yazma sırasında kopyalama (COW) yaklaşımı bile uygulanabilirdi
Bunun yerine sorun tek tek uygulamalara yıkıldı; uygulama yazarları bağımlılıkların ne yaptığını neredeyse hiç bilemediği için bu büyük bir hata. Uzun zaman önce benim içinde bulunduğum durum da böyleydi; o sırada kapalı kaynak kütüphane tedarikçisi de o oyuncak Unix klonu Linux’u bırakmamı söylemişti
Sorun implementasyonda değil, API’nin kendisinde.
setenv(),unsetenv(),putenv()ve özellikleenviron, çok iş parçacıklı programlarda doğası gereği güvenli değilgetenv_r()de tamamen kurtaramaz. Çünkü bir iş parçacığı bir ortam değişkeninin eski değerini sağlanan arabelleğe kopyalarken başka bir iş parçacığısetenv()çağırabilirElbette
getenv()ile aldıktan sonra başka bir iş parçacığınınsetenv()çağırıp o belleği geçersiz kılması durumunugetenv_r()düzeltir; ancak API’yi bozan diğer çağrıları engellemenin bir yolu yoklibc,
getenv()/setenv()/putenv()/unsetenv()içinde mutex alarak bazı sorunları hafifletebilir; amagetenv()in döndürdüğü değerin çağıran kodun kullanabileceği kadar uzun süre geçerli kalacağını libc’nin garanti etmesinin hâlâ bir yolu yokenvirona doğrudan erişimi güvenli hâle getirmenin de iyi bir yolu yok.environiş parçacığına yerel yapılabilir, ama o zaman her iş parçacığının ortam görünümü kalıcı olarak birbirinden sapabilir vegetenv_r()çağrısının sonucu ileenvironu doğrudan kontrol etmenin sonucu farklı olabilirBurada geriye dönük uyumluluğu korumak gerçekten zor; yalnızca fonksiyonları koruyan bir mutex eklemek bile mevcut programların anlamını değiştirip onları bozabilir
Daha önce
setenvin berbat olduğuna dair bir yazı vardı: https://www.evanjones.ca/setenv-is-not-thread-safe.htmlTartışması da vardı ve daha ilk yorumdan itibaren Rust’ta sorun çıkardığından bahsediliyordu: https://news.ycombinator.com/item?id=38342642
Buradaki kalan sorunların çoğu geliştirme ortamı gibi görünüyor. Amazon veri merkezindeki uzak makinelerde Docker kullanarak test etmişler ve o makine süreç çökmelerini raporlayamamış
Üstelik konteyner içinde geri izleme almak için yeterli debug sembol bilgisi de yokmuş. İlk hatada temiz bir geri izleme alınsaydı hemen netleşirdi
En başta neden
setenvkullandıkları da ayrı bir soruBunu görünce, eski bazı meslektaşlarımın büyük inanç beslediği 12-factor app hareketi aklıma geldi. O “factor”lardan biri, uygulama yapılandırmasının ortam değişkenleriyle yapılması gerektiğiydi.
Bunu hep biraz aptalca bulmuşumdur; çünkü yapılandırma yöntemi, düz bir ad alanında string türündeki değerleri bir sepet gibi tutan bir yapıdan ibaret.
getenv()/setenv()/environtehlikesini de ortam değişkenlerini yapılandırma için kullanmamak adına güçlü bir gerekçe olarak görüyorum.Elbette her zaman mükemmel ve iyi desteklenen bir alternatif yok. Ben yapılandırma dosyalarını tercih ediyorum; geliştirme, staging ve production değerlerinin doldurulduğu şablon yapılandırmalar da kullanılabilir. Genelde eksileri ve tuzakları olsa da YAML kullanıyorum; daha iyi bir yapılandırma dosyası biçimi olabilir ama YAML’ın ortam değişkenlerinden çok daha iyi olduğunu düşünüyorum.
NT’de ortam değişkenleri tiplendirilebilir ve şablonlaştırılabilir; ayrıca ad alanları olan bir yapılandırma veritabanı, yani registry de var. Gerçi uzun uzadıya ve garip.
Üstelik MSVC, neredeyse tüm standart kütüphane fonksiyonlarının thread-safe sürümlerini sunuyor.
Yeni C/C++ geliştiricilerinin MSVC’nin POSIX uyumluluğu eksikliğinden yakındığını sık duyuyorum, ama bunun pratikte ne anlama geldiğini pek derinlemesine düşünmüyor gibiler. Daha çok 1990’larda yazılmış C programlarıyla çapraz uyumlu olmasını istemek gibi.
Bir fonksiyonun imzasından davranışını çıkarma becerisini zedeliyor ve saf fonksiyon olabilecek birçok fonksiyonu saf olmayan hâle getiriyor.
Herhangi bir proseste ortam değişkenlerinin kullanılamayacağını ve yalnızca bir kez, değişken bazında değil toplu olarak tek seferde okunabileceğini belirten bir dil özelliği olsaydı, onu her yerde kullanırdım.
getenv()kendi başına tamamen sorun değil; sorunsetenv().Teoride, o gizemli uygulama başlamadan önce ortam zaten ayarlanmış olacağı için buna ihtiyaç olmamalı.
Ama düz ad alanı, string değerler ve hangi kütüphane ve modüllerin içeri girdiğini bile bilmediğiniz, herkesin paylaştığı serbest bir global alan olması,
setenv()in güvenlik sorunu olmasa bile iyi bir fikir değil.Burada insanların bahsettiği sorunların çoğu, ortamı değiştirilebilir global durum için bir anahtar-değer deposu gibi kötüye kullanma isteğinden kaynaklanıyor gibi. Bunu neden yapmak istediklerini anlamıyorum.
JVM ortamı fiilen değişmez kabul ediyor; SoundCloud gibi Scala ve Java kullanan şirketlerin 12-factor app hareketini etkilemiş olması da mümkün. Ben ortamın değiştiği veya threading sorunu çıkardığı bir durum yaşamadım.
Ortam değişse bile JVM başlarken oluşturulan değişmez kopya aynı kalıyor; genel Java API’siyle ortamla etkileşen kod bu değişikliği görmüyor.
Yapılandırma dosyalarının sorunu, ayrıştırmanın proses başına yapılması. Linux/Unix’in bu kadar karmakarışık olmasının nedeni de bu. Her aracın yapılandırma gelenekleri ve mekanizmaları farklı, bir standart yok.
Docker ekosisteminde, konteynerin içinde ne yaptığınız bir yana, dış dünyayla arayüz ya bir volume mount edip her uygulamanın karmaşık yapılandırma yöntemini izlemek ya da sadece ortam değişkenlerini kullanmak oluyor.
Bugün Docker ile çalıştırılan modern yazılımların çoğu, davranışı tamamen ortam üzerinden kontrol edilebilecek kadar Docker dostu ve bu çoğu durumda yeterli.
Docker Compose veya Kubernetes kullanırsanız, prosesi başlatma biçimini tanımlayan ortam değişkenleri listesini bir YAML dosyası olarak tutarsınız; böylece bir ölçüde istediğiniz yapı ortaya çıkar. YAML’ı sevmiyorum ama yeterince iş görüyor; sözdizimi sorunlarının gününüzü mahvetme ihtimali yüksek olsa da alternatifler de sorunsuz değil.
Ben de 12-factor app tarzını kullanıyorum, ama uygulamaya girdikten sonra ortam değişkenlerini ve verileri doğrulayıp saklıyorum. Ondan sonra hiçbir sorun yok.
Göze pek çarpmayan bir bug’ın izini süren harika bir yazı.
Aralıklı bug, mimariye özgü durumlar, bağımlılıkların içine gizlenmiş olması, Rust, Python GIL, gettext; hepsi vardı.
Böyle ayrıntılı sorun giderme raporları, insanın bizzat yaşamasına en yakın kaynaklar. Bir bağımlılık onu kullanıyorken “X kullanmasaydın ya” demek kolay değil; bunu nasıl bilebilirdim ki?
“Gece CI makinesi Amazon AWS’de çalışıyor ve konteyner değil, gerçek root kullanıcısı kullanabilme avantajı var” derken aynı zamanda “konteynerin dışında gerekli dosyalar yok ve konteyner çok minimal olduğu için
gdbyi kolayca kuramıyoruz” diyorlar.İnsanlar artık bulut ve konteyner olmadan yerelde build edip debug etme becerisini mi kaybetti?
Çok basit bir işi yapmak için her türlü bulut karmaşıklığı ve dağıtım katmanı gerekli hâle geldi. PC devrimini %100 geri sarıp hantal ve pahalı mainframe computing dönemine dönmüş olduk.
Nedeni, paranın bulutta olması ve bulutun DRM olması. Yazılımı oraya koyarsanız abonelik ücreti alabilirsiniz; bundan kaçınılamaz ve kusursuz bir bağımlılığı sonsuza kadar sürdürebilirsiniz. Kullanıcının kendi verisini bile dışarı çıkaramadığı çok olur.
Ürün optimizasyonu için gerçek zamanlı analiz yapmak da kolaylaşır.
Bilişim mimarisi, iş modelinin aşağısında yer alır. Mainframe’in ilk kez ölmesinin nedeni internetin olmaması ve PC’nin daha ucuz olmasıydı; ama satıcıların bağımlılık yaratma gücünü de büyük ölçüde kaybetmiş olmalarıydı.
Şimdi çok daha kârlı bir modeli yeniden canlandırmanın yolu bulundu. Kullanıcıların can sıkıcı özgürlüğü ortadan kalktı; açıkçası kullanıcılar o özgürlüğe sahip olunca çoğu zaman para da ödemiyorlar, bu da kaliteli yazılım işini ayakta tutmayı zorlaştırabiliyor.
Yerelde çöküşü yeniden üretememiş olmaları çok olası. Geliştirici makinelerinin çoğu x86’ydı ve orada muhtemelen çökme olmuyordu.
Çökme yönetimini daha iyi yapmaları gerekirdi, ama onlar da bu sorunun farkında gibi ve burada ele alınan ana mesele bu değil.
Değiştirilebilir küresel durum kötüdür. Dost olan, dostunun değiştirilebilir küresel durum kullanmasına izin vermez
Ortam değişkenlerinden nefret ediyorum. Bu “Linux usulü” ama veba gibi kaçınıyorum. Şiddetle tavsiye ederim
libc korkunç ve dünyanın artık bunu geride bırakması gerekiyor
Sorun Linux, değiştirilebilir küresel durum ya da kaynaklar, libc de değil
Sorun, iş yerinde işi düzgün yapmak için zaman verilmemesi. Örneğin sorun patlamadan önce GDB ile yakalamak için yöneticinizin kodu ve o kodun dokunduğu her şeyi ısrarla debug edip geriye doğru iz sürmeye zaman tanıması gerekir
Yarı pişmiş koda çok fazla para akıyor. Üzücü ama gerçek
Bu tür sorunlar o kadar çoktu ki sonunda
LD_PRELOADilegetenv/setenv/putenv’i yamaladım