*sshole、あるいはC言語からいつ解放されるのか?
*sshole, or when are we going to get rid of C? (sunshowers.io)
要約
この記事は、musl libcにおけるパスワードハッシュの脆弱性(*sshole)を例に、C言語の型システムの限界と、それがもたらすエンジニアリング文化の問題点を指摘しています。C言語ではエラー処理の不備が深刻な脆弱性につながりやすい一方、Rustのような言語では型システムとコミュニティの規範がより安全なコード記述を促進すると論じています。また、AIエージェントによる開発においても、言語の型システムとコミュニティの規範が依然として重要であると主張しています。
全文翻訳
*sshole、あるいはC言語からいつ解放されるのか?
[画像: バスに乗る幸せそうな人と悲しそうな人のミーム。悲しそうな人には「4」、幸せそうな人には「A」が257個並んだ文字列がキャプションされている。]
Original art by Genildo Ronchi.
またしても、コアなC言語コードに[また別の脆弱性]が見つかりました。今回は、同じセンチネル値(番兵値)を2つの異なる条件で使用したことが原因です。
- Unixでは、パスワードハッシュが「*」であることは、古くからアカウントがロックされていることを意味していました。
- 2012年以降、musl libcの
crypt関数は、ハッシュするには長すぎるパスワードに対して「*」という文字列を返していました。
この2つの点が組み合わさり、さらにsshdがロックされたアカウントへのログイン試行をハッシュしてタイミングリークを防ぐという動作と相まって、PAMを使用しないmuslシステム(例えば、公式のAlpine AWSイメージなど)では、認証されていないユーザーが(おそらくroot権限で!)シェルにアクセスできるようになってしまいました。詳細は[このAlpineの勧告]を参照してください。
これはメモリ安全性に関する脆弱性ではありません。むしろ、より優れた型システムを持つ言語でも防げない問題だと人々がよく言うタイプのものです。これは真実です!パスワードが長すぎる場合に「*」を返すRust関数を書くことは十分に可能です。しかし、この点においてCとRustの大きな違いは、Rustコミュニティの規範がそのようなことを許さないということです。
POSIX仕様はエラーケースを考慮しようとしています。仕様にはこう書かれています。
成功した場合、crypt()はハッシュされたパスワードへのポインタを返します。[…] それ以外の場合、NULLポインタを返し、エラーを示すためにerrnoを設定します。
しかし、Cの型システムは開発者にNULLの戻り値を確認することを強制できません。musl libcの開発者は、一部のバイナリがそれをチェックしていないことに気づき、その結果、仕様から逸脱してセンチネル値「*」を返すようになりました。
以前にも書いたように、型システムはエンジニアリング文化に強い影響を与えます。C言語には代数的データ型や結果型がないため、Cコミュニティを中心に形成されるエンジニアリングの規範は、このようなインバンドシグナリング(帯域内シグナリング)につながります。一方、Rustでは、長すぎるパスワードはエラーバリアントとして返され、呼び出し元はそれを処理することを強制されます。もし今日エラーバリアントが存在しない場合、破壊的な変更でエラーバリアントが追加され、RustSecの勧告も公開されるでしょう。
余談ですが、Linuxディストリビューションのメンテナーが、プロジェクトがRustの依存関係を導入することについて不満を言うのを聞くと、今でも驚かされます。エンジニアリングの規範がユーザーにより良いサービスを提供する可能性のある新しい世界に適応するのではなく、これらのメンテナーは世界が自分たちに適応することを主張します。コンパイル時間やニッチなアーキテクチャへの移植性は確かにコストですが、あなたは自分の利益のためにここにいるのですか、それとも大多数のユーザーのためにここにいるのですか?ここには真の[プリンシパル・エージェント問題]が起こっています。
エージェントはあなたを救わない
エージェントといえば!最近、次のような議論があります。
- Rustは、今日、人間が正しいソフトウェアを書くのに役立つことは認められている。
- LLMは、現在、または近い将来、メモリ安全性やその他のエラーケースを直接推論できるようになる。
- したがって、エージェント中心の開発ライフサイクルでは、RustとC(あるいはアセンブリ)の間にはソフトウェアの厳密さにおいて違いはない。
- しかし、Cはビルド速度や移植性などの点でRustよりも利点がある。
- だから、ソフトウェアをC(あるいはアセンブリ)で書くのも悪くない。
この議論には、決定的なアプローチがすでに機能することが示されているのに、あいまいなプロセスを信頼するという疑わしい決定を含め、多くの問題があります。しかし、決定的に欠けているのは、それぞれのコミュニティの規範です!あなたが(またはあなたのエージェントが)Rustソフトウェアを書くとき、あなたはおそらくRustライブラリを使用するでしょう。そして、あなたのエージェントはおそらくトレーニングセット内の既存のRustコードをパターンマッチするでしょう。その結果、たとえあなたがすでに熟練したRust開発者でなくても、型システムにエンコードされた不変条件がより多く存在する可能性が高いです[1]。
C言語開発者のエンジニアリング規範は、「*」をエラーを示す許容可能なセンチネル値として許容しますが、Rust開発者のエンジニアリング規範はそれを許しません。これらの規範はコミュニティの累積的な貢献の結果であり、ソフトウェアプロフェッショナルとしての責任の一部は、自分の仕事が属するコミュニティの種類について考えることです。しかし、一部の人々は、この輝かしい新世界ではこれらはほとんど重要ではなく、他人の貢献はほとんど無視できると判断しました。彼らの傲慢さが彼らの破滅となるでしょう。
- もしあなたが熟練したRust開発者であれば、エージェントがエンコードする最初の不変条件は非常に不十分であることに気づき、それらの問題が修正される長いレビューサイクルを経ることになるでしょう。