Skip to content

Fix crash on PHP 8.3 when a file is missing #20358

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

XOlegator
Copy link

Q A
Is bugfix? ✔️
New feature?
Breaks BC?
Fixed issues

This fixes a problem I had after migrating Craft CMS (based on Yii2) to PHP 8.3

Copy link

codecov bot commented Apr 21, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 64.85%. Comparing base (7037fd4) to head (690f9b0).

Additional details and impacted files
@@            Coverage Diff            @@
##             master   #20358   +/-   ##
=========================================
  Coverage     64.85%   64.85%           
- Complexity    11445    11446    +1     
=========================================
  Files           431      431           
  Lines         37208    37208           
=========================================
  Hits          24132    24132           
  Misses        13076    13076           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@samdark
Copy link
Member

samdark commented Apr 22, 2025

How does the crash looks like? Stacktrace?

@samdark samdark added this to the 2.0.53 milestone Apr 22, 2025
@XOlegator
Copy link
Author

XOlegator commented Apr 22, 2025

Error: filemtime(): stat failed. Backtrace:

2025-04-21 19:24:34 [web.INFO] [yii\db\Connection::open] Opening DB connection: mysql:host=127.0.0.1;dbname=craft_blog;port=3306 {"memory":1163464} 
2025-04-21 19:24:34 [web.INFO] [yii\web\Session::open] Session started {"memory":1730536} 
2025-04-21 19:24:34 [web.INFO] [nystudio107\codeeditor\CodeEditor::bootstrap] CodeEditor module bootstrapped {"memory":1758792} 
2025-04-21 19:24:34 [web.ERROR] [yii\base\ErrorException:2] yii\base\ErrorException: filemtime(): stat failed for /mnt/projects/sites/blog.ekhlakovy.ru/www/storage/runtime/cache/3b/CraftCMS--c2e6b7e4-c13e-492e-9c09-b5252006db833b35b093591572843edcc341f17455a0.bin in /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/yiisoft/yii2/caching/FileCache.php:113
Stack trace:
#0 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/craftcms/cms/src/web/ErrorHandler.php(115): craft\web\ErrorHandler->handleError(code: '...', message: '...', file: '...', line: '...')
#1 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/yiisoft/yii2/caching/FileCache.php(113): craft\web\ErrorHandler->handleError(code: '...', message: '...', file: '...', line: '...')
#2 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/yiisoft/yii2/caching/FileCache.php(113): ::filemtime(filename: '...')
#3 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/yiisoft/yii2/caching/Cache.php(134): craft\cache\FileCache->getValue(key: '...')
#4 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/craftcms/cms/src/services/ProjectConfig.php(1700): craft\cache\FileCache->get(key: '...')
#5 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/craftcms/cms/src/services/ProjectConfig.php(695): craft\services\ProjectConfig->getHadFileWriteIssues()
#6 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/craftcms/cms/src/helpers/Cp.php(207): craft\services\ProjectConfig->areChangesPending(path: '...', force: '...')
#7 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/craftcms/cms/src/web/twig/variables/Cp.php(529): craft\helpers\Cp::alerts(path: '...', fetch: '...')
#8 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/twig/twig/src/Extension/CoreExtension.php(1861): craft\web\twig\variables\Cp->getAlerts()
#9 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/craftcms/cms/src/helpers/Template.php(148): Twig\Extension\CoreExtension::getAttribute(env: '...', source: '...', object: '...', item: '...', arguments: '...', type: '...', isDefinedTest: '...', ignoreStrictCheck: '...', sandboxed: '...', lineno: '...')
#10 /mnt/projects/sites/blog.ekhlakovy.ru/www/storage/runtime/compiled_templates/11/11779b7006abc2b2e3d49db10de92a46.php(46): craft\helpers\Template::attribute(env: '...', source: '...', object: '...', item: '...', arguments: '...', type: '...', isDefinedTest: '...', ignoreStrictCheck: '...', sandboxed: '...', lineno: '...')
#11 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/twig/twig/src/Template.php(343): __TwigTemplate_e1a825c815db98fb38f1f494a5134d31->doDisplay(context: '...', blocks: '...')
#12 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/twig/twig/src/Template.php(358): __TwigTemplate_909b19cdb5d17880fc3de7ef93bebdc0->display(context: '...', blocks: '...')
#13 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/twig/twig/src/TemplateWrapper.php(35): __TwigTemplate_909b19cdb5d17880fc3de7ef93bebdc0->render(context: '...')
#14 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/twig/twig/src/Environment.php(320): Twig\TemplateWrapper->render(context: '...')
#15 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/craftcms/cms/src/web/View.php(581): craft\web\twig\Environment->render(name: '...', context: '...')
#16 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/craftcms/cms/src/web/View.php(634): craft\web\View->renderTemplate(template: '...', variables: '...', templateMode: '...')
#17 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/craftcms/cms/src/web/TemplateResponseFormatter.php(57): craft\web\View->renderPageTemplate(template: '...', variables: '...', templateMode: '...')
#18 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/yiisoft/yii2/web/Response.php(1109): craft\web\TemplateResponseFormatter->format(response: '...')
#19 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/craftcms/cms/src/web/Response.php(341): craft\web\Response->prepare()
#20 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/yiisoft/yii2/web/Response.php(340): craft\web\Response->prepare()
#21 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/yiisoft/yii2/base/Application.php(390): craft\web\Response->send()
#22 /mnt/projects/sites/blog.ekhlakovy.ru/www/webhome/index.php(12): craft\web\Application->run()
#23 {main} {"memory":10969160,"exception":"[object] (yii\\base\\ErrorException(code: 2): filemtime(): stat failed for /mnt/projects/sites/blog.ekhlakovy.ru/www/storage/runtime/cache/3b/CraftCMS--c2e6b7e4-c13e-492e-9c09-b5252006db833b35b093591572843edcc341f17455a0.bin at /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/yiisoft/yii2/caching/FileCache.php:113)"} 

@@ -110,7 +110,7 @@ protected function getValue($key)
{
$cacheFile = $this->getCacheFile($key);

if (@filemtime($cacheFile) > time()) {
if (file_exists($cacheFile) && @filemtime($cacheFile) > time()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a bit weird, cause @ should've suppressed the error. Any idea why it doesn't work?

Introducing file_exists make the operation non-atomic which isn't great if we're talking about cache.

A more universal way would've been something like how it is done in Yii3...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into the issue a bit deeper. In my Craft CMS setup, I had DEV_MODE=true enabled. In this mode, a PHP Warning interrupts program execution just like a PHP Error. So, the problem with the missing cache file must be happening at a different level.

Could you clarify what exactly makes the cache file operations non-atomic in this case? Yes, there are now two file operations: checking if the file exists and checking its modification time. But isn’t that the whole point of this method? First, verify the file exists, and only if it does, check its modification time. With the double check, we’d simply exit at the first step if the file isn’t there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you first check if file exist, and then read mtime from it, this means that file could be deleted by other process between these two operations. So file_exists doesn't completely protect you from such errors, it only makes them appear much less frequently (at the cost of additional call, which is not necessary in 99.99% setups).

@xicond
Copy link
Contributor

xicond commented Apr 23, 2025

Warning
Prior to PHP 8.0.0, the error_reporting() called inside the custom error handler always returned 0 if the error was suppressed by the @ operator. As of PHP 8.0.0, it returns the value of this (bitwise) expression: E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR | E_PARSE.

As per code yii\base\ErrorHandler::handleError

    public function handleError($code, $message, $file, $line)
    {
        if (error_reporting() & $code) {
            // load ErrorException manually here because autoloading them will not work
            // when error occurs while autoloading a class
            if (!class_exists('yii\\base\\ErrorException', false)) {
                require_once __DIR__ . '/ErrorException.php';
            }
            $exception = new ErrorException($message, $code, $code, $file, $line);

            if (PHP_VERSION_ID < 70400) {
                // prior to PHP 7.4 we can't throw exceptions inside of __toString() - it will result a fatal error
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
                array_shift($trace);
                foreach ($trace as $frame) {
                    if ($frame['function'] === '__toString') {
                        $this->handleException($exception);
                        if (defined('HHVM_VERSION')) {
                            flush();
                        }
                        exit(1);
                    }
                }
            }

            throw $exception;
        }

        return false;
    }

So the handleError will show error page at the end for PHP ver >= 8.0.0

for example:

set_error_handler(function($severity, $message, $file, $line) {
    echo "Error handler called! Message: $message\n";
    echo "error_reporting() inside handler: " . error_reporting() . "\n"; // 4437 in PHP 8+
    return true; // Prevent PHP's default error handler from running
});

$filename = "non_existent_file.txt";
$mtime = @filemtime($filename);

if ($mtime === false) {
    echo "filemtime failed (returned false)\n";
}

@rob006
Copy link
Contributor

rob006 commented Apr 23, 2025

As of PHP 8.0.0, it returns the value of this (bitwise) expression: E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR | E_PARSE.

filemtime() emits E_WARNING, so this change doesn't affect this call.

@xicond
Copy link
Contributor

xicond commented Apr 25, 2025

set_error_handler(function($severity, $message, $file, $line) {
echo "Error handler called! Message: $message\n";
echo "error_reporting() inside handler: " . error_reporting() . "\n"; // 4437 in PHP 8+
return true; // Prevent PHP's default error handler from running
});

$filename = "non_existent_file.txt";
$mtime = @filemtime($filename);

if ($mtime === false) {
echo "filemtime failed (returned false)\n";
}

I'm not sure, maybe because my error_level still contain E_WARNING

Error handler called! Message: filemtime(): stat failed for non_existent_file.txt
error_reporting() inside handler: 4437
filemtime failed (returned false)

But I tried the simple test on 8.2, it is shown the error

set_error_handler(function($severity, $message, $file, $line) {
    echo "Error handler called! Message: $message\n";
    echo "error_reporting() inside handler: " . error_reporting() . "\n"; // 4437 in PHP 8+
    return true; // Prevent PHP's default error handler from running
});

$filename = "non_existent_file.txt";
$mtime = @filemtime($filename);

if ($mtime === false) {
    echo "filemtime failed (returned false)\n";
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants