调试一个 xdebug 造成的 Segmentation fault

在调试一个用了 solr 扩展的 php 代码时候,发现调用 query 会发生 Segmentation fault 。于是想知道是从什么地方来的错误。开始觉得是 solr 扩展的问题。但是只有一个 Segmentation fault 不好定位。所以就先搞出 coredump 出来。先写一个简单的复现程序 test.php ,就是简单地初始化好 solr ,然后调用一次 query ,然后打开 coredump,运行一下。

ulimit -c unlimited
php test.php

这就在当前目录下产生了一个 core 文件。然后就可以上 gdb 了

gdb core test.php
.....
(gdb) bt
#0  zend_memrchr (type=1, error_filename=, error_lineno=34, format=, args=)
    at /usr/local/include/php/Zend/zend_operators.h:291
#1  xdebug_error_cb (type=1, error_filename=, error_lineno=34, format=, args=)
    at /home/xiezhenye/xdebug-2.2.1/xdebug_stack.c:626
#2  0x000000000076ac0e in zend_error_va (type=718182544, file=0xa 
, lineno=4294967295, format=0x10
) at /home/xiezhenye/php-5.4.7/Zend/zend_exceptions.c:767 #3 0x000000000076ad8e in zend_exception_error (exception=0x7fcc2ace1600, severity=1) at /home/xiezhenye/php-5.4.7/Zend/zend_exceptions.c:807 #4 0x0000000000752aba in zend_execute_scripts (type=8, retval=, file_count=3) at /home/xiezhenye/php-5.4.7/Zend/zend.c:1310 #5 0x00000000006f72fd in php_execute_script (primary_file=) at /home/xiezhenye/php-5.4.7/main/main.c:2473 #6 0x00000000007f98ac in do_cli (argc=, argv=) at /home/xiezhenye/php-5.4.7/sapi/cli/php_cli.c:988 #7 0x00000000007f9fe4 in main (argc=, argv=) at /home/xiezhenye/php-5.4.7/sapi/cli/php_cli.c:1364 (gdb)

发现调用栈里没有 solr 的东西,最可疑的倒是 xdebug_error_cb 。看起来可能是 xdebug 的问题。于是先禁用掉 xdebug ,果然一切就正常了,输出了 solr 的爆长的异常信息。

于是去看看 xdebug 的那段代码 xdebug_stack.c 626 行。附近几行是这样的:

 623                                 /* find first new line */
 624                                 p = strchr(buffer, '\n');
 625                                 /* find last quote */
 626                                 p = ((char *) zend_memrchr(buffer, '\'', p - buffer)) + 1;
 627                                 /* Create new buffer */
 628                                 tmp_buf = calloc(p - buffer + 1, 1);
 629                                 strncpy(tmp_buf, buffer, p - buffer );

zend_memrchr 看起来是个和 strrchr 差不多的函数,在字符串里查找字符。第三个参数是字符串的长度。从这里出内存错误,多半是访问越界。这个长度里,p 的来源是 strchr 。如果 buffer 里没有 ‘\n’,又没以 ‘\0’ 结尾就有可能造成返回的 p 不在 buffer 的正常范围内,就可能造成访问越界。但是什么情况下回出现呢?查看 buffer 的来历的时候,看到有这么一行。

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

log_errors_max_len 是 php.ini 里关于错误信息长度的选项。于是想到,会不会是那个爆长的 solr 错误信息引发的。

写一个测试代码:

<?php
throw new Exception("long message ".str_repeat('.', 10240));

带着 xdebug 运行一下,果然又出现 Segmentation fault 。

已提交 bug:http://bugs.xdebug.org/view.php?id=885
对应 xdebug 版本 2.2.1

php 中运行外部程序的一个潜在风险

php 中有 exec system popen 等一系列运行外部程序的函数。在 web 环境中使用这些函数的时候,即使控制好了权限,保证了被执行程序本身的安全,还可能有另外的潜在风险。

php 的这些函数实际上是使用了 popen 函数。popen 利用了 vfork 来启动一个 shell 子进程来执行命令。但是 popen 并没有在子进程中关闭原有的进程的文件描述符。这样子进程也会占有这些文件描述符,即使它们并不需要,如果子进程长时间运行,还会导致这些资源没法释放。

比如在 php-fpm 环境中,如果在子进程长时间运行时 php-fpm 崩溃,或者手动停止服务,监听的端口 9000 所对应的文件描述符还会被子进程共享。此时想重新启动 php-fpm 也会因为端口被占用而失败。

比如运行如下程序

<?php
exec("sleep 1000;");

然后

killall php-fpm

杀死 php-fpm 后,

netstat -lntp

会看到,9000 端口被一个 sh 进程占用。

...
tcp        0      0 127.0.0.1:9000          0.0.0.0:*               LISTEN      3935/sh
...

查看这个 sh 会发现这正是执行 sleep 的那个进程。

...
nobody    3935  0.0  0.0   4272   580 ?        S    11:06   0:00 sh -c cd '/usr/share/nginx/www' ; sleep 1000
...