ディスカッション (42件)
(本文がありません。タイトルから内容を推測します)
環境変数は、アプリケーションの設定管理において長年使われてきた方法ですが、その複雑さや管理の煩雑さから、しばしば問題の種となります。本記事では、環境変数の歴史的背景、メリット・デメリット、そしてよりモダンな設定管理手法への移行について深く掘り下げて解説します。環境変数の「レガシーなごちゃごちゃ」を解消し、よりスマートな開発を目指しましょう。
めっちゃ分かりやすい解説!execveからスタックダンプへの流れとか、Bashの"export local"の癖とか最高だわ。
環境変数はみんなが思ってる以上に漏れるからね。/proc/<pid>/environ、docker inspect、CIログとか。だから、長期間使うシークレットはファイルとかシークレットボリュームに隠して、execする前にLD_*を消去するか、secure_getenvを使ってLD_PRELOADの予期せぬ事態を避けよう。
だから俺は環境変数を使って、マウントされたファイルを指すようにしてるんだ。あとはデバッグ用のスイッチとか。
良い指摘だね、「scrub LD_*」が万能薬じゃないってのはその通り。
言いたかったのは「これで解決!」ってことじゃなくて、あくまで「こういうのもあるの忘れずにね」って感じ。
ユーザーの環境を引き継ぐ子プロセスを起動する場合、問題はLD_PRELOADだけじゃなくて、環境全体が攻撃対象になるんだよね(LD_LIBRARY_PATH、PYTHONPATH、NODE_OPTIONSとかも)。
ちょっとでも特権が必要な処理をするなら、一番良いのはまっさらな状態から始めること。必要最小限の、許可リストに基づいた環境をゼロから構築して、それをexecve()に直接渡すのが正解。
そうすれば、「他の人の環境を壊す」問題も、「悪意のある再注入」問題も両方避けられるよ。
execの前にLD_*を消去…LD_PRELOADのサプライズを避けるために。
これは効果的なセキュリティ対策ではないことに注意。LD_PRELOAD
経由で自身を注入するライブラリは、明らかにexec*
をインターセプトして子プロセスに再注入することもできる。(完全に無害なLD_PRELOAD
ライブラリで、これと似たことをやったことがある。)
execの前にLD_*を消去するか、secure_getenvを使ってLD_PRELOADのサプライズを避ける。
それやると、他の人の環境を壊すことになるだけだよ。これらの環境変数はローダーによって読み込まれ、子プロセスを"セキュア"モードで実行すべきかどうかを確認するために、auxvでAT_SECUREなどをチェックして、LD_PRELOADを無視するんだ。
最高の解説。いつもどういう仕組みか疑問に思ってたんだ。ありがとう!
素晴らしい解説だね。環境変数が今の形になった理由と仕組みがずっと不思議だったんだ。すごく勉強になった!
まあ、POSIX全体がレガシーなゴミだって俺も思ってるけどね :D
マジかよ
君がこれからコードを書くであろう6つのプラットフォームのうち5つはPOSIXをサポートしてるぞ。DOSで作業したいのか?完璧だとは言わないけど、あんなに広範囲な標準化は二度とないと思うよ。
(Linux, BSD, iOS, Mac, Android)。DOSがどれかはわかるよね?
DOSで作業したいの?
そんなこと言ってないし、誰にも勧めないよ(笑)
君がコードを書く6つのプラットフォームのうち5つがPOSIXをサポートしている...完璧だとは言わないけど、これほど広範囲な標準化は二度と起こらないと思うよ。
(Linux, BSD, iOS, Mac, Android)。そしてDOSは推測できると思う。
わかってるよ。僕自身も"unixファン/unix哲学の提唱者"みたいなものだし。それが今できる最善のことだ。ただ、何かがとても広まって、とても使われて、とても普及して、とても"古く"なると、長年の多くの人々の選択の上に築かれたレガシーの塊になるんだよね。それは避けられないと思う。これはもっと小さい"エコシステム"でも起こる。
みんなに叩かれるだろうけど、coreutils
以外のプログラムの99%はおそらくLinuxとPOSIXが提供する機能の0.1%も使ってないってことに気づいてないんだよね。
君は間違ってない。むしろ、POSIX+Flat64BitMemoryは"現代的なアプリケーション"が上に構築される足場なんだ。これらの"現代的なアプリケーション"はLinuxの機能は必要なくて、ネットワーク接続とストレージがあればいい。POSIXは、これらの基本的なリソースをユーザースペースアプリケーションに提供する便利なプロバイダーにすぎない。
ああ、unixは長年、その上に色々なものを構築するのに十分なものだったし、これからもおそらく十分だろう。それが現実。世界は70年代のOSで動いてるけど、もっと良いものがないからそれでいいと思う。
ここで十分に強調されていないのは、環境変数をIPCに使っちゃダメってこと。プログラム起動時に変数を読んで内部状態を設定するくらいならまだしも、それ以上はやめとけ。問題の元になるだけ。setenv()を使おうと思ってるなら、考え直してくれ。どうしても使うなら、既存の変数を読み込んだ後、プログラムの先頭で実行してくれ。このインターフェース全体がPOSIXのゴミで、競合状態とか予期せぬ状態の無効化が起こりやすい。
それ考えたことなかったな。setenv()とgetenv()だけを使ってKafkaを書き直せるかも。
今でもKafkaよりは使いやすいんじゃないかな、たぶん。
職場でKafkaが導入された瞬間、「終わった…」って思ったね。
みんなDBのスイッチを切って、Kafkaを永続ストレージとして使い始めたんだ。Kafkaチームのリーダーは、バックアップさえなくて、アップデートに失敗したときに本番データの半分が消えてしまって、大騒ぎしてた。
UIアプリケーションからのKafkaイベントの送信待ちでUIアプリケーションが遅くなるほど、毎秒数万ものイベントが送信されてた。最終的に、Kafkaチームが実行していた8つのブローカーからのACKを待つのではなく、1つのKafkaブローカーからのACKだけを待つようにする素晴らしいフラグが見つかった。こうして、UIアプリケーションから送信されたKafkaイベントが時々消えるという事態が生まれたんだ。
正直言って、会社がある種の技術を人々に無理やり押し付けようとした瞬間、それはうまくいかないとわかる。誰も使い方やどこで使うべきかを知らない。誰も設定方法、エッジケース、そして4年間も放置されているバグを知らず、ただの混乱に終わるんだ。
マジかよ。Kafkaを永続ストレージとして使うほど間抜けなのはうちだけだと思ってた。この文章のすべてが心に突き刺さる。
まずはRedisを移植しようぜ。
Redisは既にMicrosoft ResearchのGarnetプロジェクトでどこでも実行できるように移植されてるよ。
それなら、Garnetをsetenv()とgetenv()で書き直す必要があるね。
西暦2060年、ついに人々は物体を過去に送る手段を開発した。彼らの最初の行動:グレッグ90がこのアイデアを投稿するのを阻止するために、サイバネティックな殺人マシンを送った。
🤷♂️🔫🤖 抹殺!
☠️
👻
ああ、このクソみたいな仕組みは全部30年前から受け継がれてて、スレッドが登場したのに全然変わってないんだよね。もしgetenv/setenvの動作をC標準ライブラリで完全に書き換えたら、動的にライブラリをリンクしてるアプリは、再コンパイルしなくてもそれを拾えるのかな?C標準ライブラリにはgetenv/setenvを使ってる関数が他にもあるから(特に、過去にRustの開発者を苦しめた時間サブシステム)、スレッドセーフなストレージに置き換えるのはマジで大変そうだよね。
問題は残念ながらAPIに組み込まれていて、実装の変更では解決できないんだ。getenv
を呼び出すと、内部構造への生のポインタが返される。setenv
またはunsetenv
を呼び出すと、その構造が変更される。関数内のロックだけでは、データ構造を変更しようとしている間に、別のスレッドでそれらの生のポインタが読み取られないことを保証できない。これを安全にする唯一の方法は、メモリリークさせることだ。なぜなら、ポインタが一度渡されると、APIはユーザーがいつそれを使い終わるかを知る方法がないから。
これを安全にする唯一の方法はメモリリークさせることだろう。なぜなら、ポインタが一度渡されると、APIはユーザーがいつそれを使い終わるかを知る方法がないから。
いやいや、他のCのAPIがやってるように、ユーザーにバッファを渡させてそこにコピーすればいいだけじゃん。バッファが小さすぎたらエラーにするけど、サイズを報告する(か、明確に定義された"最大"値を返す)。
これにはAPIの変更が必要だ。提案されている修正は、関数のシグネチャを同じに保つものだ。
ああ、APIを修正するのが理想的だね。APIを変更せずに実装を修正できるかどうかというコメントに返信していたんだ。それはメモリリークさせる場合にしかできない。
これも、すべてを.ymlファイルに保存して、それらを簡単に配布したり、そこから他のどんなターゲット形式にも変換したファイルを配布したりできるようにした理由の一つだよ。
良い解説だね。アプリで小文字の環境変数を使うってのに驚いた。すごく勉強になった。
とても良い記事だけど、罪のないStack Overflowの回答に対する不正確な批判に混乱してる:
StackOverflowやChatGPTで繰り返されている一般的な誤解は、POSIXが大文字の環境変数のみを許可し、それ以外は未定義の動作であるということだ。
いや、これはリンクされた回答が主張していることとは全く違う。自分で確認してほしい:その回答はこの件について何も主張しておらず、POSIX標準のセクション(記事でもその後引用されている)を引用しているだけだ。
IEEE Std 1003.1-2001のシェルおよびユーティリティボリュームのユーティリティで使用される環境変数名は、大文字、数字、および'_'(アンダースコア)のみで構成される[...]
それは大文字のみが許可されていると主張することとは全く異なり、回答は「未定義の動作」についてすら言及していない。
つまり、名前が有効であっても、シェルが文字、数字、アンダースコア以外のものをサポートしていない可能性があります。
それこそが、その主張を裏付ける答えのように聞こえるんだけど。
いや、それはリンク先の回答が主張していることとは全く違う。自分で確認してごらん。
「ChatGPTに自分の主張を裏付けるリンクを探させてる」ってのが見え見えだ。
昔、誤ってドットを含む環境変数名を使ってしまったことがある(テスト目的でファイル名を上書きするために、ファイル名から環境変数名を生成していたんだ)。
Pythonでは問題なく動作することが判明したが、Pythonがシェルスクリプトを呼び出し、そのシェルスクリプトがPythonを呼び出す場合、その環境変数は引き継がれなかった。(bashとdashのどちらが原因だったかは覚えてないけど)。
うちもそうだよ。そして、ああ、めちゃくちゃだよ。ほとんどの場合うまくいくけど、実際には全然信頼できない。他のツールはアクセスできないか、完全にドロップしてしまう。
イントロがいまいち気に食わない。
なんで何でもかんでも名前空間と型が必要なんだ?
型は大好きだけど、主にデータのバイナリ形式を表現するためだし、環境変数は文字列であるべきじゃん(例えば、バイナリ形式は文字の連続だよね)。
名前空間はプレフィックスで解決できることと変わらないし、環境変数に短い制限がない限り、大した問題じゃない。
もちろん、ここで指摘されている良い問題もあるよ。特に、ディスクへの書き込みを避ければ秘密が漏れないって勝手に思い込んでるところとか。でも、定義が簡単なツールの方が理にかなう場合もあると思う。
名前空間と型の議論は納得できなかったけど、ENV変数がどこにあるのか、存在すること(そして理想的には何をするのか、どんなユースケースなのか)を追跡できるのは便利だと思う。ユーザーがどこにあるかを知らずに変数をオーバーライドする場合を見るとかね。あと、デフォルトのENV変数には、その使い方を示す簡単なコマンドラインの方法が必要だと思う。例えば、
use_of TZ
と入力したら、こう表示されるべき。
"誰かさんがTZはタイムゾーンに必要だって考えたんだ。適当な値を設定するとプログラムが壊れる可能性があるよ。"
みたいな。現状、こういうインタラクティブな機能がなくて、manページとかに頼らないといけないと思う。
昔、bash/linuxでTZ変数を変更したことがあったな。
"エイリアス"みたいなのを使ってて、結局めちゃくちゃ変数を使うことになって、TZは.tar.gzのショートカットだったんだ。tar.xzに乗り換える前は、それをシェルスクリプトで使ってた。
で、TZは...タイムゾーンだってことが判明。今なら当たり前だと思う人もいるかもしれないけど、当時は知らなかった。ENV変数が...厄介だって気づいた最初の瞬間だった。
ENV変数を設定するとおかしくなる例は他にもたくさんある。長いENV変数はそれほど問題ないから、そっちに変えていったけど、TZみたいなのを変更してもシェルが警告してくれないのがやっぱり嫌だ。もっと良いシェルならそうしてくれるかもしれないけど、単純さからbashを使い続けてるんだよね。bashの開発者には、もっと色々考えて欲しい。まあ、僕が少数派だって考えることもできるけどね。ほとんどの人はTZなんて変更しないだろうし。でも、似たような例は他にもあって、bashは馬鹿みたいに喜んで処理を続けようとして、失敗することに気づきもしない。
基本的にENV変数はただのキーバリューマッパーだよね。最近は間接的に使ってて、システムの記述に使ってる色々なyamlファイルを、対応するシェル(例えば、windowsのcmderとかpowershellは別の形式が必要だったから、変換するrubyスクリプトを書いたんだ)に変換するrubyコンバーターとか。
一方、Bashでは、変数名に空白が許可されていないため、それを参照できません。
ここでよく使われる回避策はこんな感じだと思う。
FOO_BAR_BLA = 123
みたいな。大文字にして、単語の区切りに_を使う。
以前はFooBar = 123とかにしてたけど、結局大文字と_だけの方が好みになった。_の方が目が速い気がするんだ。
UTF-8の代わりに、POSIXで義務付けられているPortable Character Set (PCS) – 基本的に制御文字のないASCIIを使用します。
僕もなんとなくそうしてる。唯一のトレードオフは、名前がすごく長くなることかな。でも大したことじゃない。全部で1200個くらいのENV変数しかないと思う。ほとんどは必要なくて、単に便利だから使ってるだけ。例えば、
cd $MY_VIDEOS
が確実に動くようにするためとか。これをスクリプトでも使って、例えば、ENV['MY_VIDEOS']ディレクトリからすべてのファイルを取得したり。ENV変数が設定されてない時にどうするかは、まだ考えないといけないけどね。その場合はハードコードされたパスをデフォルトにして、.ymlファイルやコマンドラインでもオーバーライドできるようにするかな(必要で便利な場合に限るけど)。
セキュアな値に環境変数を使うのはずっと嫌だった。ソフトウェアではグローバル変数が有害だって言ってるのに、環境をそれとは違うものとして扱うのはなぜ?良い代替案が出てきたら喜んで乗り換えるよ。
注意すべきもう一つの落とし穴はint main(int argc, const char** argv, const char** envp)
だ。これはほとんどのC/C++コンパイラでサポートされている一般的な拡張で、これに依存していて、かつenviron
とsetenv
のPOSIX的な使い方を混ぜているソフトウェアを見たら、燃やしてしまえ。バグがあるから。