|
php.net | support | documentation | report a bug | advanced search | search howto | statistics | random bug | login |
[2018-11-23 13:55 UTC] 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. PatchesPull RequestsHistoryAllCommentsChangesGit/SVN commits
|
|||||||||||||||||||||||||||
Copyright © 2001-2025 The PHP GroupAll rights reserved. |
Last updated: Sun Oct 26 05:00:01 2025 UTC |
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 );> 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.@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) weI wonder whether we don't have to call pcre2_get_mark() before calling pcre2_get_ovector_pointer(), i.e. partly reverting commit b81d712. I mean something like this instead the patch above: ext/pcre/php_pcre.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ext/pcre/php_pcre.c b/ext/pcre/php_pcre.c index 5165209b85..4277ffb6aa 100644 --- a/ext/pcre/php_pcre.c +++ b/ext/pcre/php_pcre.c @@ -1792,6 +1792,7 @@ static zend_string *php_pcre_replace_func_impl(pcre_cache_entry *pce, zend_strin char *match, /* The current match */ *piece; /* The current piece of subject */ size_t result_len; /* Length of result */ + PCRE2_SPTR mark = NULL; /* Target for MARK name */ zend_string *result; /* Result of replacement */ zend_string *eval_result; /* Result of custom function */ pcre2_match_data *match_data; @@ -1852,6 +1853,8 @@ static zend_string *php_pcre_replace_func_impl(pcre_cache_entry *pce, zend_strin while (1) { piece = subject + start_offset; + mark = pcre2_get_mark(match_data); + if (count >= 0 && limit) { /* Check for too many substrings condition. */ if (UNEXPECTED(count == 0)) { @@ -1881,8 +1884,7 @@ matched: new_len = result_len + offsets[0] - start_offset; /* part before the match */ /* 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)); + eval_result = preg_do_repl_func(fci, fcc, subject, offsets, subpat_names, count, mark); ZEND_ASSERT(eval_result); new_len = zend_safe_address_guarded(1, ZSTR_LEN(eval_result), new_len); [1] <http://git.php.net/?p=php-src.git;a=commit;h=b81d712961aa3cbc64dc8bb521f2427cf443e550>It took me quite some time to keep removing stuff to get to a test script as simple as possible, and here it is: <?php $text = '{CCM:CID_2}'; echo '1'; preg_replace_callback( '/([0-9]+)/i', function ($matches) { echo $matches[1]; filter_var('http', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^http$/i']]); }, $text ); echo '3'; It should print '123', but it prints '1' and an infinite number of '2'. If you set $text to just a digit (eg '2'), just '12' is printed out, and PHP quits with an error level -1073741819 (I'm using a 32-bit PHP).What happens with this? <?php $text = '{CCM:CID_2}'; echo '1'; preg_replace_callback( '/([0-9]+)/i', function ($matches) { echo $matches[1]; preg_match('/^http$/i', ''); }, $text ); echo '3';