简单的问题。复杂的答案!
是的,这类正则表达式会重复地(并且默默地)使 Apache/PHP 崩溃,并由于堆栈溢出而导致未处理的分段错误!
背景:
PHPpreg_*
系列的正则表达式函数使用 Philip Hazel 强大的PCRE 库。使用这个库,有一类正则表达式需要对其内部match()
函数进行大量递归调用,这会占用大量堆栈空间,(使用的堆栈空间与匹配的主题字符串的大小成正比) . 因此,如果主题字符串太长,就会发生堆栈溢出和相应的分段错误。此行为在PCRE 文档末尾标题为pcrestack的部分下进行了描述。
PHP 错误 1:PHP 集:pcre.recursion_limit
太大。
PCRE 文档描述了如何通过将递归深度限制为一个安全值来避免堆栈溢出分段错误,该安全值大致等于链接应用程序的堆栈大小除以 500。当递归深度按照建议适当限制时,库不会生成堆栈溢出,而是优雅地退出并显示错误代码。在 PHP 中,这个最大递归深度由pcre.recursion_limit
配置变量指定,并且(不幸的是)默认值设置为 100,000。这个值太大了!pcre.recursion_limit
以下是各种可执行堆栈大小的安全值表:
Stacksize pcre.recursion_limit
64 MB 134217
32 MB 67108
16 MB 33554
8 MB 16777
4 MB 8388
2 MB 4194
1 MB 2097
512 KB 1048
256 KB 524
因此,对于 Apache webserver ( httpd.exe
) 的 Win32 版本,其堆栈大小(相对较小)为 256KB,正确的值pcre.recursion_limit
应该设置为 524。这可以通过以下 PHP 代码行来完成:
ini_set("pcre.recursion_limit", "524"); // PHP default is 100,000.
将此代码添加到 PHP 脚本时,不会发生堆栈溢出,而是会生成有意义的错误代码。也就是说,它应该生成错误代码!(但不幸的是,由于另一个 PHP 错误,preg_match()
没有。)
PHP 错误 2:preg_match()
错误时不返回 FALSE。
PHP 文档preg_match()
说它在错误时返回 FALSE。不幸的是,PHP 5.3.3 及以下版本有一个错误(#52732),错误时preg_match()
不返回FALSE
(而是返回int(0)
,与不匹配时返回的值相同)。此错误已在 PHP 版本 5.3.4 中修复。
解决方案:
假设您将继续使用 WAMP 2.0(使用 PHP 5.3.0),解决方案需要考虑上述两个错误。以下是我的建议:
- 需要降低
pcre.recursion_limit
到安全值:524。
- 每当
preg_match()
返回除int(1)
.
- 如果
preg_match()
返回int(1)
,则匹配成功。
- 如果
preg_match()
返回int(0)
,则匹配不成功,或者出现错误。
这是脚本的修改版本(旨在从命令行运行),它确定导致递归限制错误的主题字符串长度:
<?php
// This test script is designed to be run from the command line.
// It measures the subject string length that results in a
// PREG_RECURSION_LIMIT_ERROR error in the preg_match() function.
echo("Entering TEST.PHP...\n");
// Set and display pcre.recursion_limit. (set to stacksize / 500).
// Under Win32 httpd.exe has a stack = 256KB and 8MB for php.exe.
//ini_set("pcre.recursion_limit", "524"); // Stacksize = 256KB.
ini_set("pcre.recursion_limit", "16777"); // Stacksize = 8MB.
echo(sprintf("PCRE pcre.recursion_limit is set to %s\n",
ini_get("pcre.recursion_limit")));
function parseAPIResults($results){
$pattern = "/\[(.|\n)+\]/";
$resultsArray = preg_match($pattern, $results, $matches);
if ($resultsArray === 1) {
$msg = 'Successful match.';
} else {
// Either an unsuccessful match, or a PCRE error occurred.
$pcre_err = preg_last_error(); // PHP 5.2 and above.
if ($pcre_err === PREG_NO_ERROR) {
$msg = 'Successful non-match.';
} else {
// preg_match error!
switch ($pcre_err) {
case PREG_INTERNAL_ERROR:
$msg = 'PREG_INTERNAL_ERROR';
break;
case PREG_BACKTRACK_LIMIT_ERROR:
$msg = 'PREG_BACKTRACK_LIMIT_ERROR';
break;
case PREG_RECURSION_LIMIT_ERROR:
$msg = 'PREG_RECURSION_LIMIT_ERROR';
break;
case PREG_BAD_UTF8_ERROR:
$msg = 'PREG_BAD_UTF8_ERROR';
break;
case PREG_BAD_UTF8_OFFSET_ERROR:
$msg = 'PREG_BAD_UTF8_OFFSET_ERROR';
break;
default:
$msg = 'Unrecognized PREG error';
break;
}
}
}
return($msg);
}
// Build a matching test string of increasing size.
function buildTestString() {
static $content = "";
$content .= "A";
return '['. $content .']';
}
// Find subject string length that results in error.
for (;;) { // Infinite loop. Break out.
$str = buildTestString();
$msg = parseAPIResults($str);
printf("Length =%10d\r", strlen($str));
if ($msg !== 'Successful match.') break;
}
echo(sprintf("\nPCRE_ERROR = \"%s\" at subject string length = %d\n",
$msg, strlen($str)));
echo("Exiting TEST.PHP...");
?>
当您运行此脚本时,它会提供主题字符串当前长度的连续读数。如果pcre.recursion_limit
保留太高的默认值,这允许您测量导致可执行文件崩溃的字符串长度。
注释:
- 在调查这个问题的答案之前,我不知道PCRE 库中发生错误时
preg_match()
无法返回的 PHP 错误。FALSE
这个错误肯定会质疑很多使用preg_match
! (我当然会清点我自己的 PHP 代码。)
- 在 Windows 下,Apache webserver 可执行文件 (
httpd.exe
) 的堆栈大小为 256KB。PHP 命令行可执行文件 ( php.exe
) 的堆栈大小为 8MB。安全值pcre.recursion_limit
应根据脚本运行的可执行文件(分别为 524 和 16777)设置。
- 在 *nix 系统下,Apache webserver 和命令行可执行文件通常都使用 8MB 的堆栈大小构建,因此不会经常遇到这个问题。
- PHP 开发人员应将默认值设置
pcre.recursion_limit
为安全值。
- PHP 开发人员应将
preg_match()
错误修复应用于 PHP 5.2 版。
- 可以使用CFF Explorer免费软件程序手动修改 Windows 可执行文件的堆栈大小。您可以使用此程序来增加 Apache
httpd.exe
可执行文件的堆栈大小。(这在 XP 下有效,但 Vista 和 Win7 可能会抱怨。)