- Fungsi
setenv() dan unsetenv() dalam bahasa C tidak aman digunakan pada program yang memakai thread
- Fungsi-fungsi ini memodifikasi status global, dan dapat menimbulkan konflik saat thread lain memanggil
getenv()
- Konflik juga terjadi di bahasa lain yang menggunakan fungsi pustaka standar C, seperti
os.Setenv di Go dan std::env::set_var() di Rust
- Dibutuhkan 2 hari untuk melacak masalah terkait dan melaporkan bug di program Go
- Karena resolver DNS internal Go menggunakan
getaddrinfo(), dan itu memanggil getenv()
- Namun masalah ini sudah sangat lama. Ada tulisan terkait pada 2017, dan di bagian bawah tulisan itu tertulis “sampai jumpa lagi 5 tahun kemudian pada 2022!”, lalu benar-benar bertemu lagi pada 2023
- Ini adalah cacat pada standar POSIX, yang memperluas standar C agar memungkinkan modifikasi variabel lingkungan
- Bagian yang paling menjengkelkan adalah, banyak orang yang bisa memengaruhi standar atau memelihara pustaka C tidak menganggap ini sebagai masalah
- Alasannya karena spesifikasi sudah dengan jelas menyatakan bahwa
setenv() tidak boleh digunakan bersama thread
- Jadi jika seseorang tetap melakukannya, crash dianggap sebagai kesalahan mereka
- Jadi kita seolah harus “membaca spesifikasi semua fungsi dengan saksama, tidak memakai perangkat lunak yang ditulis orang lain, dan tidak memakai thread”
- Tetapi ini adalah asumsi yang tidak realistis dalam perangkat lunak modern
- Daripada itu, kita seharusnya berupaya membuat API yang lebih sulit disalahgunakan dan bisa berevolusi mengikuti perubahan ekosistem
- Bahasa C dan pustaka standarnya tetap memainkan peran penting sebagai fondasi sebagian besar perangkat lunak, jadi kita harus menemukan cara untuk memperbaikinya atau mencari cara untuk meninggalkannya
Mengapa setenv() tidak Thread-Safe
getenv() mengembalikan char*, dan aplikasi tidak perlu membebaskannya nanti
- Saat satu thread sedang memakai pointer ini, thread lain dapat mengubah variabel lingkungan yang sama dengan
setenv() atau unsetenv()
- Standar C hanya mencakup
getenv(), tetapi sebagian besar implementasi mengikuti standar POSIX dan menyertakan fungsi untuk memodifikasi lingkungan
putenv() menambahkan char* ke kumpulan variabel lingkungan, dan jika aplikasi memodifikasi memori setelah putenv() mengembalikan hasil, variabel lingkungan itu juga ikut berubah
environ adalah array pointer yang diakhiri NULL (char**) yang dapat dibaca dan di-assign oleh aplikasi, dan akses ke array ini tidak thread-safe
Cara implementasi variabel lingkungan
- Saat aplikasi menimpa variabel yang sudah ada, implementasi harus memutuskan bagaimana menanganinya
- glibc dan Solaris/Illumos tidak pernah membebaskan variabel lingkungan, sehingga nilai yang dikembalikan dari
getenv() bersifat tidak berubah dan bisa dipakai dengan aman antar-thread
- musl dan FreeBSD/Apple membebaskan variabel lingkungan, sehingga bisa terjadi crash jika pointer yang dikembalikan dari
getenv() dipakai setelah thread lain memanggil setenv()
- Menjamin agar kumpulan variabel lingkungan diperbarui secara thread-safe adalah masalah kedua, dan hal ini menyebabkan crash di glibc
Mengapa program memakai variabel lingkungan
- Variabel lingkungan berguna untuk mengonfigurasi shared library atau language runtime yang disertakan dalam program lain
- Pengguna dapat mengubah konfigurasi tanpa penulis program harus secara eksplisit meneruskan konfigurasi tersebut
- Banyak library memanggil
getenv(), dan program perlu mengubah variabel-variabel ini untuk mengonfigurasi library yang mereka gunakan
Masalah ini harus diperbaiki, dan caranya bisa seperti berikut
- Menurut saya, tidak masuk akal bahwa ini telah lama diketahui sebagai masalah
- Ribuan jam terbuang untuk men-debug masalah ini atau mendiskusikan cara mengatasinya
- Cara menyelesaikan masalah ini
- Membuat implementasi yang thread-safe seperti Illumos/Solaris
- Ini tetap punya sedikit keterbatasan. Ada kebocoran memori di
setenv(), dan tetap tidak aman jika program memakai putenv() atau environ
- Tetapi ini tetap lebih baik dibanding implementasi Linux dan Apple saat ini
- Kedua, menambahkan API baru untuk mengambil semua variabel lingkungan yang thread-safe by design, seperti
getenv_s() milik Microsoft
- Solusi yang saya sukai adalah menggunakan keduanya
- Ini mengurangi kemungkinan masalah pada program dan library lama, sekaligus menyediakan jalur untuk sepenuhnya menghindari masalah pada kode atau bahasa baru seperti Go dan Rust
- Menambahkan fungsi, mirip
getenv_s(), yang menyalin satu variabel lingkungan ke buffer yang ditentukan pengguna
- Menambahkan API thread-safe untuk mengiterasi semua variabel lingkungan atau menyalin semuanya
- Menandai
getenv() sebagai tidak lagi direkomendasikan, dan sebagai gantinya merekomendasikan fungsi getenv() baru yang thread-safe
- Menandai
putenv() sebagai tidak lagi direkomendasikan, dan sebagai gantinya merekomendasikan setenv()
- Menandai
environ sebagai usang, dan sebagai gantinya merekomendasikan fungsi-fungsi variabel lingkungan
- Memperbarui implementasi variabel lingkungan agar thread-safe
3 komentar
"Karena spesifikasi secara jelas menyatakan bahwa
setenv()tidak bisa digunakan bersama thread" ==> saat menggunakan API atau SDK, wajib memeriksa specification/spesifikasi dengan teliti adalah hal yang paling mendasar. Ini terlihat tidak lebih dari sekadar pemaksaan penggunaan.Masalahnya adalah menggunakan fitur yang sejak awal dirancang dengan buruk
....