Ruby の落とし方

Ruby の落とし方

要約

本稿では、Ruby では簡単に core を吐かせることができることを示すことにより、 信頼できないコードを $SAFE=4 で安全に実行できるという機能の存在に警鐘を鳴らします。

はじめに

ほとんどのソフトウェアにはバグがありますが Ruby インタプリタも例外ではなく、さまざまなバグがあります。

この記事では、最近数多く発見された配列や文字列などのバグについて解説します。 これらのバグはメモリの不適切な場所をアクセスしてしまうもので、 典型的には Segmentation fault により core を吐く症状としてあらわれます。 なお、Ruby で記述されたコードから Segmentation fault を起こせてしまうのはほぼ確実に Ruby のバグです。*1

これらのバグの原因は、配列の中身などのメモリが realloc されて移動したときに、C で記述されたメソッドがその移動に気がつかずにもとのアドレスにアクセスしてしまうことです。 このような状況は C で記述されたメソッド中で配列の中身を指すポインタを記憶しているときに、 ブロックを yield するなどして Ruby で記述されたコードが動作し、 そこで配列の長さを変更したときに起こります。 このような問題を引き起こす原因となる Ruby のコードが実行され得る場所は Ruby の様々な場所に存在するため、短期的にすべてを修正するのはおそらく困難で、 しばらくはこのようなバグが存在することを前提としなければならないでしょう。

さまざまな問題

最近、配列の比較中に配列の長さを変更することによって、Ruby を落とせることが発見されました。 それに続き、様々なメソッドの実行中に配列・ハッシュ・文字列を変更することにより、 同様に Ruby を落とせることが発見されています。

  • [ruby-dev:24254] Array#== 実行中に比較対象の配列の長さを変える
  • [ruby-dev:24261] Array#collect! 実行中に配列の長さを変える
  • [ruby-dev:24262] Array#{reject!,eql?,<=>} 実行中に配列の長さを変える
  • [ruby-dev:24269] Array#assoc 実行中に配列の長さを変える
  • [ruby-dev:24273] Array#delete 実行中に配列の長さを変える
  • [ruby-dev:24274] Array#- 実行中に配列の長さを変える
  • [ruby-dev:24275] Array#rindex 実行中に配列の長さを変える
  • [ruby-dev:24278] Array#select 実行中に配列の長さを変える
  • [ruby-dev:24279] Array#transpose 実行中に配列の長さを変える
  • [ruby-dev:24284] Array.new で生成中の配列の長さを変える
  • [ruby-dev:24287] Array#sort! 実行中に配列の長さを変える
  • [ruby-dev:24289] Hash#each 実行中にハッシュの大きさを変える
  • [ruby-dev:24290] Array#& 実行中に配列の長さを変える
  • [ruby-dev:24291] Enumerable#sort_by 実行中に返値の配列の長さを変える
  • [ruby-dev:24292] Array#| 実行中に配列の長さを変える
  • [ruby-dev:24301] Hash#rehash を継続を使って途中から再開させる
  • [ruby-dev:24303] Hash#each 実行中に保存した継続をハッシュを空にしてから呼ぶ
  • [ruby-dev:24315] String#sub! 実行中に文字列の長さを変える
  • [ruby-dev:24320] Struct#[] 実行中の構造体の定義を変える
  • [ruby-dev:24324] Array#[] 実行中に起きた GC で配列の長さを変える
  • [ruby-dev:24325] 共有された配列の長さを変える
  • [ruby-dev:24332] Marshal.dump でハッシュをダンプしている途中でハッシュを空にする
  • [ruby-dev:24336] Array#+ 実行中に起きた GC で配列の長さを変える
  • [ruby-dev:24341] Array#* 実行中に起きた GC で配列の長さを変える
  • [ruby-dev:24343] Array#flatten! 実行中に配列の長さを変える
  • [ruby-dev:24344] Array#shift 実行中に起きた GC で配列の長さを変える
  • [ruby-dev:24347] String#crypt 実行中に起きた GC で crypt(3) を呼び出して crypt(3) が返す static な領域を書き換える
  • [ruby-dev:24348] Array#delete 実行中に配列の長さを変える
  • [ruby-dev:24366] IO#read 実行中にバッファ文字列の長さを変える
  • [ruby-dev:24368] Enumerable#sort_by 実行中にテンポラリな配列の長さを変える
  • [ruby-dev:24371] String#chomp! 実行中に文字列の長さを変える
  • [ruby-dev:24373] eval 実行中にファイルネーム文字列の長さを変える
  • [ruby-dev:24375] IO.popen 実行中にコマンド文字列の長さを変える
  • [ruby-dev:24377] File.basename 実行中に拡張子文字列の長さを変える
  • [ruby-dev:24378] Enumerable#sort_by 実行中の継続を不適切なテンポラリオブジェクトを用意して呼び出す
  • [ruby-dev:24381] String#sum 実行中に文字列の長さを変える
  • [ruby-dev:24382] instance_eval 実行中にファイルネーム文字列の長さを変える
  • [ruby-dev:24400] IO#read 実行中に返値の文字列の長さを変える
  • [ruby-dev:24404] Marshal.load 実行中に文字列の長さを変える
  • [ruby-dev:24408] open 実行中にモード文字列の長さを変える
  • [ruby-dev:24432] String#gsub! を継続で途中から再開させる
  • [ruby-dev:24434] String#ljust 実行中に padding 文字列の長さを変える
  • [ruby-dev:24438] File.sysopen 実行中にファイルネーム文字列の長さを変える
  • [ruby-dev:24439] String#unpack 実行中に文字列の長さを変える
  • [ruby-dev:24445] Array#pack 実行中にフォーマット文字列の長さを変える
  • [ruby-dev:24454] コマンドを実行する open の実行中にモード文字列の長さを変える
  • [ruby-dev:24461] IO#gets 実行中にレコードセパレータ文字列の長さを変える
  • [ruby-dev:24463] Enumerable#each_with_index を継続で途中から再開させる
  • [ruby-dev:24479] IO#read(nil, str) 実行中にバッファ文字列の長さを変える
  • [ruby-dev:24487] Dir.glob を継続で途中から再開させる
  • [ruby-dev:24490] DBM#delete_if 実行中にテンポラリ配列の内容を文字列から整数に置き換える
  • [ruby-dev:24492] require 実行中に feature 名文字列の長さを変える
  • [ruby-dev:24499] Enumerable::Enumerator#each_slice を継続で途中から再開させる

まず、最初に発見された Array#== のバグについて詳しく解説し、 そのあとで他の問題も含めて一般的に解説します。

Array#== のバグ

次の小さな Ruby スクリプトは [BUG] Segmentation fault というメッセージと core を残して終了します。Ruby 自らが [BUG] と報告しているので、これはバグです。 また、先頭で $SAFE = 4 としていますが、これはこのバグを発現させることを防げていません。

% ruby-1.8.1 -e '
$SAFE = 4
len = 100000
ary1 = Array.new(len)
ary2 = Array.new(len)
o = Object.new
o.instance_eval { @ary2 = ary2 }
def o.==(o2)
  @ary2.compact!
  true
end
ary1[0] = o
ary1 == ary2
'
-e:13: [BUG] Segmentation fault
ruby 1.8.1 (2003-12-25) [i686-linux]

zsh: abort (core dumped)  ruby-1.8.1 -e

このコードでは、まず、ary1 と ary2 というふたつの配列を作っています。Array.new(len) で作られた配列の各要素は nil になるので、ary1、ary2 は次のようになります。

index: 0    1         99999
ary1: [nil, nil, ..., nil]
ary2: [nil, nil, ..., nil]

また、o というオブジェクトを作っています。o は Object クラスのインスタンスですが、特異メソッド == を定義してあって、 その定義は「ary2.compact! を呼び出した後に true を返す」というものです。 そして、o を ary1[0] に代入していますので、ary1、ary2 は次のようになります。

index: 0    1         99999
ary1: [o  , nil, ..., nil]
ary2: [nil, nil, ..., nil]

このような状態で、ary1 == ary2 を実行すると Ruby は落ちます。

Array#== の実装は array.c にある次の関数です。

static VALUE
rb_ary_equal(ary1, ary2)
    VALUE ary1, ary2;
{
    long i;

    if (ary1 == ary2) return Qtrue;
    if (TYPE(ary2) != T_ARRAY) {
        if (!rb_respond_to(ary2, rb_intern("to_ary"))) {
            return Qfalse;
        }
        return rb_equal(ary2, ary1);
    }
    if (RARRAY(ary1)->len != RARRAY(ary2)->len) return Qfalse;
    for (i=0; i<RARRAY(ary1)->len; i++) {
        if (!rb_equal(RARRAY(ary1)->ptr[i], RARRAY(ary2)->ptr[i]))
            return Qfalse;
    }
    return Qtrue;
}

ここで、C のコード中では、Ruby のオブジェクトは VALUE 型の値として表現されます。 また、ary という VALUE 型の変数が配列オブジェクトを指しているとすれば、 配列の長さと内容は次のようにしてアクセスされます。

RARRAY(ary)->len
配列の長さ
RARRAY(ary)->ptr
配列の内容を表現する VALUE の並びへのポインタ

したがって、rb_ary_equal は次の順序で配列の等しさを判定していることになります。

  1. 両辺の配列が同じものを指していてたら等しい
  2. 両辺の配列の長さが異なっていたら異なる
  3. 両辺の配列の各要素を順に比較してひとつでも異なっているものがあれば異なる
  4. 上記の条件すべてに当てはまらなければ両辺の配列は等しい

(ary2 には配列が渡されていると仮定して、TYPE(ary2) は T_ARRAY を返すとします)

問題は、3 段階目の「両辺の配列の各要素を順に比較」する次の部分です。

    for (i=0; i<RARRAY(ary1)->len; i++) {
        if (!rb_equal(RARRAY(ary1)->ptr[i], RARRAY(ary2)->ptr[i]))
            return Qfalse;
    }

この for ループは ary1 の各要素について繰り返されますが、ary2 の長さは確認していません。 これは通常は問題ありません。 それは、直前の「両辺の配列の長さが異なっていたら異なる」という次の部分によって、 長さが異なる場合には上記のループは実行されないからです。

    if (RARRAY(ary1)->len != RARRAY(ary2)->len) return Qfalse;

しかし、ループが始まったときに長さが同じだったからといって、 ループが終るときまで同じとは限りません。rb_equal は要素の == メソッドを呼び出します から、== メソッドの定義によっては ary2 の長さを変えてしまうこともあり得ます。 実際、ary1[0] つまり o の == メソッドは次のように ary2 の長さを変更するように定義されています。

def o.==(o2)
  @ary2.compact!
  true
end

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 を吐く確率を高めてあります。

C コード中での Ruby コードの実行

Array#== の問題は、ループの途中で ary2 の長さが変わらないという仮定をしていることです。 この仮定は、ループ中において各要素に対して呼び出される == メソッドが ary2 の長さを変更することによって崩れます。

一般には、変更可能なオブジェクトについて C コード中でなんらかの仮定を行なっている状況で Ruby コードが 動作してそのオブジェクトを変更しその仮定を崩せてしまうのが問題です。 そうすると、Ruby コードから Segmentation fault を起こすことが可能になってしまう場合があります。

Ruby コードが実行される機会には、少なくとも次のケースがあります。

  • rb_yield してブロックを呼び出す
  • rb_funcall でメソッドを呼び出す
  • rb_equal でオブジェクトを比較すると == メソッドが呼び出される
  • Hash の key としてオブジェクトを使うと、hash メソッドおよび eql? メソッドが呼び出される
  • StringValue、StringValuePtr、StringValueCStr などを使うと、to_str が呼び出される
  • NUM2INT などのオブジェクトから整数への変換で to_int が呼び出される
  • 配列の内部にアクセスする場合には to_ary が呼び出される
  • I/O を行なうときにスレッドが切り替わって他のスレッドが動作する
  • rb_gc を呼び出して GC が行なわれるとき、finalizer が付加されているオブジェクトが削除されるとその finalizer が呼び出される
  • オブジェクトを作ったり配列の長さが変わるなどしてメモリの確保が起こると、GC が行なわれて finalizer が呼び出される *2

また、オブジェクトを変更するにはそのオブジェクトを参照する必要があります。 このため、メソッド内で新しくオブジェクトを作った場合にはたとえ任意の 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.


*1 スタックオーバーフローによるものなど、Segmentation fault が起きてもしかたがないとされる場合も稀にはあります

*2 2004-09-27 に finalizer の実行は遅延されるようになり、メモリ確保の時点で起こる GC では Ruby コードは実行されなくなりました