- C dilindeki
setenv() ve unsetenv() fonksiyonları, thread kullanan programlarda güvenle kullanılamıyor
- Bu fonksiyonlar global durumu değiştiriyor ve başka bir thread
getenv() çağırdığında çakışmaya yol açabiliyor
- Go’daki
os.Setenv ve Rust’taki std::env::set_var() gibi C standart kütüphanesi fonksiyonlarını kullanan diğer dillerde de çakışmalar ortaya çıkıyor
- Go programında ilgili sorunun izini sürmek ve bug raporu hazırlamak 2 gün sürdü
- Çünkü Go’nun DNS resolver’ı dahili olarak
getaddrinfo() kullanıyor ve bu da getenv() çağırıyor
- Ama bu sorun çok eski
- 2017’de de bununla ilgili bir yazı var ve yazının sonunda “5 yıl sonra 2022’de görüşürüz!” deniyor; konu 2023’te yeniden karşımıza çıkmış
- Bu, POSIX standardındaki bir kusur; C standardı genişletilerek environment variable’ların değiştirilebilmesi sağlanmış
- En sinir bozucu kısım, standartları etkileyebilen ya da C kütüphanelerini sürdüren birçok kişinin bunu sorun olarak görmemesi
- Bunun nedeni, belirtimde
setenv()’in thread’lerle birlikte kullanılamayacağının açıkça yazılı olması
- Yani biri bunu yaparsa çakışma onun suçu sayılıyor
- O halde “tüm fonksiyonların belirtimlerini dikkatle okuyalım, başkalarının yazdığı yazılımları kullanmayalım ve thread kullanmayalım” deniyor
- Ama modern yazılımda bu gerçekçi bir varsayım değil
- Bunun yerine, bozması daha zor olan ve ekosistemdeki değişimlere göre evrilen API’ler geliştirmeye çalışmak gerekiyor
- C dili ve standart kütüphanesi, yazılımların büyük bölümünün temelinde hâlâ önemli bir rol oynuyor; bu yüzden ya iyileştirmenin bir yolunu bulmalı ya da terk etmenin bir yolunu bulmalıyız
setenv() neden thread-safe değil?
getenv() bir char* döndürüyor ve uygulamanın bunu daha sonra serbest bırakması gerekmiyor
- Bir thread bu pointer’ı kullanırken başka bir thread
setenv() veya unsetenv() ile aynı environment variable’ı değiştirebiliyor
- C standardında yalnızca
getenv() bulunuyor; ancak çoğu implementasyon POSIX standardını izlediği için environment’ı değiştiren fonksiyonları da içeriyor
putenv(), environment variable kümesine bir char* ekliyor; uygulama putenv() döndükten sonra bu belleği değiştirirse environment variable da değişiyor
environ, uygulamanın okuyabildiği ve atama yapabildiği, NULL ile sonlanan bir pointer dizisi (char**); bu diziye erişim thread-safe değil
Environment variable’lar nasıl implemente ediliyor?
- Uygulama mevcut bir değişkenin üzerine yazdığında implementasyonun bunu nasıl ele alacağına karar vermesi gerekiyor
- glibc ile Solaris/Illumos, environment variable’ları asla serbest bırakmıyor; bu yüzden
getenv() tarafından döndürülen değer sabit kalıyor ve thread’ler arasında güvenle kullanılabiliyor
- musl ile FreeBSD/Apple ise environment variable’ları serbest bırakıyor; başka bir thread
setenv() çağırdıktan sonra getenv()’in döndürdüğü pointer’ı kullanmak çakışmaya yol açabiliyor
- Environment variable kümesinin thread-safe biçimde güncellendiğinin garanti edilmesi ikinci sorun ve glibc’deki çakışmanın nedeni de bu
Programlar environment variable’ları neden kullanır?
- Environment variable’lar, başka programlara gömülü paylaşımlı kütüphaneleri veya dil runtime’larını yapılandırmak için kullanışlıdır
- Kullanıcılar, program yazarının yapılandırmayı açıkça iletmesine gerek kalmadan ayarları değiştirebilir
- Pek çok kütüphane
getenv() çağırır ve programlar kullandıkları kütüphaneleri yapılandırmak için bu değişkenleri değiştirmek zorunda kalır
Bu sorun çözülmeli ve şu yollar izlenebilir
- Bana göre bunun uzun zamandır bilinen bir sorun olması akıl almaz
- Sorunu debug etmek veya geçici çözümleri tartışmak için binlerce saat boşa harcandı
- Sorunu çözme yolları
- Illumos/Solaris’teki gibi thread-safe bir implementasyon yapmak
- Bunun bazı sınırları var.
setenv() içinde memory leak oluşuyor ve program putenv() ya da environment variable’ları kullanıyorsa yine de güvenli olmuyor
- Yine de bu, mevcut Linux ve Apple implementasyonlarına göre bir iyileştirme
- İkinci yol ise Microsoft’un
getenv_s() fonksiyonuna benzer şekilde, tasarım gereği thread-safe olan ve tüm environment variable’ları okumayı sağlayan yeni bir API eklemek
- Benim tercih ettiğim çözüm ikisini birden yapmak
- Böylece mevcut programlar ve kütüphaneler için sorun çıkma olasılığı azaltılırken, Go ve Rust gibi yeni kodlar veya diller için sorunun tamamen önüne geçilecek bir yol da sunulmuş olur
getenv_s() benzeri şekilde, tek bir environment variable’ı kullanıcının sağladığı buffer’a kopyalayan bir fonksiyon eklemek
- Tüm environment variable’lar üzerinde dolaşmayı veya hepsini kopyalamayı sağlayan thread-safe API’ler eklemek
getenv()’i artık önerilmeyen olarak işaretleyip yerine thread-safe yeni bir getenv() fonksiyonunu tavsiye etmek
putenv()’i artık önerilmeyen olarak işaretleyip yerine setenv()’i tavsiye etmek
environ kullanımını artık önerilmeyen olarak işaretleyip yerine environment variable fonksiyonlarını tavsiye etmek
- Environment variable implementasyonunu thread-safe olacak şekilde güncellemek
3 yorum
"Spesifikasyonda,
setenv()iş parçacıklarıyla birlikte kullanılamayacağının açıkça belirtilmiş olması" ==> API ya da SDK kullanırken specification'ı dikkatle kontrol etmenin temel kuralı tam da budur. Bana göre bu, zorla kullanmaya çalışmaktan başka bir şey değil.Sorun, en baştan kötü tasarlanmış bir özelliği kullanmakta.
....