From: Chikanaga Tomoyuki Date: 2007-03-01T12:07:40+09:00 Subject: [ruby-dev:30460] Re: ruby-1.8 で SEGV 日本コントロールシステム(株)の近永と申します。 よろしくおねがいします。 ようやく再現させるスクリプトを作れました。 少なくとも手元の環境では確実に再現できているようです。 原因も把握できたと思います。 ただし再現させることができるのは「free()したメモリ内容を参照する」ことまでで、 2重 free() が起きるかどうかは、free()したメモリを参照した結果に依存している為 確実にアボートさせることはできません。 以下レポートです。 まず再現させるスクリプトを貼ります。 =========== 再現スクリプト(duplicate_free.rb) =========== require "thread" # (A) for tuning sweep order. $gcflag = false def gc_once unless $gcflag GC.start $gcflag = true end end def m1 q = Queue.new dummy = Array.new(100) do [] end (class << q; self; end).module_eval do define_method(:push) do |item| puts "push(#{item.inspect})" super(item) end end dummy = nil gc_once m2(q) end def m2(q) thr = Thread.new do 2.times do q.push :tok sleep 0.1 end end q.pop q.pop thr.join nil end # call twice for sweep original SCOPE... m1() m1() GC.start =========== duplicate_free.rb ここまで =========== valgrind で起動すると、free() したメモリを読もうとしていることが確認できます。 少しスクリプトを変えただけで起きなかったりするので、 他の環境でも再現するか不安ですが.... $ valgrind ruby-1.8.6-preview2 duplicate_free.rb --------------- snip ----------------- ==17496== Invalid read of size 4 ==17496== at 0x8073D1B: obj_free (gc.c:1256) ==17496== by 0x80736F0: gc_sweep (gc.c:1085) ==17496== by 0x8073FBF: garbage_collect (gc.c:1416) ==17496== by 0x8073FCC: rb_gc (gc.c:1422) ==17496== by 0x8073FDE: rb_gc_start (gc.c:1439) ==17496== by 0x805F5D6: call_cfunc (eval.c:5653) ==17496== by 0x805EB2A: rb_call0 (eval.c:5806) ==17496== by 0x80600DC: rb_call (eval.c:6053) ==17496== by 0x8058F1B: rb_eval (eval.c:3438) ==17496== by 0x80543CA: eval_node (eval.c:1423) ==17496== by 0x805491F: ruby_exec_internal (eval.c:1599) ==17496== by 0x8054963: ruby_exec (eval.c:1619) ==17496== Address 0x40D9FC0 is 0 bytes inside a block of size 20 free'd ==17496== at 0x400501A: free (vg_replace_malloc.c:233) ==17496== by 0x8073D72: obj_free (gc.c:1259) ==17496== by 0x80736F0: gc_sweep (gc.c:1085) ==17496== by 0x8073FBF: garbage_collect (gc.c:1416) ==17496== by 0x8073FCC: rb_gc (gc.c:1422) ==17496== by 0x8073FDE: rb_gc_start (gc.c:1439) ==17496== by 0x805F5D6: call_cfunc (eval.c:5653) ==17496== by 0x805EB2A: rb_call0 (eval.c:5806) ==17496== by 0x80600DC: rb_call (eval.c:6053) ==17496== by 0x8058F1B: rb_eval (eval.c:3438) ==17496== by 0x80543CA: eval_node (eval.c:1423) ==17496== by 0x805491F: ruby_exec_internal (eval.c:1599) これが発生する条件はかなり込みいってまして 1) define_method でメソッドを定義 2) 1) で定義したメソッドを呼び出し中に、スレッドのコンテキストスイッチ発生 3) 1) の定義時の struct SCOPE と、2)でメソッド呼び出しした時に、 eval.c:8551(1.8.6-preview2) あたりで生成される struct SCOPE が同時に GC 対象となる。 4) しかもオリジナルの方の struct SCOPE が先に obj_free() が呼ばれる。 というところだと思います。 再現スクリプトの gc_once の存在とか、m1 を2度呼んでいるのは、3),4) の条件を 満たすため必要なようだったからです。 コピーされた struct SCOPE の flags が 2) により SCOPE_DONT_RECYCLE(4)が 付けられた後で scope_dup() されないまま obj_free() に来ると、 オリジナルと同じ local_vars[-1] を見てしまいます。 同じ回の GC でオリジナルの SCOPE も GC 対象で、かつ先に sweep されて いた場合、既にその領域は free() されてしまっています。 さらにここで free() されたメモリ領域の先頭4バイトが 0 になって いる時にかぎり、local_tbl(これもオリジナルの SCOPE の sweep 時に free()済み)を free() してしまって 2重 free() となります。 一応拙作のパッチも貼ります。 valgrind の実行で「Address *** is 0 bytes inside a block of size 20 free'd」 のメッセージが出なくなることと、make check が通るこだけは確認致しました。 もうすぐ 1.8.6 がリリースされることと思いますが、できればこの件への対策も 入っていたら嬉しいです。 というのも、今回のサーバは将来お客さんにも提供するかもしれず、 客先環境ではたいてい各種ディストリビューションのパッケージを 使うことになると思いますので、ruby-1.8.6 以降、というような 指定ができるほうが楽ですので。 よろしくご検討ください。 以下 ruby-1.8.6-preview2 に対するパッチです。 --- src/ruby-1.8.6-preview2/env.h 2007-02-13 08:01:19.000000000 +0900 +++ src/ruby-1.8.6-preview2-modify/env.h 2007-03-01 11:19:50.000000000 +0900 @@ -43,6 +43,7 @@ #define SCOPE_MALLOC 1 #define SCOPE_NOSTACK 2 #define SCOPE_DONT_RECYCLE 4 +#define SCOPE_CLONE 8 extern int ruby_in_eval; --- src/ruby-1.8.6-preview2/parse.y 2007-02-20 15:53:16.000000000 +0900 +++ src/ruby-1.8.6-preview2-modify/parse.y 2007-03-01 11:26:35.000000000 +0900 @@ -5725,7 +5725,8 @@ rb_mem_clear(ruby_scope->local_vars+i, len-i); } if (ruby_scope->local_tbl && ruby_scope->local_vars[-1] == 0) { - xfree(ruby_scope->local_tbl); + if (!(ruby_scope->flags & SCOPE_CLONE)) + xfree(ruby_scope->local_tbl); } ruby_scope->local_tbl = local_tbl(); } --- src/ruby-1.8.6-preview2/eval.c 2007-02-19 18:28:43.000000000 +0900 +++ src/ruby-1.8.6-preview2-modify/eval.c 2007-03-01 10:39:08.000000000 +0900 @@ -8547,11 +8547,12 @@ if (klass) _block.frame.last_class = klass; _block.frame.argc = RARRAY(tmp)->len; _block.frame.flags = ruby_frame->flags; - if (_block.frame.argc && (ruby_frame->flags & FRAME_DMETH)) { + if (_block.frame.argc && DMETHOD_P()) { NEWOBJ(scope, struct SCOPE); OBJSETUP(scope, tmp, T_SCOPE); scope->local_tbl = _block.scope->local_tbl; scope->local_vars = _block.scope->local_vars; + scope->flags |= SCOPE_CLONE; _block.scope = scope; } /* modify current frame */ --- src/ruby-1.8.6-preview2/gc.c 2007-02-13 08:01:19.000000000 +0900 +++ src/ruby-1.8.6-preview2-modify/gc.c 2007-03-01 11:23:48.000000000 +0900 @@ -1253,7 +1253,7 @@ if (RANY(obj)->as.scope.local_vars && RANY(obj)->as.scope.flags != SCOPE_ALLOCA) { VALUE *vars = RANY(obj)->as.scope.local_vars-1; - if (vars[0] == 0) + if (!(RANY(obj)->as.scope.flags & SCOPE_CLONE) && vars[0] == 0) RUBY_CRITICAL(free(RANY(obj)->as.scope.local_tbl)); if (RANY(obj)->as.scope.flags & SCOPE_MALLOC) RUBY_CRITICAL(free(vars)); -- 日本コントロールシステム(株) 近永 智之