Edit report at https://bugs.php.net/bug.php?id=77193&edit=1
ID: 77193
Updated by: ab@php.net
Reported by: mlocati at gmail dot com
Summary: Infinite loop in preg_replace_callback
-Status: Open
+Status: Feedback
Type: Bug
Package: PCRE related
PHP Version: 7.3.0RC6
Block user comment: N
Private report: N
New Comment:
@mlocati thanks, with the docker way reproduced it as well.
I still can't repro it on a reduced snippet. I guess, the method call inside the callback
somehow resets the match context (so somewhere in the call trace is a PCRE API call), thus
preventing loop termination. Could you please check, whether this fixes it?
diff --git a/ext/pcre/php_pcre.c b/ext/pcre/php_pcre.c
index 5165209b85..f8fc721d81 100644
--- a/ext/pcre/php_pcre.c
+++ b/ext/pcre/php_pcre.c
@@ -1880,6 +1880,9 @@ matched:
new_len = result_len + offsets[0] - start_offset; /* part before the match
*/
+ /* Advance to the next piece. */
+ start_offset = offsets[1];
+
/* Use custom function to get replacement string and its length. */
eval_result = preg_do_repl_func(fci, fcc, subject, offsets, subpat_names,
count,
pcre2_get_mark(match_data));
@@ -1908,9 +1911,6 @@ matched:
limit--;
- /* Advance to the next piece. */
- start_offset = offsets[1];
-
/* If we have matched an empty string, mimic what Perl's /g options
does.
This turns out to be rather cunning. First we set PCRE2_NOTEMPTY_ATSTART
and try
the match again at the same point. If this fails (picked up above) we
Previous Comments:
------------------------------------------------------------------------
[2018-11-27 15:56:27] mlocati at gmail dot com
> This doesn't look like a PCRE issue. URL::to() seems to block the callback,
> so then it never returns. The actual issue should be somewhere later in
> the call stack starting from URL::to(), something like an endless loop or
> unterminated recursive call.
I modified the code a bit to show what's happening.
With this code:
ob_end_clean(); // Required because PHPUnit enables output buffering
echo 'Method: ', __METHOD__, "\n";
echo 'Text: ', $text, "\n";
$text = preg_replace_callback(
'/{CCM:CID_([0-9]+)}/i',
function ($matches) {
print_r($matches);
$cID = $matches[1];
if ($cID > 0) {
$c = Page::getByID($cID, 'ACTIVE');
if ($c->isActive()) {
return (string) \URL::to($c);
}
}
},
$text
);
The output is the following:
Method: Concrete\Core\Editor\LinkAbstractor::translateFrom
Text: <a href="{CCM:CID_3}">Super Cool!</a>
Array
(
[0] => {CCM:CID_3}
[1] => 3
)
Array
(
[0] => {CCM:CID_3}
[1] => 3
)
Array
(
[0] => {CCM:CID_3}
[1] => 3
)
[...endless list of Array()...]
As you can see, the \URL::to() call is not blocking the execution, but the callback is called
forever even if we have just one match.
------------------------------------------------------------------------
[2018-11-27 15:05:34] ab@php.net
This doesn't look like a PCRE issue. URL::to() seems to block the callback, so then it never
returns. The actual issue should be somewhere later in the call stack starting from URL::to(),
something like an endless loop or unterminated recursive call.
I'm going to check the suggested repro way with the docker container next days otherwise.
Thanks.
------------------------------------------------------------------------
[2018-11-25 19:21:55] mlocati at gmail dot com
I tried both with
pcre.jit=0
and with
pcre.jit=1
in php.ini.
The infinite loop also occurs when I remove the "return" instruction from "return
(string) \URL::to($c);", and always returning an empty string, that is:
$text = preg_replace_callback(
'/{CCM:CID_([0-9]+)}/i',
function ($matches) {
$cID = $matches[1];
if ($cID > 0) {
$c = Page::getByID($cID, 'ACTIVE');
if ($c->isActive()) {
(string) \URL::to($c);
}
}
return '';
},
$text
);
------------------------------------------------------------------------
[2018-11-25 12:37:21] cmb@php.net
Does this happen regardless of the value of pcre.jit? Also, does
this happen if you actually return an empty string from the
callback instead of the implicit NULL?
------------------------------------------------------------------------
[2018-11-23 13:55:58] mlocati at gmail dot com
Description:
------------
While testing the develop branch of concrete5 for PHP 7.3 compatibility, I've seen that the
code at https://github.com/concrete5/concrete5/blob/27bcd1b36ef35f21b0df32bfd7e57d2ecf347d65/concrete/src/Editor/LinkAbstractor.php#L97-L109
loops forever.
Here's a copy of the code:
$text = preg_replace_callback(
'/{CCM:CID_([0-9]+)}/i',
function ($matches) {
$cID = $matches[1];
if ($cID > 0) {
$c = Page::getByID($cID, 'ACTIVE');
if ($c->isActive()) {
return (string) \URL::to($c);
}
}
},
$text
);
If I comment the "return (string) \URL::to($c);" line, everything works fine, but with
that line the closure is called an infinite number of times.
I tried hard but I really can't replicate this issue in a simpler test case.
I tested it both on Windows (PHP 7.3.0RC6 32bit TS) and on the php:7.3.0RC5-cli-stretch docker image
(sorry, no RC6 available yet).
To replicate the problem in the same environment as mine, you can run the php:7.3.0RC5-cli-stretch
docker image ("docker run --rm -it php:7.3.0RC5-cli-stretch bash") and call these shell
commands:
# Update the APT repository & install required packages
apt update && apt install -y mysql-server git unzip
# Setup MySQL/MariaDB
service mysql start
mysql -e "CREATE USER 'travis'@'localhost' IDENTIFIED BY '';
GRANT ALL PRIVILEGES ON * . * TO 'travis'@'localhost'; FLUSH PRIVILEGES;"
# Build and enable GD and PDO_MYSQL
curl https://raw.githubusercontent.com/mlocati/docker-php-extension-installer/master/install-php-extensions
-o /usr/local/bin/install-php-extensions
chmod +x /usr/local/bin/install-php-extensions
install-php-extensions gd pdo_mysql
# Install Composer
php -r "copy('https://getcomposer.org/installer',
'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') ===
'93b54496392c062774670ac18b134c3b3a95e5a5e5c8f1a9f115f203b75bf9a129d5daa8ba6a13e2cc8a1da0806388a8')
{ echo 'Installer verified'; } else { echo 'Installer corrupt';
unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"
chmod a+x composer.phar
mv composer.phar /usr/local/bin/composer
# Clone repository
git clone --depth=1 https://github.com/concrete5/concrete5.git
cd concrete5
# Patch required for making the tests work
sed -i -e 's/localhost/127.0.0.1/g' tests/config/database.php
# Install composer dependencies
composer install
# Execute the test
composer test -- --filter=ContentPageTranslateTest::testFrom
The test will last forever (well, actually until PHP goes out of memory). On previous PHP versions
everything works fine.
------------------------------------------------------------------------
--
Edit this bug report at https://bugs.php.net/bug.php?id=77193&edit=1