Description
Since PHP 8.6.0 (tested on 8.6.0RC2, and present on the current PHP-8.6 and master branches), a new Fiber starts with error_reporting set to 0 whenever the error_reporting INI directive has no value. That is the default: without a php.ini (e.g. the official Docker images, or php -n), the directive is unset. All diagnostics raised inside the fiber are then silently suppressed. Outside the fiber, error_reporting() is E_ALL as expected.
The following code:
<?php
echo error_reporting(), "\n";
(new Fiber(function () {
echo error_reporting(), "\n";
echo $undefined;
echo "after\n";
}))->start();
Run as php -n -d display_errors=1 test.php, it resulted in this output:
But I expected this output instead (what PHP 8.5 prints):
30719
30719
Warning: Undefined variable $undefined in /test.php on line 5
after
Behaviour matrix, error_reporting() as seen inside the fiber (same results on NTS and ZTS builds):
| Setup |
8.5 |
8.6.0RC2 |
| no php.ini (official image) |
30719 |
0 |
php -n |
30719 |
0 |
-d error_reporting=0 |
0 |
0 |
-d error_reporting=E_ALL |
30719 |
30719 |
error_reporting(E_ALL) called at runtime before the fiber, directive unset |
30719 |
0 |
Cause
Introduced by cd75300 (#21146).
zend_fiber_execute() seeds the fiber's EG(error_reporting) from the directive, falling back to E_ALL when the directive is unset:
/* Determine the current error_reporting ini setting. */
zend_long error_reporting = zend_ini_long_literal("error_reporting");
/* If error_reporting is 0 and not explicitly set to 0, zend_ini_str returns a null pointer. */
if (!error_reporting && !zend_ini_str_literal("error_reporting")) {
error_reporting = E_ALL;
}
That commit ("Audit INI functions and macros, and replace them with better alternatives") replaced INI_INT()/INI_STR() with zend_ini_long_literal()/zend_ini_str_literal(). The old INI_STR() expanded to zend_ini_string_ex(name, len, 0, NULL), which returns NULL when the entry exists but has no value. error_reporting is registered with a NULL default in Zend/zend.c. The new zend_ini_str() returns ZSTR_EMPTY_ALLOC() in that case and returns NULL only when the directive does not exist at all. So the !zend_ini_str_literal(...) check is never true for error_reporting, and the E_ALL fallback is dead code. The comment above it still describes the old behaviour.
zend_ini_string() has the same empty-string mapping, so it is not a drop-in fix either. zend_ini_str_ex() keeps the old semantics:
if (!error_reporting && !zend_ini_str_ex("error_reporting", sizeof("error_reporting") - 1, false, NULL)) {
error_reporting = E_ALL;
}
I grepped PHP-8.6 for other callers that null-check the result of zend_ini_str() / zend_ini_str_literal(). The only other ones (ext/com_dotnet, ext/standard/basic_functions.c) use NULL to mean "directive does not exist", which the new function still reports correctly. zend_fibers.c appears to be the only affected site. The "Unset directive" behaviour change of zend_ini_str() is not mentioned in UPGRADING.INTERNALS, so third-party code that migrated INI_STR() the same way may have the same issue.
PHP Version
PHP 8.6.0RC2 (also present on PHP-8.6 at 6f49ae8 and master at 99c2dba). Not affected: PHP 8.5.
Operating System
Alpine Linux 3.23 (official php:8.6.0RC2-cli-alpine and php:8.6.0RC2-zts-alpine3.23 images); not OS-specific.
Description
Since PHP 8.6.0 (tested on 8.6.0RC2, and present on the current
PHP-8.6andmasterbranches), a newFiberstarts witherror_reportingset to0whenever theerror_reportingINI directive has no value. That is the default: without aphp.ini(e.g. the official Docker images, orphp -n), the directive is unset. All diagnostics raised inside the fiber are then silently suppressed. Outside the fiber,error_reporting()isE_ALLas expected.The following code:
Run as
php -n -d display_errors=1 test.php, it resulted in this output:But I expected this output instead (what PHP 8.5 prints):
Behaviour matrix,
error_reporting()as seen inside the fiber (same results on NTS and ZTS builds):php -n-d error_reporting=0-d error_reporting=E_ALLerror_reporting(E_ALL)called at runtime before the fiber, directive unsetCause
Introduced by cd75300 (#21146).
zend_fiber_execute()seeds the fiber'sEG(error_reporting)from the directive, falling back toE_ALLwhen the directive is unset:That commit ("Audit INI functions and macros, and replace them with better alternatives") replaced
INI_INT()/INI_STR()withzend_ini_long_literal()/zend_ini_str_literal(). The oldINI_STR()expanded tozend_ini_string_ex(name, len, 0, NULL), which returnsNULLwhen the entry exists but has no value.error_reportingis registered with aNULLdefault inZend/zend.c. The newzend_ini_str()returnsZSTR_EMPTY_ALLOC()in that case and returnsNULLonly when the directive does not exist at all. So the!zend_ini_str_literal(...)check is never true forerror_reporting, and theE_ALLfallback is dead code. The comment above it still describes the old behaviour.zend_ini_string()has the same empty-string mapping, so it is not a drop-in fix either.zend_ini_str_ex()keeps the old semantics:I grepped
PHP-8.6for other callers that null-check the result ofzend_ini_str()/zend_ini_str_literal(). The only other ones (ext/com_dotnet,ext/standard/basic_functions.c) useNULLto mean "directive does not exist", which the new function still reports correctly.zend_fibers.cappears to be the only affected site. The "Unset directive" behaviour change ofzend_ini_str()is not mentioned inUPGRADING.INTERNALS, so third-party code that migratedINI_STR()the same way may have the same issue.PHP Version
PHP 8.6.0RC2 (also present on
PHP-8.6at 6f49ae8 andmasterat 99c2dba). Not affected: PHP 8.5.Operating System
Alpine Linux 3.23 (official
php:8.6.0RC2-cli-alpineandphp:8.6.0RC2-zts-alpine3.23images); not OS-specific.