本稿では、Ruby では簡単に core を吐かせることができることを示すことにより、 信頼できないコードを $SAFE=4 で安全に実行できるという機能の存在に警鐘を鳴らします。
ほとんどのソフトウェアにはバグがありますが Ruby インタプリタも例外ではなく、さまざまなバグがあります。
この記事では、最近数多く発見された配列や文字列などのバグについて解説します。 これらのバグはメモリの不適切な場所をアクセスしてしまうもので、 典型的には Segmentation fault により core を吐く症状としてあらわれます。 なお、Ruby で記述されたコードから Segmentation fault を起こせてしまうのはほぼ確実に Ruby のバグです。1
これらのバグの原因は、配列の中身などのメモリが realloc されて移動したときに、C で記述されたメソッドがその移動に気がつかずにもとのアドレスにアクセスしてしまうことです。 このような状況は C で記述されたメソッド中で配列の中身を指すポインタを記憶しているときに、 ブロックを yield するなどして Ruby で記述されたコードが動作し、 そこで配列の長さを変更したときに起こります。 このような問題を引き起こす原因となる Ruby のコードが実行され得る場所は Ruby の様々な場所に存在するため、短期的にすべてを修正するのはおそらく困難で、 しばらくはこのようなバグが存在することを前提としなければならないでしょう。
最近、配列の比較中に配列の長さを変更することによって、Ruby を落とせることが発見されました。 それに続き、様々なメソッドの実行中に配列・ハッシュ・文字列を変更することにより、 同様に Ruby を落とせることが発見されています。
[ruby-dev:24292] Array# | 実行中に配列の長さを変える |
まず、最初に発見された Array#== のバグについて詳しく解説し、 そのあとで他の問題も含めて一般的に解説します。
次の小さな Ruby スクリプトは [BUG] Segmentation fault というメッセージと core を残して終了します。Ruby 自らが [BUG] と報告しているので、これはバグです。 また、先頭で $SAFE = 4 としていますが、これはこのバグを発現させることを防げていません。
このコードでは、まず、ary1 と ary2 というふたつの配列を作っています。Array.new(len) で作られた配列の各要素は nil になるので、ary1、ary2 は次のようになります。
また、o というオブジェクトを作っています。o は Object クラスのインスタンスですが、特異メソッド == を定義してあって、 その定義は「ary2.compact! を呼び出した後に true を返す」というものです。 そして、o を ary1[0] に代入していますので、ary1、ary2 は次のようになります。
このような状態で、ary1 == ary2 を実行すると Ruby は落ちます。
Array#== の実装は array.c にある次の関数です。
ここで、C のコード中では、Ruby のオブジェクトは VALUE 型の値として表現されます。 また、ary という VALUE 型の変数が配列オブジェクトを指しているとすれば、 配列の長さと内容は次のようにしてアクセスされます。
したがって、rb_ary_equal は次の順序で配列の等しさを判定していることになります。
(ary2 には配列が渡されていると仮定して、TYPE(ary2) は T_ARRAY を返すとします)
問題は、3 段階目の「両辺の配列の各要素を順に比較」する次の部分です。
この for ループは ary1 の各要素について繰り返されますが、ary2 の長さは確認していません。 これは通常は問題ありません。 それは、直前の「両辺の配列の長さが異なっていたら異なる」という次の部分によって、 長さが異なる場合には上記のループは実行されないからです。
しかし、ループが始まったときに長さが同じだったからといって、 ループが終るときまで同じとは限りません。rb_equal は要素の == メソッドを呼び出します から、== メソッドの定義によっては ary2 の長さを変えてしまうこともあり得ます。 実際、ary1[0] つまり o の == メソッドは次のように ary2 の長さを変更するように定義されています。
ary1[0] に対して == メソッドを呼び出すと ary2.compact! を実行し、要素が nil の部分を削除します。 ここで ary2 の要素はすべて nil なので、結果的に ary2 は長さ 0 の配列、つまり [] になります。 また、compact! は配列の長さが変わるときには必ず realloc を行なうため、 メモリが移動して配列の範囲外のアクセスが実際にオブジェクトではないものにアクセスすることが期待できるようになります。
そして、o.== は true を返すため、rb_equal は真を返し、ループは終了せずに継続されます。 ループの次の繰り返しにおいては RARRAY(ary2)->ptr[1]) にアクセスしますが、 この時点で ary2 の長さは 0 であるため、これは配列の範囲外をアクセスしていることになります。 配列の範囲外の内容は一般には保証されませんから、 その内容をオブジェクトと解釈して扱えば core を吐く可能性があります。
ただし、偶然 core を吐かない可能性もあるため、 初期状態の配列の長さを 100000 として、 範囲外のアクセスが繰り返し十分に多く行なわれるようにし、core を吐く確率を高めてあります。
Array#== の問題は、ループの途中で ary2 の長さが変わらないという仮定をしていることです。 この仮定は、ループ中において各要素に対して呼び出される == メソッドが ary2 の長さを変更することによって崩れます。
一般には、変更可能なオブジェクトについて C コード中でなんらかの仮定を行なっている状況で Ruby コードが 動作してそのオブジェクトを変更しその仮定を崩せてしまうのが問題です。 そうすると、Ruby コードから Segmentation fault を起こすことが可能になってしまう場合があります。
Ruby コードが実行される機会には、少なくとも次のケースがあります。
また、オブジェクトを変更するにはそのオブジェクトを参照する必要があります。 このため、メソッド内で新しくオブジェクトを作った場合にはたとえ任意の Ruby コードを実行できても、 そのオブジェクトを Ruby コードからは参照できないため安全だと思えるかもしれません。 しかし、ObjectSpace.each_object を使えば Ruby コードからはそのようなオブジェクトを捜し出すことが可能なため、 これは安全ではありません。 また、そのようなオブジェクトがメソッドの返値となる場合、callcc を使ってメソッド中の継続を記録し、 メソッドが終了してそのオブジェクトの参照を得てから継続を呼び出すことによってもメソッドの実行中にオブジェクトを変更できます。
なお、C コード中でオブジェクトの状態に仮定が行なわれていても、 オブジェクトが隠蔽されていて各操作で確実にエラー検出が行なわれていれば少なくとも Segmentation fault は発生しません。 そのため、問題が起こるのはオブジェクトの内部を直接アクセスする場合です。 この条件にあてはまるのは典型的には配列や文字列です。 配列や文字列は内容を記録する領域をもっており、 その領域は長さの変化により realloc されてアドレスと長さが変わり得るため、 アドレスと長さが変わらないことを仮定すると問題が起こります。
Ruby コードが実行される可能性がある場所では、 オブジェクトが変更されている可能性があることを考慮し、 変更されていないことを確かめるコードを記述することによって修正できます。 また、Ruby コードが実行される可能性がある場所を移動することによって修正できる場合もあります。
しかし、Ruby コードが実行され得る場所は多岐に渡ります。 これは、Ruby の大きな特徴とされるブロックや、 オブジェクト指向の多態性によって呼び出し側が意図していないコードを動作させられるという 基本的な Ruby の性質自体が C のコード中で Ruby コードが実行される機会を増やす方向に影響を与えているからです。 このため、即座にすべての箇所が修正されることは期待できませんし、 今後も新しく C のコードが書かれるたびにそのような機会が増えていくことが予想されます。
個々に修正するのではなく、抜本的に修正するひとつの方法は、より完全な保守的 GC を導入することです。 ここで「より完全な」というのは、VALUE 型だけでなく配列や文字列の中身まで含め動的に確保される メモリすべてを GC で管理して扱うということを意味します。 そのようにすれば、少なくともアクセスする可能性のあるメモリを 開放してしまうことはなくなり、Segmentation fault の可能性を除去できます。 ただし、現在の実装からそのような実装に切替えるためにどの程度のコストがかかるかは不明であり、 現実的な解となり得るかどうかについては検討が必要でしょう。
また、長期的には、組み込みメソッドの記述言語を C から Ruby に移行することも有効かもしれません。 これは、Ruby で記述する限りは GC の管理下でしかメモリを 扱えないため、Segmentation fault を起こす機会は与えられないためです。 ただし、これを行なうためには Ruby で記述したメソッドが十分に高速に動作する必要があり、 高速な処理系として期待されている Rite (YARV) の出来しだいといえるでしょう。
$SAFE = 4 にしても、信用できないコードが Ruby 自体を落とすことは防げないので、 信用できないコードは実行しないようにしましょう。Ruby は悪意のあるコードに対してはあまりに脆弱です。 とくに mod_ruby のように、信用性の異なるコードをひとつのプロセス内で動かしがちなケースは十分な注意が必要です。
callcc は使わないようにしましょう。callcc を使うとメソッドを途中から何度も実行することができますが、 実装がそのような場合を考慮していることは多くありません。
finalizer は使わないようにしましょう。 どうしても必要な場合でも、finalizer でのオブジェクトの変更は 行なわないようにしましょう。finalizer の動作するタイミングは予想困難であるため、 予想外の影響が出る場合があります。
拡張ライブラリでオブジェクトにアクセスするときは、提供された関数を用いてアクセスしましょう。 たとえば、配列をアクセスするときは、RARRAY(ary)->ptr[index] ではなく、rb_ary_entry(ary, index) と rb_ary_store(ary, index, val) を使いましょう。 また、RARRAY(ary)->ptr や RARRAY(ary)->len をローカル変数に保存するのはさらに危険です。
Ruby を信用しすぎるのはやめましょう。 奇妙な機能 ($SAFE、callcc、finalizer など) を使うときには細心の注意を払い、可能なら使用を避けるべきです。
田中哲 (産業技術総合研究所)
深く関わったメソッド: fork, Time.utc, Time#utc_offset, allocate, marshal_dump, marshal_load, Regexp#to_s, Regexp.union, Process.daemon, readpartial, etc.