「バグを意図的にバグのまま残す」という選択肢がある
はじめに
gcc v12.1において、C++の正規表現ライブラリstd::regexに、正規表現のバリデーションを改善するパッチ(以下"改善パッチ"と表記)が取り込まれました。改善パッチによって、これまではバリデーションにひっかからなかった不正な正規表現文字列が"正しく"不正なものと認識されて例外が発生するようになりました。
これだけ聞けばいいことだけのように思えるかもしれませんが、実はそうでもなかったりします。経験豊富なかたであれば見た瞬間ゾッとしたかもしれません。本記事では、この一見問題なさそうな改善パッチによって発生しうる問題、および、その具体的例について紹介するとともに、この手のパッチを当てるかどうかは難しい判断になるという知見を共有します。
結論
- 改善パッチによって発生する問題
- 発生条件
- gcc v12.1以降、あるいは改善パッチをバックポートされた任意のバージョンを使ってC++プログラムをビルド
- C++のstd::regexを使っている
- 「"["と"]"の間に"\w"のような文字クラスを指定する(例:
[\w-a]
)」というstd:regexからすると不正な正規表現文字列を与えているコードがある
- 発生する問題
- 不正な正規表現を与えたときに、これまでは何も起きていなかったところで例外が発生するようになる。この例外を処理していない場合はプログラムのクラッシュなどに至る
- 発生条件
- 知見
- 不具合を修正すると、不具合を前提として動作していたコードが動かなくなって、別の問題を引き起こすことがある
- たとえば分散ストレージCephはこの問題が発生するv17.2.2を使うとクラスタが機能しなくなる
- 本来間違っているのは不正な使い方をしている側だが、ときとして修正によって起こる、より大きな問題を防ぐために、バグをバグのまま残しておくという選択肢もある
- 不具合を修正すると、不具合を前提として動作していたコードが動かなくなって、別の問題を引き起こすことがある
改善パッチの内容
std::regexは不正な正規表現文字列をユーザが与えた場合、regex_errorという例外が発生します。ところが従来は「"["と"]"の間に"\w"のような文字クラスを指定する(例: [\w-a]
)」というstd:regexからすると不正な文字列を与えても例外は発生しませんでした。前節において紹介したパッチを当てるとこの問題は解決します。以下、このパッチのコミットメッセージからの引用です。
std::regex currently allows invalid bracket ranges such as [\w-a] which
are only allowed by ECMAScript when in web browser compatibility mode.
It should be an error, because the start of the range is a character
class, not a single character. The current implementation of
_Compiler::_M_expression_term does not provide a way to reject this,
because we only remember a previous character, not whether we just
processed a character class (or collating symbol etc.)
このような修正をするか否かの判断
ここからはgccに限らずあらゆるプログラムで起こりうるバグ修正可否の判断について、これまで述べてきたgccの改善パッチを題材として書きます。
改善パッチには何の疑問もないように見えますが、実はそうではありません。前節において述べた不正な正規表現文字列を与えた上でたまたま動いてしまっていたプログラムを改善パッチを含んだgccでビルドしたら、たとえプログラム自身はコードを一切変更していなくても、問題のコードを実行したときに例外が発生するようになってしまうのです。例外処理をサボっていたような場合はプログラムのクラッシュなどの問題が発生します。「それ、悪いのは呼び出し側では?」といわれるとその通りなのですが、とにかく、これまで(たまたま)動いていたプログラムが動かなくなるのです。
ここからが難しい判断を迫られるところです。gccは非常に多くのプログラムから使われており、かつ、誰が使っているのかの全体像を把握するのは困難なので、改善パッチをあえて当てずに、バグをバグのまま残して、もともと動いていたプログラムが動くようにし続けるという選択をすることがあります[1]。gcc以外にもLinuxなど非常に修正時の影響度が高いソフトウェアではこのような選択を迫られることがよくあります。大変そうですね。
なお筆者はgcc開発者たちが改善パッチを適用した判断について何か否定的な思いを持っているわけではありません。なぜなら彼らがどういう情報を持った上でどういう理由で改善パッチの適用に至ったということがわからないので、何も知らない外野の立場で「こうしておけばよかった」などというのは無責任だと思うからです。この記事の目的は、あくまで「このような場合には修正しない場合もある」という一般的な選択肢をみなさんに知ってもらうことです。
gccの改善パッチによって発生した具体的な問題
最近Cephは重要な脆弱性を修正するためv17.2.2という新しいバージョンをリリースしました。v17.2.2はv17.2.1に脆弱性をふさぐための最小限の修正だけ入れたものだったのですが、v17.2.2へのアップデート直後から脆弱性の修正とは全然関係ないところで非常に重要なdaemonがクラッシュし続けてしまい、Cephクラスタ全体が機能しなくなるという問題が発生しました。
ここで勘のいいかたは気づいたかもしれませんが、その重要なdaemonはまさに改善パッチによって例外が発生するようになる不正な正規表現文字列を使っており、さらにCeph v17.2.1は改善パッチを当てていないgccでビルドした一方でv17.2.2は改善パッチを含むgcc[2]でビルドしたという偶然が重なったのです。この問題を修正するために、数日後にはさらにv17.2.3がリリースされました。
余談ですが、任意のクラスタが立ち上がらないような問題を含むバージョンがなぜリリースされてしまったか質問したところ、QAプロセスになんらかの問題があった。今後はこのようなことが起きないようにするとの回答がありました。
おわりに
筆者はCephのv17.2.2のリリース後にクラスタが機能しなくなったという問題が多数報告されていたのをたまたま見ていたので改善パッチの存在を知りました。そして、これは経験の浅いソフトウェア技術者たちにとって、「バグがあるとわかっていてもあえて修正をしない」という選択肢があることを伝える非常によいケーススタディになると思って紹介することにしました。この狙い通りに、ひとりでも多くのかたにこのことを知ってもらえるようになることを願います。
Discussion
修正が本当に問題になる場合、既存の関数やクラスの破壊的変更の代わりに新しい関数やクラスを作ってそれで置き換え、もともとの関数やクラスを非推奨にするという方法があります。
今回の場合は、そもそも不正な正規表現を与えていなければ問題は起きないので、修正しても十分許容範囲にあると思います。