PHP的preg_match_all导致Apache崩溃

3
我正在使用两个正则表达式从MySQL查询中提取赋值语句,并将它们用于创建审计跟踪。其中一个是“挑剔”的,需要引用列名等内容,而另一个则不需要。
这两个正则表达式都经过测试,并可以正确解析出值。我的问题在于,对于某些查询,使用“挑剔”的正则表达式实际上会导致Apache崩溃。
我尝试了各种方法来确定这是原因,包括保留正则表达式的代码,只修改条件以确保它不会运行(排除编译时问题或其他问题)。没有任何问题。只有当它针对特定查询运行正则表达式时,它才会崩溃,而我找不到任何明显的模式来告诉我为什么。
相关代码:
if ($picky)
    preg_match_all("/[`'\"]((?:[A-Z]|[a-z]|_|[0-9])+)[`'\"] *= *'((?:[^'\\\\]|\\\\.)*)'/", $sql, $matches);
else
    preg_match_all("/[`'\"]?((?:[A-Z]|[a-z]|_|[0-9])+)[`'\"]? *= *[`'\"]?([^`'\" ,]+)[`'\"]?/", $sql, $matches);

两者之间唯一的区别在于第一个删除了引号上的问号,使它们变得非可选,并且删除了在值上使用不同类型引号的选项——只允许单引号。将第一个正则表达式替换为第二个(用于测试目的),并使用相同的数据删除问题——这绝对与正则表达式有关。
导致我烦恼的具体SQL可在以下位置找到:
http://stackoverflow.pastebin.com/m75c2a2a0 有趣的是,当我删除了突出显示的部分时,它就可以正常工作了。试图仅提交突出显示的部分不会导致错误。
我对这里发生的事情感到非常困惑。有人能提供任何进一步调试或修复建议吗?
编辑:没有什么特别激动人心的东西,但为了完整起见,以下是Apache的相关日志条目(/var/log/apache2/error.log - 站点的error.log中没有任何内容。甚至在access log中也没有提及请求。)
[Thu Dec 10 10:08:03 2009] [notice] child pid 20835 exit signal Segmentation fault (11)

每个请求包含该查询的一个。编辑2:在Kuroki Kaze的建议下,我尝试了同样长度的胡言乱语,并得到了相同的段错误。尝试了许多不同的长度并找到了限制。6035个字符运行良好,6036个会导致段错误。编辑3:在php.ini中更改pcre.backtrack_limit和pcre.recursion_limit的值在一定程度上缓解了问题。Apache不再段错误,但我的正则表达式不能匹配字符串中的所有匹配项。显然,这是PHP/PCRE中一个已知很久(从2007年)的错误:
http://bugs.php.net/bug.php?id=40909。编辑4:我在下面的答案中发布了用于替换此特定正则表达式的代码,因为这些解决方法对我的目的来说不可接受(销售产品,无法保证php.ini的更改,而且只部分工作的正则表达式会删除我们需要的功能)。我发布的代码在公共领域中发布,不提供任何形式的保修或支持。希望能帮助其他人。 :)感谢大家的帮助!亚当
3个回答

4
有趣的是,当我删除了突出部分时,一切都正常。尝试单独提交突出部分不会导致错误。
那么提交的大小呢?如果传递等长的无意义内容,会发生什么?
编辑:拆分和合并将看起来像这样:
$strings = explode("\n", $sql);

$matches = array(array(), array(), array());

foreach ($strings AS $string) {
    preg_match_all("/[`'\"]?((?:[A-Z]|[a-z]|_|[0-9])+)[`'\"]? *= *[`'\"]?([^`'\" ,]+)[`'\"]?/", $string, $matches_temp);
    $matches[0] = array_merge($matches[0], $matches_temp[0]);
    $matches[1] = array_merge($matches[1], $matches_temp[1]);
    $matches[2] = array_merge($matches[2], $matches_temp[2]);
}

此外,preg 函数现在不是已经被弃用了吗? - Kuroki Kaze
1
好的,一个相同长度的“X”字符串会导致相同的错误。我尝试了一下,发现查询长度恰好为6035个字符时可以正常工作,而6036个字符则会出现段错误。 - NuclearDog
好的。现在你可以通过某些不匹配的标记将其拆分,从分割的字符串中获取匹配项并将它们合并。 - Kuroki Kaze
1
SQL中实际上没有任何换行符,我认为解决方案仍然会遇到单个字段超过长度(这是此处的问题)或包含换行符的问题。不过还是谢谢您 :) - NuclearDog

4

我遇到了一个类似的preg_match相关问题,同样是Apache故障。只有在我使用的CMS(WordPress)中内置的preg_match会导致此问题。

提供的"解决方法"是更改php.ini中的以下设置:

[Pcre] ;PCRE库回溯限制。 ;pcre.backtrack_limit=100000 pcre.recursion_limit=200000000 pcre.backtrack_limit=100000000

这种折衷方案适用于呈现较大页面(在我的情况下,> 200行;当其中一列限制为1500个字符的文本说明时),您将获得相当高的CPU利用率,并且我仍然看到segfaults。只是不那么频繁了。

我的网站接近生命周期结束,所以我没有太多需要(或预算)寻找真正的解决方案。但也许这可以缓解您正在遇到的问题。


1
提高这些值并没有缓解问题,但降低它们确实有用。不幸的是,正则表达式不再匹配长字段(page_content)。停止段错误对我来说肯定是一个好的临时解决方案,谢谢 :) 进一步搜索发现,这似乎是 PHP/PCRE 中一个早已知道的 bug:http://bugs.php.net/bug.php?id=40909 - NuclearDog

1

鉴于这只需要在保存页面或执行其他不太频繁执行的操作时与查询匹配,我认为以下代码的性能损失是可以接受的。它解析 SQL 查询 ($sql) 并将名称=>值对放入 $data 中。看起来工作得很好,并且可以处理大型查询。

            $quoted = '';
            $escaped = false;

            $key = '';
            $value = '';
            $target = 'key';

            for ($i=0; $i<strlen($sql); $i++)
            {
                if ($escaped)
                {
                    $$target .= $sql[$i];
                    $escaped = false;
                }
                else if ($quoted!='')
                {
                    if ($sql[$i]=='\\')
                        $escaped = true;
                    else if ($sql[$i]==$quoted)
                        $quoted = '';
                    else
                        $$target .= $sql[$i];
                }
                else
                {
                    if ($sql[$i]=='\'' || $sql[$i]=='`')
                    {
                        $quoted = $sql[$i];
                        $$target = '';
                    }
                    else if ($sql[$i]=='=')
                        $target = 'value';
                    else if ($sql[$i]==',')
                    {
                        $target = 'key';
                        $data[$key] = $value;
                        $key = '';
                        $value = '';
                    }
                }
            }

            if ($value!='')
                $data[$key] = $value;

感谢大家的帮助和指引!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接