[#30549] [ANN] Ruby 1.8.6 has been released — "Akinori MUSHA" <knu@...>

 Ruby 1.8.6 をリリースしました。

14 messages 2007/03/12

[#30553] help: lib/shell for ruby 1.9 — keiju@... (Keiju ISHITSUKA)

けいじゅ@いしつかです.

13 messages 2007/03/13
[#30585] Re: help: lib/shell for ruby 1.9 — Yukihiro Matsumoto <matz@...> 2007/03/15

まつもと ゆきひろです

[#30587] Re: help: lib/shell for ruby 1.9 — keiju@... (石塚圭樹) 2007/03/15

けいじゅ@いしつかです.

[#30588] Re: help: lib/shell for ruby 1.9 — Yukihiro Matsumoto <matz@...> 2007/03/15

まつもと ゆきひろです

[ruby-dev:30460] Re: ruby-1.8 で SEGV

From: Chikanaga Tomoyuki <chikanag@...>
Date: 2007-03-01 03:07:40 UTC
List: ruby-dev #30460
日本コントロールシステム(株)の近永と申します。
よろしくおねがいします。

ようやく再現させるスクリプトを作れました。
少なくとも手元の環境では確実に再現できているようです。
原因も把握できたと思います。

ただし再現させることができるのは「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));


-- 
日本コントロールシステム(株)
近永 智之 <chikanag@nippon-control-system.co.jp>

In This Thread