简单的问题。复杂的答案!
是的,这类正则表达式会重复(且无声地)使 Apache/PHP 崩溃,并由于堆栈溢出导致未处理的分段错误!
背景:
PHP preg_* 系列正则表达式函数使用 Philip Hazel 强大的 PCRE library。使用这个库,有一类正则表达式需要对其内部 match() 函数进行大量递归调用,这会占用大量堆栈空间(并且使用的堆栈空间与主题字符串的大小成正比)被匹配)。因此,如果主题字符串太长,就会发生堆栈溢出和相应的分段错误。此行为在末尾标题为 pcrestack 的部分下的 PCRE documentation 中进行了描述。
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 网络服务器 (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。
preg_match() 的 PHP 文档说它在出错时返回 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) 以外的任何内容时,都需要显式检查PCRE 错误。
- 如果
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() 无法返回 FALSE 的 PHP 错误。这个错误肯定会质疑很多使用preg_match 的代码! (我肯定会清点我自己的 PHP 代码。)
- 在 Windows 下,Apache 网络服务器可执行文件 (
httpd.exe) 的堆栈大小为 256KB。 PHP 命令行可执行文件 (php.exe) 的堆栈大小为 8MB。 pcre.recursion_limit 的安全值应根据运行脚本的可执行文件(分别为 524 和 16777)设置。
- 在 *nix 系统下,Apache 网络服务器和命令行可执行文件通常都使用 8MB 的堆栈大小构建,因此不会经常遇到此问题。
- PHP 开发人员应将默认值
pcre.recursion_limit 设置为安全值。
- PHP 开发人员应将
preg_match() 错误修复应用于 PHP 5.2 版。
- 可以使用CFF Explorer 免费软件程序手动修改Windows 可执行文件的堆栈大小。您可以使用此程序来增加 Apache
httpd.exe 可执行文件的堆栈大小。 (这在 XP 下有效,但 Vista 和 Win7 可能会报错。)