ApacheのSegmentation faultはgdbで簡単に特定(mod_mruby編)

Apacheのデバッグの方法は多く紹介されていて、例えばgdbを使ってみましょうと紹介されている記事も多いです。しかし、操作の仕方が多岐に渡っていて、なんとなく敷居が高く感じて使わないという人も多いかもしれません。

例えば、Apache周りのエンジニアが一番気になるのは、Segmentation faultだと思います。そこで、今回は自分がSegmentation faultの原因を特定する時の一番手っ取り早い、gdbを使った方法を紹介しようと思います。gdbを使っていない人にとって、gdbって少し敷居が高いんじゃないかなぁ、と思っている人も多いかもしれませんが、今回の方法であればだれでも気軽にできると思います。

まずはバグを作る

今回は、自分でSegmentation faultを発生させて、それをどのようにgdbで発見するかをチュートリアルっぽく紹介したいと思います。どうせなので、今回はmod_mruby上でバグを作ってみました。一番手っ取り早いSegmentation faultの発生の仕方として、strlen()にNULLを渡す方法を使ってみたいと思います。

まず、mod_mrubyの本体であるmod_mruby.cの中から呼んでいるap_mrb_request.c内で定義されている関数ap_mrb_get_request_filename()を以下のよう、5行目に「val = NULL;」を追加しました。この関数は、mrbスクリプト上でrequest_rec構造体内のfilenameの値を呼び出す時に使われる関数です。

mrb_value ap_mrb_get_request_filename(mrb_state *mrb, mrb_value str)
{
    request_rec *r = ap_mrb_get_request();
    char *val = apr_pstrdup(r->pool, ap_mrb_string_check(r->pool, r->filename));
    val = NULL;
    return mrb_str_new(mrb, val, strlen(val));
}

例えば、mrbスクリプト上で、以下のように記述した場合に、Apache内部のr->filenameの値が一旦上記関数内でval変数にコピーされ、それをmrubyスクリプト上のr.filenameの値として扱えるようになるのですが、そんな値を無視して上記の追加実装によって、valにNULLが入ってしまいます。

hoge = r.filename

その結果、return時のstrlen(val)は引数valにNULLが渡されるため、Segmentation faultが起こるはずです。

Segmentation faultを起こしてみる

では実際に、Apache経由でmrbスクリプトをフックさせて、request_recのfilenameを取得してみましょう。Apacheの設定は以下です。

LoadModule mruby_module modules/mod_mruby.so
mrubyTranslateNameMiddle /var/www/html/request.mrb

この設定で、Apache内でURLとファイル名を紐付けるフェイズであるap_hook_translate_name時に、request.mrbをフックさせる事ができます。request.mrbは以下のようにしました。

require 'Apache'

hoge = r.filename
r.filename = "/var/www/html/test.html"

Apache.return(Apache::OK)

本来は、このスクリプトによって、URLとファイル名の紐付けがどのURLにアクセスがあっても全てtest.htmlと紐付ける事ができます。しかし、hoge = r.filenameによって、ap_mrb_get_request_filenam()内でNULLが渡されて、それがstrlen()されるので、mrbスクリプト上でr.filenameを呼び出してしまうと、必ずSegmentation faultが起こるはずですね。

では、試してみましょう。実際に適当なURLにアクセスしてみました。

すると、以下のようにエラーログにSegmentation faultが出力されました。

[Thu May 24 11:10:56 2012] [notice] child pid 1792 exit signal Segmentation fault (11)

gdbで調査

本来は、上記のような事情を知らないで対応するので、「なんかApacheがSegmentation fault吐いてる!昨日確かにホゲモジュール組み込んだけどその影響かなぁ」とか議論すると思います。

僕はこういうとき、gdbを使って手っ取り早く原因を突き止めるようにしています。では、実際にgdbを使ってみましょう。

まずは、起動中のApacheを停止させます。

/etc/init.d/httpd stop

そして、gdbを使ってシングルプロセスでApacheを起動させます。

gdb /usr/sbin/httpd

すると、以下のような出力と共にgdbの入力に入ります。

GNU gdb (GDB) CentOS (7.0.1-42.el5.centos)
Copyright (C) 2009 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i386-redhat-linux-gnu".
For bug reporting instructions, please see:
...
Reading symbols from /usr/sbin/httpd...(no debugging symbols found)...done.
(gdb)

では、早速Apacheをシングルプロセスで起動させてみましょう。

(gdb) run -X
Starting program: /usr/sbin/httpd -X
  - snip -
[Thread debugging using libthread_db enabled]
  - snip -

上記のように、Apacheがシングルプロセスで起動します。では、Apacheにアクセスしてみます。すると、gdbが以下のように情報を出力します。

Program received signal SIGSEGV, Segmentation fault.
0x00717c73 in strlen () from /lib/libc.so.6
(gdb)

何やら、Segmentation faultを起こしたようですね!さらに、上記出力だけで大体strlen()でSegmentation faultを起こしている事が分かります。これで、大体の人は「あー、なんかNULLチェックできてなくてstrlen()にNULL渡されてるのかなぁ」と予想できると思います。

さらに、どこの実装なのかは、以下のようにwhereコマンドを実行する事でわかります。

(gdb) where
#0 0x00717c73 in strlen () from /lib/libc.so.6
#1 0xb784292b in ap_mrb_get_request_filename (mrb=0xb756d4e0, str=...) at ap_mrb_request.c:97
#2 0xb78b064e in mrb_run (mrb=0xb756d4e0, proc=0xb7571354, self=...) at vm.c:701
#3 0xb78435b3 in ap_mruby_run (mrb=0xb756d4e0, r=0xb758a270, conf=0xb7f19100, mruby_code_file=0xb7f41cd0 "/var/www/html/request.mrb", module_status=0) at mod_mruby.c:665
#4 0xb78439d2 in mod_mruby_translate_name_middle (r=0xb758a270) at mod_mruby.c:758
#5 0x009c0c5d in ap_run_translate_name ()
#6 0x009c204a in ap_process_request_internal ()
#7 0x009d59cb in ap_process_request ()
#8 0x009d277f in ?? ()
#9 0x009cdd9d in ap_run_process_connection ()
#10 0x009cde9c in ap_process_connection ()
#11 0x009daa24 in ?? ()
#12 0x009dac94 in ?? ()
#13 0x009dbba9 in ap_mpm_run ()
#14 0x009b1277 in main ()
(gdb)

このように、スタックが出力されて、下からみていくとどのように関数が呼ばれているかがわかります。

#5でap_run_translate_nameによって#4でmod_mruby.cのmod_mruby_transalte_name_middle()が呼び出され、#3でrequest.mrbのコードファイルがap_mruby_run()に渡されて実行され、#2でmrb_run()が実行されて、#1でap_mrb_request.cの97行目でap_mrb_get_request_filename()が実行されて、そこの中のstrlen()でSegmentation faultが発生した、と理解できます。

これは、結局、上記で作ったバグの行であることが明快にわかります。

最後に

というように、ApacheでSegmentaion faultが起きた際には、gdbを使えば簡単に原因を追求できるので、gdbに慣れていない人でもこのような使い方であればすぐにでも使う事ができると思います。使っている内に、他の使い方もできるようになっていき、気がつけばgdbマスターになっている可能性もありますので、まずはこういった簡単かつ有効性の高い使い方から試してみるとよいかもしれません。