未来のいつか/hyoshiokの日記

hyoshiokの日々思うことをあれやこれや

スタックオバーフローに対処する

rubyのバグ(d:id:hyoshiok:20081106#p1)でsigsegvでrubyが異常終了するという話を書いた。その続き。

BINARY HACKSのHACK #76(pp. 291-300)、"sigaltstackでスタックオーバフローに対処する"が参考になりそうだ。

ということでsigaltstack(2)を使うというところを写経してみた。BINARY HACKSによれば、スタックオバーフローでSEGVした場合、通常のsignal(2)やsigaction(2)で処理することはできない。というのはスタックオバーフローした場合シグナルハンドラーを動かすスタックすら確保できないかららしい。なるほどね。そのため、スタックオバーフローを捕捉するために代替シグナルスタックを設定する必要がある。それには、sigaltstack(2)を使う。

http://www.linux.or.jp/JM/html/LDP_man-pages/man2/sigaltstack.2.htmlによれば、

sigaltstack() を使うと、プロセスは新しい代替シグナルスタックを定義した
り、既存の代替シグナルスタックの状態を取得できる。シグナルハンドラが代
替シグナルスタックを要求するように設定されていると (sigaction(2) 参照)、
ハンドラの実行中はそのシグナルスタックが使われる。

代替シグナルスタックを使う際の一般的な手順は、以下の通りである:

1.  代替シグナルスタックで使うメモリ領域を確保する。 
2.  sigaltstack() を使って、代替シグナルスタックの存在と場所をシステムに知らせる。 
3.  sigaction(2) を使ってシグナルハンドラを確立する際、 SA_ONSTACK フラ
グを指定することにより、そのシグナルハンドラを代替シグナルスタック上で
実行することをシステムに知らせる。 ss 引き数は、新しいシグナルスタック
を指定するために使う。また oss 引き数は、現在確立されているシグナルスタッ
クの情報を取得するために使う。この操作のうち 1 つだけを実行させるには、
使用しない引き数を NULL に指定すればよい。引き数となる構造体は、以下の
ような型である:

    typedef struct {
        void  *ss_sp;     /* スタックのベースアドレス */
        int    ss_flags;  /* フラグ */
        size_t ss_size;   /* スタックのバイト数 */
    } stack_t;

    新規の代替シグナルスタックを確立するには、 ss.ss_flags を 0 に設定
    し、 ss.ss_sp と ss.ss_size にスタックの開始アドレスとスタックサイ
    ズを指定する。定数 SIGSTKSZ は、代替シグナルスタックが通常必要する
    サイズよりも充分大きく定義されている。また定数 MINSIGSTKSZ は、シグ
    ナルハンドラの実行に必要な最小サイズに定義されている。

    代替スタックでシグナルハンドラが起動された場合には、カーネルにより
    自動的に、ss.ss_sp で指定されたアドレスは動作しているハードウェアアー
    キテクチャに適したアドレス境界に調整される。

    既存のスタックを無効にするには、 ss.ss_flags を SS_DISABLE に指定す
    る。この場合、ss の他のフィールドは無視される。

    oss が NULL 以外の場合、 oss に代替シグナルスタックの情報が返される。
    これは (実質的に) sigaltstack() の呼び出しより先に行われる。
    oss.ss_sp と oss.ss_size フィールドにスタックの開始アドレスとスタッ
    クサイズが返される。 oss.ss_flags には以下のどちらかの値が返される:

注意のところも、そのまま引くと

代替シグナルスタックを使用する最もよくある場面は、 SIGSEGV シグナルを扱
うときである。 SIGSEGV はプロセスの通常のスタックが利用できる空間が使い
果たされた際に生成されるシグナルである。この場合には、 SIGSEGV 用のシグ
ナルハンドラをプロセスのスタック上では起動することができない。そのため、
このシグナルを扱おうとする場合には、代替シグナルスタックを使用しなけれ
ばならない。 プロセスが標準のシグナルスタックを使い果たすことが予想され
る場合は、代替シグナルスタックを確立すると便利である。例えば、スタック
が最上位アドレスから下位アドレス方向に非常にたくさん積まれてしまうこと
で、最下位アドレスから上位アドレス方向に積まれるヒープとぶつかってしま
う場合や、 setrlimit(RLIMIT_STACK, &rlim) の呼び出しで確立された制限に
達してしまった場合に、この様な事が起こる。標準のスタックを使い果たして
しまうと、カーネルはプロセスに SIGSEGV シグナルを送る。このような状況で
は、代替シグナルスタック上でしかシグナルをキャッチできない。 Linux がサ
ポートする多くのハードウェアアーキテクチャでは、スタックは下位アドレス
方向に積まれる。 sigaltstack() はスタックが積まれる方向を自動的に決定す
る。 代替シグナルスタック上で実行されているシグナルハンドラから呼ばれる
関数も、代替シグナルハンドラを使う (プロセスが代替シグナルスタック上で
実行されている場合、他のシグナルで呼び出されるハンドラもこの代替シグナ
ルハンドラを使う)。標準のスタックとは異なり、システムは代替シグナルスタッ
クを自動的に拡張しない。代替シグナルスタック用に確保したサイズを越えた
場合、結果は予想できない。 execve(2) の呼び出しが成功すると、既存の全て
の代替シグナルスタックが削除される。 fork() 経由で作成された子プロセス
は、親プロセスの代替シグナルスタックの設定のコピーを継承する。
sigaltstack() は以前の sigstack() を置き換えるものである。過去プログラ
ムとの互換性のため、glibc では sigstack() も提供している。新しいのアプ
リケーションは全て sigaltstack() を使って書くべきである。  

プログラム例は下記だ。

stack_t ss;

ss.ss_sp = malloc(SIGSTKSZ);
if (ss.ss_sp == NULL)
    /* ハンドルエラー */;
ss.ss_size = SIGSTKSZ;
ss.ss_flags = 0;
if (sigaltstack(&ss, NULL) == -1)
    /* ハンドルエラー */;

なるほどね。

で写経した。

$ ./a.out 
stack overflow error.

ふむふむ。今日はここまでにする。とか言いながら結局パッチを書いてしまった。

rubyのスタックオーバフローに対処する。(その2)

sigsltstack(2)を利用して、SIGSEGVのシグナルハンドラを書いてみた。下記参照。
先に書いたとおりスタックオーバフローでのSIGSEGVの場合、スタックを使いきってしまうので、シグナルハンドラが動く余地もないので、綺麗に後処理ができない。そこで代替スタックを準備して、各種後処理ができるようにする。register_sigaltstack()で代替スタックをsigaltstack(2)で設定している。シグナルハンドラを設定する前に代替スタックの設定処理を呼ぶことにしている。

なお、register_sigaltstack()はBINARY_HACKを写経したものにエラー処理を若干追加したものである。

$ svn diff signal.c
Index: signal.c
===================================================================
--- signal.c	(revision 20086)
+++ signal.c	(working copy)
@@ -47,6 +47,10 @@
 # define NSIG (_SIGMAX + 1)      /* For QNX */
 #endif
 
+#ifdef SIGSEGV
+int is_altstack_defined = 0;
+#endif
+
 static const struct signals {
     const char *signm;
     int  signo;
@@ -410,6 +414,28 @@
 typedef RETSIGTYPE (*sighandler_t)(int);
 
 #ifdef POSIX_SIGNAL
+#define ALT_STACK_SIZE (4*1024)
+#ifdef SIGSEGV
+/* alternate stack for SIGSEGV */
+static void register_sigaltstack() {
+    stack_t newSS, oldSS;
+
+    if(is_altstack_defined)
+      return;
+
+    newSS.ss_sp = malloc(ALT_STACK_SIZE);
+    if(newSS.ss_sp == NULL)
+      /* should handle error */
+       rb_bug("register_sigaltstack. malloc error\n");
+    newSS.ss_size = ALT_STACK_SIZE;
+    newSS.ss_flags = 0;
+
+    if (sigaltstack(&newSS, &oldSS) < 0) 
+        rb_bug("register_sigaltstack. error\n");
+    is_altstack_defined = 1;
+}
+#endif
+
 static sighandler_t
 ruby_signal(int signum, sighandler_t handler)
 {
@@ -432,7 +458,12 @@
     if (signum == SIGCHLD && handler == SIG_IGN)
 	sigact.sa_flags |= SA_NOCLDWAIT;
 #endif
-    sigaction(signum, &sigact, &old);
+#ifdef SA_ONSTACK
+    if (signum == SIGSEGV)
+        sigact.sa_flags |= SA_ONSTACK;
+#endif
+    if (sigaction(signum, &sigact, &old) < 0)
+        rb_bug("sigaction error.\n");
     return old.sa_handler;
 }
 
@@ -663,6 +694,7 @@
 #ifdef SIGSEGV
       case SIGSEGV:
         func = sigsegv;
+        register_sigaltstack();
         break;
 #endif
 #ifdef SIGPIPE
@@ -1070,6 +1102,7 @@
     install_sighandler(SIGBUS, sigbus);
 #endif
 #ifdef SIGSEGV
+    register_sigaltstack();
     install_sighandler(SIGSEGV, sigsegv);
 #endif
     }

でもって、実行結果は下記だ。

$ ./ruby -e 'eval("1+" * 100000 + "1")'
-e:1: [BUG] Segmentation fault
ruby 1.9.0 (2008-11-01 revision 20086) [i686-linux]

-- control frame ----------
c:0004 p:---- s:0010 b:0010 l:000009 d:000009 CFUNC  :eval
c:0003 p:0017 s:0006 b:0006 l:000005 d:000005 TOP    -e:1
c:0002 p:---- s:0004 b:0004 l:000003 d:000003 FINISH :inherited
c:0001 p:0000 s:0002 b:0002 l:000001 d:000001 TOP    <dummy toplevel>:17
---------------------------
-e:1:in `eval': stack level too deep (SystemStackError)
	from -e:1:in `<main>'

どこで落ちているかrubyが教えてくれるので、デバッグのヒントになる。いきなり、"Segmentation fault"といって、コアダンプを吐くよりかは、若干ましになったかと思う。五十歩百歩かな。どうだろうか。

現状の落ち方。

$ ./ruby -e 'eval("1+" * 100000 + "1")'
Segmentation fault

味もそっけもない。

rubyの識者のご意見を待つ。