ほっしーの技術ネタ備忘録

技術ネタの備忘録です。基本的に私が忘れないためのものです。他の人の役にも立つといいなぁ。

PHP のバグ ( httpd in free(): recursive call )


というわけで、httpd が SIGABRT で死ぬ原因を探しました。
dmesg がこんな感じ。

pid 49335 (httpd), uid 80: exited on signal 6
pid 61486 (httpd), uid 80: exited on signal 6
pid 61530 (httpd), uid 80: exited on signal 6
pid 62145 (httpd), uid 80: exited on signal 6
pid 62139 (httpd), uid 80: exited on signal 6
pid 62714 (httpd), uid 80: exited on signal 6
pid 62771 (httpd), uid 80: exited on signal 6
pid 61289 (httpd), uid 80: exited on signal 6

このとき http-error.log はこんな感じ。

httpd in free(): error: recursive call
[notice] child pid 62714 exit signal Abort trap (6)
httpd in free(): error: recursive call
[notice] child pid 62771 exit signal Abort trap (6)
httpd in free(): error: recursive call
[notice] child pid 61289 exit signal Abort trap (6)


結論としてはシグナルハンドラで余計なことするなということで。


パッチを作ろうかと思ったけども思いのほか根が深くて断念。
とりあえず max_execution_time=0 で回避。

1. パフォーマンスタイマーの設定


まず発端は main/main.c にて。

PHP_INI_ENTRY("max_execution_time","30",PHP_INI_ALL,OnUpdateTimeout)

によって、max_execution_time の設定で OnUpdateTimeout() が呼ばれます。


そして、同じく main/main.c にて

        zend_set_timeout(EG(timeout_seconds));

と、zend_set_timeout() が呼ばれます。この実体は Zend/zend_execute_API.c にあって

                setitimer(ITIMER_PROF, &t_r, NULL);
                signal(SIGPROF, zend_timeout);

と、これで指定時間後に SIGPROF が飛んできて、zend_timeout() が呼ばれます。

2. タイムアウトでシグナル拿捕


zend_timeout() も同じく Zend/zend_execute_API.c にあって

        zend_error(E_ERROR, "Maximum execution time of %d second%s exceeded",
                          EG(timeout_seconds), EG(timeout_seconds) == 1 ? "" : "s");

エラーメッセージを表示して終了します。


この zend_error() は Zend/zend.c に実体があり

                        /* The error may not be safe to handle in user-space */
                        zend_error_cb(type, error_filename, error_lineno, format, args);

こんなコメントと共にコールバック関数へと飛びます。


zend_error_cb は関数ポインタで、Zend/zend.c の zend_startup で初期化されて、
実際の値は main/main.c の php_error_cb() を指しています。


ここで

        buffer_len = vspprintf(&buffer, PG(log_errors_max_len), format, args);

やら

efree(buffer);

やらした挙句に

zend_bailout();

で終了します。

3. ベイルアウトぉぉぉーーー


zend_bailout() は Zend/zend.c に実体があって

longjmp(*EG(bailout), FAILURE);

と。ちなみに Zend/zend.h には

#define zend_try ...
#define zend_catch ...
#define zend_end_try() ...

と、まぁいわゆる try - catch の C 言語版でしょうか。
setjump - longjump で実現しているわけですね。

4. 最悪のタイミング


これが、malloc, free の実装関数でロック中にタイムアウトするわけです。


そうすると、

  1. mod_php のどこかで realloc() を呼ぶ
  2. realloc() 内部では排他ロックしたまま memcpy() とか
  3. ...している最中にシグナルハンドラへジャンプ
  4. php_error_cb() で malloc() だの free() だのを呼ぶ
  5. 再帰呼び出しの完成!

というわけで再帰を検出して libc/stdlib/malloc.c の wrterror() で abort() します。