Heartbleedを出発点に
最近のOpenSSLのバグをめぐる強い反応はよく分かります。インターネット全体に突然パッチを当てなければならなくなるのは、楽しいことではありません。さらに個人的に引っかかったのは、バグがあまりにも単純だったことです。OpenSSLの開発者を責めたいわけではありませんが、OpenSSLのようなソフトウェアであれば、この手の問題はもう少し巧妙なものだろうと普通は考えます。通常はサニティチェックを*正しく*行えなかったという形で失敗するのに対し、今回のバグはmemcpy()呼び出しに境界チェックが*まったく存在しない*というものだったからです。 とはいえ、朝になって前夜に自分が書いたコードを読み返し、深く恥じ入ることもあります。プログラマは誰でも間違えますし、私自身しょっちゅう間違えます。だから必要なのは別のOpenSSLチームではなく、別のプロセスなのだろうと思います。 Cよりも安全な別の言語にすべきだと言う人もいれば、仕様が複雑すぎて壊れているのだと主張する人もいます。どちらの意見にも一理あるのでしょう。しかし、すぐに別の仕様やシステム言語に移行できるとは考えにくいです。だとすれば本当の問いは、今すぐシステムソフトウェアの安全性を高めるために何ができるか、ということです。 1) 資金を投じる。 投資さえあれば、システムのコードをより安全にすることは単純です。さまざまな企業がセキュリティ専門家を雇ってOpenSSLのコードベースを監査すれば、Heartbleedのようなバグが見つかる可能性は高まります。 私は、自明ではない複数の条件が重なって初めて発動するような非常に複雑なバグでさえ、真剣なコード監査によって発見されるのを見てきました。境界チェックのないmemcpy()のようなものは、セキュリティの観点からコードを分析すれば、一読しただけで目立つはずです。ではHeartbleedはどうやって発見されたと思いますか? Googleが行ったセキュリティ監査によってです。 オープンソースを、ただ受け取るだけのものと考える時代はもう終わったのかもしれません。多くの企業がGoogleなどの例に倣い、OSSの開発やセキュリティのために人材を投じるべきです。 2) 静的・動的チェック。 静的コード解析は、副次的にコード監査を半自動化する方法でもあります。OpenSSLのような重要なシステムコードであれば、静的解析をより効果的にするためにソースコードにアノテーションを付けたり、一連のルールを適用したりすることさえ、十分に許容されます。 今の静的解析ツールが完全な解決策ではないのは確かですが、専門的なプログラマがその出力を注意深く精査すれば、一定の価値は得られます。 もう一つの大きな助けになるのが、Valgrindのような動的チェックです。Cで書かれたシステムソフトウェアは、すべての新しいコミットで自動的にValgrindを使ってテストすべきです。 3) ライブラリでCを抽象化する。 Cは低水準で、言語自体に安全性の仕組みがありません。しかしCの良いところは、そのむき出しの部分の上にレイヤーを構築できることです。 まともな動的文字列ライブラリがあれば、多くのバッファオーバーフローを防げますし、今ではまともなプロジェクトならほぼどれも使っています。しかし、それ以外にもできることはあります。たとえば秘密鍵のようなものを含む可能性のあるメモリを扱うセキュリティ上重要なコードでは、暗黙的にサニティチェックを行いながら一方のバッファから他方へだけコピーするメモリコピープリミティブを、動的文字列ライブラリに追加できます。 さらに、バッファに重要なデータが含まれている場合は、論理的なパーミッションを設定し、この領域からのコピーを試みたときにプログラムを中断させることもできます。メモリ管理を使って重要なメモリページをさらに効果的に保護する、より移植性の低い方法もあります。しかし、移植性や予測可能性の観点から、現実的にはCレベルでのより高次の保護の方がはるかにシンプルな場合もあります。 一般に、保護なしでCを使うことを避け、より安全にプログラミングできるよう、その上に抽象化するライブラリを作ることで探求できることは数多くあります。 4) ランダム化テスト。 単体テストでは、エッジケースやサニティチェックの失敗を引き起こすことはまずありません。何十年も前から知られているテスト手法で、私の意見では十分に使われていないものがあります。ファジーテストです。 OpenSSLのバグは、さまざまなランダム化されたパラメータを持つ異なる種類のOpenSSLパケットを送信することで、Valgrindのような動的解析ツールと組み合わせれば、間違いなく発見可能でした。 私の経験では、大量のランダム化テストを用意し、同じテストをValgrind上でプログラムを動かしながら何度も繰り返し実行する環境があれば、そうしなければ見逃されてしまうような現実世界のバグを数多く発見できます。探求すべきモデルはたくさんありますが、通常は完全にランダムなデータを注入するものと、有効なパケットをさまざまなランダムな方法で破損させる中間的なモデルが必要です。 この手法の典型例が、昔のDNS圧縮の無限ループのバグです。素朴な実装にランダムなパケットをいくつか投げ込めば、数分で見つかります。 5) セキュリティとパフォーマンスに関する考え方を変える。 OpenSSLが一部のシステムでmalloc/freeが遅いという理由で独自のアロケーションのキャッシュを行っているのは興味深いことです。これは、セキュリティ上重要なコードにおいてさえ、依然として安全性よりもパフォーマンスが過剰に尊重されている兆候です。この件に関して言えば、OpenSSLの開発者がmallocをラップしたときに、そうすることによるセキュリティへの影響を考えもしなかったのは確かでしょう。しかし、*一部の*システムにおけるアロケーション関数のような低水準の詳細を気にしたという事実自体が、パフォーマンスへの深い関心の表れであり、本来はシステムの正確性や安全性をもっと深く懸念すべきだったのです。 一般に、今日のサーバーインフラの事実上の標準であるシステム、すなわちLinuxが、主にライセンス上の懸念から、他ではなかなか見ないほど出来の悪いアロケータを抱えてきたし、今も抱えていることは、状況を助けていません。より優れたアロケータはGPLではなくBSDライセンスだからです。 おそらく大企業が貢献すべきもう一つの領域が、glibcのmallocの大幅な改善でしょう。より良い選択肢があるとしても、現実の多くのシステムソフトウェアが結局使うことになるのはglibcのmallocなのです。 Heartbleedについての議論が、より現実的なアプローチを取るようになることを願っています。なぜなら一つだけ確かなことがあるからです。あちこちを非難しても、OpenSSLや他の何かの実際のセキュリティ水準が変わることはありません。そして将来には新たな課題が控えています。たとえばHTTP/2.0の実装は、セキュリティの観点から非常にデリケートな局面になるかもしれません。 追記: 実は私の認識は誤りで、Glibcの中のmalloc実装はBSDライセンスでした。したがってライセンスの問題ではありません。なぜGlibcが非常に優れていて活発に開発されているアロケータであるJemallocを使っていないのか、私には分かりません。
記事をランダムに読む