- C言語の
setenv()およびunsetenv()関数は、スレッドを使用するプログラムでは安全に使えない
- これらの関数はグローバル状態を変更し、別スレッドで
getenv()が呼ばれたときに衝突を引き起こす可能性がある
- Goの
os.SetenvやRustのstd::env::set_var()のように、C標準ライブラリ関数を利用する他言語でも衝突が発生する
- Goプログラムで関連する問題を追跡し、バグを報告するのに2日かかった
- GoのDNSリゾルバ内部で
getaddrinfo()を使っており、これがgetenv()を呼ぶため
- しかしこの問題は非常に古い。2017年にも関連する記事があり、記事の末尾に「5年後の2022年に会いましょう!」と書かれていたのに、2023年にまた再会した
- これはPOSIX標準の欠陥であり、C標準を拡張して環境変数を変更できるようにしたことに起因する
- 最も腹立たしいのは、標準に影響を与えたりCライブラリを保守したりできる多くの人たちが、これを問題だと思っていないこと
- その理由は、仕様に「スレッドと一緒に
setenv()を使えない」と明確に書かれているから
- したがって、誰かがこれを行ってクラッシュしたなら、その人の責任だということになる
- つまり私たちは「すべての関数の仕様を注意深く読み、他人が書いたソフトウェアを使わず、スレッドも使わない」べきだということになる
- しかし現代のソフトウェアでは、これは非現実的な前提である
- それよりも、壊しにくくエコシステムの変化に合わせて進化するAPIを作る努力をすべきだと考える
- C言語と標準ライブラリは、ほとんどのソフトウェアの基盤として今後も重要な役割を果たし続けるのだから、改善する方法を見つけるか、さもなくば捨てる方法を見つけるべきである
なぜsetenv()はスレッドセーフではないのか
getenv()はchar*を返し、アプリケーションは後でそれを解放する必要がない
- あるスレッドがこのポインタを使っている間に、別スレッドが
setenv()またはunsetenv()を使って同じ環境変数を変更できてしまう
- C標準には
getenv()しか含まれていないが、ほとんどの実装はPOSIX標準に従い、環境を変更する関数を含んでいる
putenv()は環境変数集合にchar*を追加し、アプリケーションがputenv()の返却後にそのメモリを変更すると、環境変数も変更される
environは、アプリケーションが読み取りや代入を行えるNULL終端のポインタ配列(char**)であり、この配列へのアクセスはスレッドセーフではない
環境変数の実装方式
- アプリケーションが既存の変数を上書きするとき、実装はどう処理するかを決めなければならない
- glibcとSolaris/Illumosは環境変数を決して解放せず、
getenv()が返した値は不変であり、スレッドでも安全に使える
- muslとFreeBSD/Appleは環境変数を解放するため、別スレッドが
setenv()を呼んだ後にgetenv()が返したポインタを使うとクラッシュする可能性がある
- 環境変数集合がスレッドセーフに更新されることを保証するのが第2の問題であり、これによってglibcでもクラッシュが発生する
プログラムが環境変数を使う理由
- 環境変数は、他のプログラムに含まれる共有ライブラリや言語ランタイムを設定するのに便利である
- ユーザーは、プログラム作者が明示的に設定を渡さなくても設定を変更できる
- 多くのライブラリが
getenv()を呼び出し、プログラムは利用するライブラリを設定するためにこれらの変数を変更する必要がある
この問題は解決すべきであり、次のような方法がある
- これが長年知られていた問題だったというのは、私には信じがたい
- 問題のデバッグや回避策の議論に何千時間も無駄にされてきた
- 問題を解決する方法
- Illumos/Solarisのようにスレッドセーフな実装を作る
- これには多少限界がある。
setenv()でメモリリークが起き、プログラムがputenv()またはenvironを使う場合は依然として安全ではない
- それでも現在のLinuxやAppleの実装よりは改善になる
- 2つ目は、Microsoftの
getenv_s()のように設計上スレッドセーフな、全環境変数取得のための新しいAPIを追加すること
- 私が好む解決策は、その両方を使うことだ
- 既存のプログラムやライブラリで問題が起きる可能性を減らし、GoやRustのような新しいコードや言語では問題を完全に回避できる道を提供する
getenv_s()と同様に、1つの環境変数をユーザー指定バッファにコピーする関数を追加する
- すべての環境変数を列挙したり、すべての変数をコピーしたりできるスレッドセーフAPIを追加する
getenv()を非推奨として扱い、代わりにスレッドセーフな新しいgetenv()関数を推奨する
putenv()を非推奨として扱い、代わりにsetenv()を推奨する
environを非推奨として扱い、代わりに環境変数関数を推奨する
- 環境変数の実装をスレッドセーフになるよう更新する
3件のコメント
「仕様で、
setenv()をスレッドと一緒に使用できないことが明確に記されているから」==> API や SDK を使う際には、必ず specification の記述を細かく確認しなければならないというのが基本中の基本です。無理やり使っているようにしか見えません。そもそも設計がまずい機能を使うこと自体が問題だ
setenv はスレッドセーフではなく、C はこれを修正したくない
....