Fix error handling inconsistency with opcache

When opcache is enabled, error handling is altered in the following ways:

 * Errors emitted during compilation bypass the user-defined error handler
 * Exceptions emitted during class linking are turned into fatal errors

Changes here make the behavior consistent regardless of opcache being enabled or
not:

 * Errors emitted during compilation and class linking are always delayed and
   handled after compilation or class linking. During handling, user-defined
   error handlers are not bypassed. Fatal errors emitted during compilation or
   class linking cause any delayed errors to be handled immediately (without
   calling user-defined error handlers, as it would be unsafe).
 * Exceptions thrown by user-defined error handlers when handling class linking
   error are not promoted to fatal errors anymore and do not prevent linking.

Fixes GH-17422.
Closes GH-18541.
Closes GH-17627.

Co-authored-by: Tim Düsterhus <tim@bastelstu.be>
This commit is contained in:
Arnaud Le Blanc 2025-07-27 10:10:20 +02:00
parent 9b777b3c35
commit 7b3e68ff69
No known key found for this signature in database
36 changed files with 408 additions and 61 deletions

View file

@ -128,6 +128,7 @@ mkdir %PHP_BUILD_DIR%\test_file_cache
rem generate php.ini rem generate php.ini
echo extension_dir=%PHP_BUILD_DIR% > %PHP_BUILD_DIR%\php.ini echo extension_dir=%PHP_BUILD_DIR% > %PHP_BUILD_DIR%\php.ini
echo opcache.file_cache=%PHP_BUILD_DIR%\test_file_cache >> %PHP_BUILD_DIR%\php.ini echo opcache.file_cache=%PHP_BUILD_DIR%\test_file_cache >> %PHP_BUILD_DIR%\php.ini
echo opcache.record_warnings=1 >> %PHP_BUILD_DIR%\php.ini
rem work-around for some spawned PHP processes requiring OpenSSL and sockets rem work-around for some spawned PHP processes requiring OpenSSL and sockets
echo extension=php_openssl.dll >> %PHP_BUILD_DIR%\php.ini echo extension=php_openssl.dll >> %PHP_BUILD_DIR%\php.ini
echo extension=php_sockets.dll >> %PHP_BUILD_DIR%\php.ini echo extension=php_sockets.dll >> %PHP_BUILD_DIR%\php.ini

5
NEWS
View file

@ -15,8 +15,11 @@ PHP NEWS
- OPcache: - OPcache:
. Disallow changing opcache.memory_consumption when SHM is already set up. . Disallow changing opcache.memory_consumption when SHM is already set up.
(timwolla) (timwolla)
. Fixed GH-15074 (Compiling opcache statically into ZTS PHP fails). (Arnaud) . Fixed bug GH-15074 (Compiling opcache statically into ZTS PHP fails).
(Arnaud)
. Make OPcache non-optional (Arnaud, timwolla) . Make OPcache non-optional (Arnaud, timwolla)
. Fixed bug GH-17422 (OPcache bypasses the user-defined error handler for
deprecations). (Arnaud, timwolla)
- OpenSSL: - OpenSSL:
. Add $digest_algo parameter to openssl_public_encrypt() and . Add $digest_algo parameter to openssl_public_encrypt() and

View file

@ -44,6 +44,12 @@ PHP 8.5 UPGRADE NOTES
. Traits are now bound before the parent class. This is a subtle behavioral . Traits are now bound before the parent class. This is a subtle behavioral
change, but should more closely match user expectations, demonstrated by change, but should more closely match user expectations, demonstrated by
GH-15753 and GH-16198. GH-15753 and GH-16198.
. Errors emitted during compilation and class linking are now always delayed
and handled after compilation or class linking. Fatal errors emitted during
compilation or class linking cause any delayed errors to be handled
immediately, without calling user-defined error handlers.
. Exceptions thrown by user-defined error handlers when handling class linking
errors are not promoted to fatal errors anymore and do not prevent linking.
- DOM: - DOM:
. Cloning a DOMNamedNodeMap, DOMNodeList, Dom\NamedNodeMap, Dom\NodeList, . Cloning a DOMNamedNodeMap, DOMNodeList, Dom\NamedNodeMap, Dom\NodeList,

View file

@ -71,6 +71,8 @@ PHP 8.5 INTERNALS UPGRADE NOTES
* zend_register_string_constant() * zend_register_string_constant()
* zend_register_stringl_constant() * zend_register_stringl_constant()
. EG(fake_scope) now is a _const_ zend_class_entry*. . EG(fake_scope) now is a _const_ zend_class_entry*.
. zend_begin_record_errors() or EG(record_errors)=true cause errors to be
delayed. Before, errors would be recorded but not delayed.
======================== ========================
2. Build system changes 2. Build system changes

View file

@ -1,5 +1,5 @@
--TEST-- --TEST--
Deprecation promoted to exception should result in fatal error during inheritance Deprecation promoted to exception during inheritance
--SKIPIF-- --SKIPIF--
<?php <?php
if (getenv('SKIP_PRELOAD')) die('skip Error handler not active during preloading'); if (getenv('SKIP_PRELOAD')) die('skip Error handler not active during preloading');
@ -17,7 +17,8 @@ $class = new class extends DateTime {
?> ?>
--EXPECTF-- --EXPECTF--
Fatal error: During inheritance of DateTime: Uncaught Exception: Return type of DateTime@anonymous::getTimezone() should either be compatible with DateTime::getTimezone(): DateTimeZone|false, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in %s:%d Fatal error: Uncaught Exception: Return type of DateTime@anonymous::getTimezone() should either be compatible with DateTime::getTimezone(): DateTimeZone|false, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in %s:%d
Stack trace: Stack trace:
#0 %s(%d): {closure:%s:%d}(8192, 'Return type of ...', '%s', 8) #0 %s(%d): {closure:%s:%d}(8192, 'Return type of ...', '%s', 8)
#1 {main} in %s on line %d #1 {main}
thrown in %s on line %d

View file

@ -0,0 +1,35 @@
--TEST--
Deprecation promoted to exception during inheritance
--SKIPIF--
<?php
if (getenv('SKIP_PRELOAD')) die('skip Error handler not active during preloading');
?>
--FILE--
<?php
set_error_handler(function($code, $message) {
throw new Exception($message);
});
try {
class C extends DateTime {
public function getTimezone() {}
public function getTimestamp() {}
};
} catch (Exception $e) {
printf("%s: %s\n", $e::class, $e->getMessage());
}
var_dump(new C());
?>
--EXPECTF--
Exception: Return type of C::getTimezone() should either be compatible with DateTime::getTimezone(): DateTimeZone|false, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice
object(C)#%d (3) {
["date"]=>
string(%d) "%s"
["timezone_type"]=>
int(3)
["timezone"]=>
string(3) "UTC"
}

View file

@ -14,5 +14,8 @@ class C implements Serializable {
?> ?>
--EXPECTF-- --EXPECTF--
Fatal error: During inheritance of C, while implementing Serializable: Uncaught Exception: C implements the Serializable interface, which is deprecated. Implement __serialize() and __unserialize() instead (or in addition, if support for old PHP versions is necessary) in %s:%d Fatal error: Uncaught Exception: C implements the Serializable interface, which is deprecated. Implement __serialize() and __unserialize() instead (or in addition, if support for old PHP versions is necessary) in %s:%d
%a Stack trace:
#0 %s(%d): {closure:%s:%d}(8192, 'C implements th...', '%s', 7)
#1 {main}
thrown in %s on line %d

View file

@ -1452,6 +1452,29 @@ ZEND_API ZEND_COLD void zend_error_zstr_at(
return; return;
} }
/* Emit any delayed error before handling fatal error */
if ((type & E_FATAL_ERRORS) && !(type & E_DONT_BAIL) && EG(num_errors)) {
uint32_t num_errors = EG(num_errors);
zend_error_info **errors = EG(errors);
EG(num_errors) = 0;
EG(errors) = NULL;
bool orig_record_errors = EG(record_errors);
EG(record_errors) = false;
/* Disable user error handler before emitting delayed errors, as
* it's unsafe to execute user code after a fatal error. */
int orig_user_error_handler_error_reporting = EG(user_error_handler_error_reporting);
EG(user_error_handler_error_reporting) = 0;
zend_emit_recorded_errors_ex(num_errors, errors);
EG(user_error_handler_error_reporting) = orig_user_error_handler_error_reporting;
EG(record_errors) = orig_record_errors;
EG(num_errors) = num_errors;
EG(errors) = errors;
}
if (EG(record_errors)) { if (EG(record_errors)) {
zend_error_info *info = emalloc(sizeof(zend_error_info)); zend_error_info *info = emalloc(sizeof(zend_error_info));
info->type = type; info->type = type;
@ -1464,6 +1487,11 @@ ZEND_API ZEND_COLD void zend_error_zstr_at(
EG(num_errors)++; EG(num_errors)++;
EG(errors) = erealloc(EG(errors), sizeof(zend_error_info*) * EG(num_errors)); EG(errors) = erealloc(EG(errors), sizeof(zend_error_info*) * EG(num_errors));
EG(errors)[EG(num_errors)-1] = info; EG(errors)[EG(num_errors)-1] = info;
/* Do not process non-fatal recorded error */
if (!(type & E_FATAL_ERRORS) || (type & E_DONT_BAIL)) {
return;
}
} }
// Always clear the last backtrace. // Always clear the last backtrace.
@ -1752,13 +1780,18 @@ ZEND_API void zend_begin_record_errors(void)
EG(errors) = NULL; EG(errors) = NULL;
} }
ZEND_API void zend_emit_recorded_errors_ex(uint32_t num_errors, zend_error_info **errors)
{
for (uint32_t i = 0; i < num_errors; i++) {
zend_error_info *error = errors[i];
zend_error_zstr_at(error->type, error->filename, error->lineno, error->message);
}
}
ZEND_API void zend_emit_recorded_errors(void) ZEND_API void zend_emit_recorded_errors(void)
{ {
EG(record_errors) = false; EG(record_errors) = false;
for (uint32_t i = 0; i < EG(num_errors); i++) { zend_emit_recorded_errors_ex(EG(num_errors), EG(errors));
zend_error_info *error = EG(errors)[i];
zend_error_zstr_at(error->type, error->filename, error->lineno, error->message);
}
} }
ZEND_API void zend_free_recorded_errors(void) ZEND_API void zend_free_recorded_errors(void)

View file

@ -444,6 +444,7 @@ ZEND_API void zend_replace_error_handling(zend_error_handling_t error_handling,
ZEND_API void zend_restore_error_handling(zend_error_handling *saved); ZEND_API void zend_restore_error_handling(zend_error_handling *saved);
ZEND_API void zend_begin_record_errors(void); ZEND_API void zend_begin_record_errors(void);
ZEND_API void zend_emit_recorded_errors(void); ZEND_API void zend_emit_recorded_errors(void);
ZEND_API void zend_emit_recorded_errors_ex(uint32_t num_errors, zend_error_info **errors);
ZEND_API void zend_free_recorded_errors(void); ZEND_API void zend_free_recorded_errors(void);
END_EXTERN_C() END_EXTERN_C()

View file

@ -1327,7 +1327,6 @@ ZEND_API zend_class_entry *zend_bind_class_in_slot(
ce = zend_do_link_class(ce, lc_parent_name, Z_STR_P(lcname)); ce = zend_do_link_class(ce, lc_parent_name, Z_STR_P(lcname));
if (ce) { if (ce) {
ZEND_ASSERT(!EG(exception));
zend_observer_class_linked_notify(ce, Z_STR_P(lcname)); zend_observer_class_linked_notify(ce, Z_STR_P(lcname));
return ce; return ce;
} }

View file

@ -295,7 +295,8 @@ struct _zend_executor_globals {
size_t fiber_stack_size; size_t fiber_stack_size;
/* If record_errors is enabled, all emitted diagnostics will be recorded, /* If record_errors is enabled, all emitted diagnostics will be recorded,
* in addition to being processed as usual. */ * and their processing is delayed until zend_emit_recorded_errors()
* is called or a fatal diagnostic is emitted. */
bool record_errors; bool record_errors;
uint32_t num_errors; uint32_t num_errors;
zend_error_info **errors; zend_error_info **errors;

View file

@ -1085,10 +1085,7 @@ static void ZEND_COLD emit_incompatible_method_error(
"Return type of %s should either be compatible with %s, " "Return type of %s should either be compatible with %s, "
"or the #[\\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice", "or the #[\\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice",
ZSTR_VAL(child_prototype), ZSTR_VAL(parent_prototype)); ZSTR_VAL(child_prototype), ZSTR_VAL(parent_prototype));
if (EG(exception)) { ZEND_ASSERT(!EG(exception));
zend_exception_uncaught_error(
"During inheritance of %s", ZSTR_VAL(parent_scope->name));
}
} }
} else { } else {
zend_error_at(E_COMPILE_ERROR, func_filename(child), func_lineno(child), zend_error_at(E_COMPILE_ERROR, func_filename(child), func_lineno(child),
@ -3561,8 +3558,6 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string
} }
#endif #endif
bool orig_record_errors = EG(record_errors);
if (ce->ce_flags & ZEND_ACC_IMMUTABLE && is_cacheable) { if (ce->ce_flags & ZEND_ACC_IMMUTABLE && is_cacheable) {
if (zend_inheritance_cache_get && zend_inheritance_cache_add) { if (zend_inheritance_cache_get && zend_inheritance_cache_add) {
zend_class_entry *ret = zend_inheritance_cache_get(ce, parent, traits_and_interfaces); zend_class_entry *ret = zend_inheritance_cache_get(ce, parent, traits_and_interfaces);
@ -3574,16 +3569,21 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string
Z_CE_P(zv) = ret; Z_CE_P(zv) = ret;
return ret; return ret;
} }
/* Make sure warnings (such as deprecations) thrown during inheritance
* will be recorded in the inheritance cache. */
zend_begin_record_errors();
} else { } else {
is_cacheable = 0; is_cacheable = 0;
} }
proto = ce; proto = ce;
} }
/* Delay and record warnings (such as deprecations) thrown during
* inheritance, so they will be recorded in the inheritance cache.
* Warnings must be delayed in all cases so that we get a consistent
* behavior regardless of cacheability. */
bool orig_record_errors = EG(record_errors);
if (!orig_record_errors) {
zend_begin_record_errors();
}
zend_try { zend_try {
if (ce->ce_flags & ZEND_ACC_IMMUTABLE) { if (ce->ce_flags & ZEND_ACC_IMMUTABLE) {
/* Lazy class loading */ /* Lazy class loading */
@ -3774,6 +3774,7 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string
} }
if (!orig_record_errors) { if (!orig_record_errors) {
zend_emit_recorded_errors();
zend_free_recorded_errors(); zend_free_recorded_errors();
} }
if (traits_and_interfaces) { if (traits_and_interfaces) {
@ -3934,10 +3935,12 @@ ZEND_API zend_class_entry *zend_try_early_bind(zend_class_entry *ce, zend_class_
orig_linking_class = CG(current_linking_class); orig_linking_class = CG(current_linking_class);
CG(current_linking_class) = is_cacheable ? ce : NULL; CG(current_linking_class) = is_cacheable ? ce : NULL;
bool orig_record_errors = EG(record_errors);
zend_try{ zend_try{
CG(zend_lineno) = ce->info.user.line_start; CG(zend_lineno) = ce->info.user.line_start;
if (is_cacheable) { if (!orig_record_errors) {
zend_begin_record_errors(); zend_begin_record_errors();
} }
@ -3959,13 +3962,13 @@ ZEND_API zend_class_entry *zend_try_early_bind(zend_class_entry *ce, zend_class_
CG(current_linking_class) = orig_linking_class; CG(current_linking_class) = orig_linking_class;
} zend_catch { } zend_catch {
EG(record_errors) = false; if (!orig_record_errors) {
zend_free_recorded_errors(); EG(record_errors) = false;
zend_free_recorded_errors();
}
zend_bailout(); zend_bailout();
} zend_end_try(); } zend_end_try();
EG(record_errors) = false;
if (is_cacheable) { if (is_cacheable) {
HashTable *ht = (HashTable*)ce->inheritance_cache; HashTable *ht = (HashTable*)ce->inheritance_cache;
zend_class_entry *new_ce; zend_class_entry *new_ce;
@ -3983,6 +3986,11 @@ ZEND_API zend_class_entry *zend_try_early_bind(zend_class_entry *ce, zend_class_
} }
} }
if (!orig_record_errors) {
zend_emit_recorded_errors();
zend_free_recorded_errors();
}
if (ZSTR_HAS_CE_CACHE(ce->name)) { if (ZSTR_HAS_CE_CACHE(ce->name)) {
ZSTR_SET_CE_CACHE(ce->name, ce); ZSTR_SET_CE_CACHE(ce->name, ce);
} }

View file

@ -650,7 +650,17 @@ ZEND_API zend_op_array *compile_file(zend_file_handle *file_handle, int type)
} }
} }
} else { } else {
bool orig_record_errors = EG(record_errors);
if (!orig_record_errors) {
zend_begin_record_errors();
}
op_array = zend_compile(ZEND_USER_FUNCTION); op_array = zend_compile(ZEND_USER_FUNCTION);
if (!orig_record_errors) {
zend_emit_recorded_errors();
zend_free_recorded_errors();
}
} }
zend_restore_lexical_state(&original_lex_state); zend_restore_lexical_state(&original_lex_state);

View file

@ -7938,7 +7938,7 @@ ZEND_VM_HANDLER(145, ZEND_DECLARE_CLASS_DELAYED, CONST, CONST)
if (zv) { if (zv) {
SAVE_OPLINE(); SAVE_OPLINE();
ce = zend_bind_class_in_slot(zv, lcname, Z_STR_P(RT_CONSTANT(opline, opline->op2))); ce = zend_bind_class_in_slot(zv, lcname, Z_STR_P(RT_CONSTANT(opline, opline->op2)));
if (!ce) { if (EG(exception)) {
HANDLE_EXCEPTION(); HANDLE_EXCEPTION();
} }
} }
@ -7962,7 +7962,7 @@ ZEND_VM_HANDLER(146, ZEND_DECLARE_ANON_CLASS, ANY, ANY, CACHE_SLOT)
if (!(ce->ce_flags & ZEND_ACC_LINKED)) { if (!(ce->ce_flags & ZEND_ACC_LINKED)) {
SAVE_OPLINE(); SAVE_OPLINE();
ce = zend_do_link_class(ce, (OP2_TYPE == IS_CONST) ? Z_STR_P(RT_CONSTANT(opline, opline->op2)) : NULL, rtd_key); ce = zend_do_link_class(ce, (OP2_TYPE == IS_CONST) ? Z_STR_P(RT_CONSTANT(opline, opline->op2)) : NULL, rtd_key);
if (!ce) { if (EG(exception)) {
HANDLE_EXCEPTION(); HANDLE_EXCEPTION();
} }
} }

View file

@ -3212,7 +3212,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DECLARE_ANON_CLASS_SPEC_HANDLE
if (!(ce->ce_flags & ZEND_ACC_LINKED)) { if (!(ce->ce_flags & ZEND_ACC_LINKED)) {
SAVE_OPLINE(); SAVE_OPLINE();
ce = zend_do_link_class(ce, (opline->op2_type == IS_CONST) ? Z_STR_P(RT_CONSTANT(opline, opline->op2)) : NULL, rtd_key); ce = zend_do_link_class(ce, (opline->op2_type == IS_CONST) ? Z_STR_P(RT_CONSTANT(opline, opline->op2)) : NULL, rtd_key);
if (!ce) { if (EG(exception)) {
HANDLE_EXCEPTION(); HANDLE_EXCEPTION();
} }
} }
@ -8014,7 +8014,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DECLARE_CLASS_DELAYED_SPEC_CON
if (zv) { if (zv) {
SAVE_OPLINE(); SAVE_OPLINE();
ce = zend_bind_class_in_slot(zv, lcname, Z_STR_P(RT_CONSTANT(opline, opline->op2))); ce = zend_bind_class_in_slot(zv, lcname, Z_STR_P(RT_CONSTANT(opline, opline->op2)));
if (!ce) { if (EG(exception)) {
HANDLE_EXCEPTION(); HANDLE_EXCEPTION();
} }
} }

View file

@ -1734,19 +1734,11 @@ static void zend_accel_set_auto_globals(int mask)
ZCG(auto_globals_mask) |= mask; ZCG(auto_globals_mask) |= mask;
} }
static void replay_warnings(uint32_t num_warnings, zend_error_info **warnings) {
for (uint32_t i = 0; i < num_warnings; i++) {
zend_error_info *warning = warnings[i];
zend_error_zstr_at(warning->type, warning->filename, warning->lineno, warning->message);
}
}
static zend_persistent_script *opcache_compile_file(zend_file_handle *file_handle, int type, zend_op_array **op_array_p) static zend_persistent_script *opcache_compile_file(zend_file_handle *file_handle, int type, zend_op_array **op_array_p)
{ {
zend_persistent_script *new_persistent_script; zend_persistent_script *new_persistent_script;
uint32_t orig_functions_count, orig_class_count; uint32_t orig_functions_count, orig_class_count;
zend_op_array *orig_active_op_array; zend_op_array *orig_active_op_array;
zval orig_user_error_handler;
zend_op_array *op_array; zend_op_array *op_array;
bool do_bailout = false; bool do_bailout = false;
accel_time_t timestamp = 0; accel_time_t timestamp = 0;
@ -1814,13 +1806,6 @@ static zend_persistent_script *opcache_compile_file(zend_file_handle *file_handl
orig_active_op_array = CG(active_op_array); orig_active_op_array = CG(active_op_array);
orig_functions_count = EG(function_table)->nNumUsed; orig_functions_count = EG(function_table)->nNumUsed;
orig_class_count = EG(class_table)->nNumUsed; orig_class_count = EG(class_table)->nNumUsed;
ZVAL_COPY_VALUE(&orig_user_error_handler, &EG(user_error_handler));
/* Override them with ours */
ZVAL_UNDEF(&EG(user_error_handler));
if (ZCG(accel_directives).record_warnings) {
zend_begin_record_errors();
}
zend_try { zend_try {
orig_compiler_options = CG(compiler_options); orig_compiler_options = CG(compiler_options);
@ -1850,13 +1835,12 @@ static zend_persistent_script *opcache_compile_file(zend_file_handle *file_handl
/* Restore originals */ /* Restore originals */
CG(active_op_array) = orig_active_op_array; CG(active_op_array) = orig_active_op_array;
EG(user_error_handler) = orig_user_error_handler;
EG(record_errors) = 0;
if (!op_array) { if (!op_array) {
/* compilation failed */ /* compilation failed */
zend_free_recorded_errors();
if (do_bailout) { if (do_bailout) {
EG(record_errors) = false;
zend_free_recorded_errors();
zend_bailout(); zend_bailout();
} }
return NULL; return NULL;
@ -1871,10 +1855,6 @@ static zend_persistent_script *opcache_compile_file(zend_file_handle *file_handl
zend_accel_move_user_functions(CG(function_table), CG(function_table)->nNumUsed - orig_functions_count, &new_persistent_script->script); zend_accel_move_user_functions(CG(function_table), CG(function_table)->nNumUsed - orig_functions_count, &new_persistent_script->script);
zend_accel_move_user_classes(CG(class_table), CG(class_table)->nNumUsed - orig_class_count, &new_persistent_script->script); zend_accel_move_user_classes(CG(class_table), CG(class_table)->nNumUsed - orig_class_count, &new_persistent_script->script);
zend_accel_build_delayed_early_binding_list(new_persistent_script); zend_accel_build_delayed_early_binding_list(new_persistent_script);
new_persistent_script->num_warnings = EG(num_errors);
new_persistent_script->warnings = EG(errors);
EG(num_errors) = 0;
EG(errors) = NULL;
efree(op_array); /* we have valid persistent_script, so it's safe to free op_array */ efree(op_array); /* we have valid persistent_script, so it's safe to free op_array */
@ -1956,7 +1936,7 @@ static zend_op_array *file_cache_compile_file(zend_file_handle *file_handle, int
} }
} }
} }
replay_warnings(persistent_script->num_warnings, persistent_script->warnings); zend_emit_recorded_errors_ex(persistent_script->num_warnings, persistent_script->warnings);
if (persistent_script->ping_auto_globals_mask & ~ZCG(auto_globals_mask)) { if (persistent_script->ping_auto_globals_mask & ~ZCG(auto_globals_mask)) {
zend_accel_set_auto_globals(persistent_script->ping_auto_globals_mask & ~ZCG(auto_globals_mask)); zend_accel_set_auto_globals(persistent_script->ping_auto_globals_mask & ~ZCG(auto_globals_mask));
@ -1965,11 +1945,22 @@ static zend_op_array *file_cache_compile_file(zend_file_handle *file_handle, int
return zend_accel_load_script(persistent_script, 1); return zend_accel_load_script(persistent_script, 1);
} }
zend_begin_record_errors();
persistent_script = opcache_compile_file(file_handle, type, &op_array); persistent_script = opcache_compile_file(file_handle, type, &op_array);
if (persistent_script) { if (persistent_script) {
if (ZCG(accel_directives).record_warnings) {
persistent_script->num_warnings = EG(num_errors);
persistent_script->warnings = EG(errors);
}
from_memory = false; from_memory = false;
persistent_script = cache_script_in_file_cache(persistent_script, &from_memory); persistent_script = cache_script_in_file_cache(persistent_script, &from_memory);
zend_emit_recorded_errors();
zend_free_recorded_errors();
return zend_accel_load_script(persistent_script, from_memory); return zend_accel_load_script(persistent_script, from_memory);
} }
@ -2168,6 +2159,8 @@ zend_op_array *persistent_compile_file(zend_file_handle *file_handle, int type)
return accelerator_orig_compile_file(file_handle, type); return accelerator_orig_compile_file(file_handle, type);
} }
zend_begin_record_errors();
SHM_PROTECT(); SHM_PROTECT();
HANDLE_UNBLOCK_INTERRUPTIONS(); HANDLE_UNBLOCK_INTERRUPTIONS();
persistent_script = opcache_compile_file(file_handle, type, &op_array); persistent_script = opcache_compile_file(file_handle, type, &op_array);
@ -2179,6 +2172,11 @@ zend_op_array *persistent_compile_file(zend_file_handle *file_handle, int type)
*/ */
from_shared_memory = false; from_shared_memory = false;
if (persistent_script) { if (persistent_script) {
if (ZCG(accel_directives).record_warnings) {
persistent_script->num_warnings = EG(num_errors);
persistent_script->warnings = EG(errors);
}
/* See GH-17246: we disable GC so that user code cannot be executed during the optimizer run. */ /* See GH-17246: we disable GC so that user code cannot be executed during the optimizer run. */
bool orig_gc_state = gc_enable(false); bool orig_gc_state = gc_enable(false);
persistent_script = cache_script_in_shared_memory(persistent_script, key, &from_shared_memory); persistent_script = cache_script_in_shared_memory(persistent_script, key, &from_shared_memory);
@ -2191,6 +2189,8 @@ zend_op_array *persistent_compile_file(zend_file_handle *file_handle, int type)
if (!persistent_script) { if (!persistent_script) {
SHM_PROTECT(); SHM_PROTECT();
HANDLE_UNBLOCK_INTERRUPTIONS(); HANDLE_UNBLOCK_INTERRUPTIONS();
zend_emit_recorded_errors();
zend_free_recorded_errors();
return op_array; return op_array;
} }
if (from_shared_memory) { if (from_shared_memory) {
@ -2204,6 +2204,9 @@ zend_op_array *persistent_compile_file(zend_file_handle *file_handle, int type)
persistent_script->dynamic_members.last_used = ZCG(request_time); persistent_script->dynamic_members.last_used = ZCG(request_time);
SHM_PROTECT(); SHM_PROTECT();
HANDLE_UNBLOCK_INTERRUPTIONS(); HANDLE_UNBLOCK_INTERRUPTIONS();
zend_emit_recorded_errors();
zend_free_recorded_errors();
} else { } else {
#ifndef ZEND_WIN32 #ifndef ZEND_WIN32
@ -2246,7 +2249,7 @@ zend_op_array *persistent_compile_file(zend_file_handle *file_handle, int type)
SHM_PROTECT(); SHM_PROTECT();
HANDLE_UNBLOCK_INTERRUPTIONS(); HANDLE_UNBLOCK_INTERRUPTIONS();
replay_warnings(persistent_script->num_warnings, persistent_script->warnings); zend_emit_recorded_errors_ex(persistent_script->num_warnings, persistent_script->warnings);
from_shared_memory = true; from_shared_memory = true;
} }
@ -2313,7 +2316,7 @@ static zend_class_entry* zend_accel_inheritance_cache_get(zend_class_entry *ce,
entry = zend_accel_inheritance_cache_find(entry, ce, parent, traits_and_interfaces, &needs_autoload); entry = zend_accel_inheritance_cache_find(entry, ce, parent, traits_and_interfaces, &needs_autoload);
if (entry) { if (entry) {
if (!needs_autoload) { if (!needs_autoload) {
replay_warnings(entry->num_warnings, entry->warnings); zend_emit_recorded_errors_ex(entry->num_warnings, entry->warnings);
if (ZCSG(map_ptr_last) > CG(map_ptr_last)) { if (ZCSG(map_ptr_last) > CG(map_ptr_last)) {
zend_map_ptr_extend(ZCSG(map_ptr_last)); zend_map_ptr_extend(ZCSG(map_ptr_last));
} }
@ -2467,9 +2470,6 @@ static zend_class_entry* zend_accel_inheritance_cache_add(zend_class_entry *ce,
entry->next = proto->inheritance_cache; entry->next = proto->inheritance_cache;
proto->inheritance_cache = entry; proto->inheritance_cache = entry;
EG(num_errors) = 0;
EG(errors) = NULL;
ZCSG(map_ptr_last) = CG(map_ptr_last); ZCSG(map_ptr_last) = CG(map_ptr_last);
zend_shared_alloc_destroy_xlat_table(); zend_shared_alloc_destroy_xlat_table();

View file

@ -0,0 +1,17 @@
--TEST--
GH-17422 (OPcache bypasses the user-defined error handler for deprecations)
--FILE--
<?php
set_error_handler(static function (int $errno, string $errstr, string $errfile, int $errline) {
echo "set_error_handler: {$errstr}", PHP_EOL;
});
require __DIR__ . "/warning.inc";
warning();
?>
--EXPECT--
set_error_handler: "continue" targeting switch is equivalent to "break"
OK: warning

View file

@ -0,0 +1,21 @@
--TEST--
GH-17422 (OPcache bypasses the user-defined error handler for deprecations) - Throwing error handler
--FILE--
<?php
set_error_handler(static function (int $errno, string $errstr, string $errfile, int $errline) {
throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
});
try {
require __DIR__ . "/warning.inc";
} catch (\Exception $e) {
echo "Caught: ", $e->getMessage(), PHP_EOL;
}
warning();
?>
--EXPECT--
Caught: "continue" targeting switch is equivalent to "break"
OK: warning

View file

@ -0,0 +1,17 @@
--TEST--
GH-17422 (OPcache bypasses the user-defined error handler for deprecations) - Fatal Error
--FILE--
<?php
set_error_handler(static function (int $errno, string $errstr, string $errfile, int $errline) {
function fatal_error() {}
function fatal_error() {}
});
require __DIR__ . "/warning.inc";
warning();
?>
--EXPECTF--
Fatal error: Cannot redeclare function fatal_error() (previously declared in %s:%d) in %s on line %d

View file

@ -0,0 +1,22 @@
--TEST--
GH-17422 (OPcache bypasses the user-defined error handler for deprecations) - eval
--FILE--
<?php
set_error_handler(static function (int $errno, string $errstr, string $errfile, int $errline) {
eval(
<<<'PHP'
function warning() {
echo "NOK", PHP_EOL;
}
PHP
);
});
require __DIR__ . "/warning.inc";
warning();
?>
--EXPECTF--
Fatal error: Cannot redeclare function warning() %s

View file

@ -0,0 +1,16 @@
--TEST--
GH-17422 (OPcache bypasses the user-defined error handler for deprecations) - require
--FILE--
<?php
set_error_handler(static function (int $errno, string $errstr, string $errfile, int $errline) {
require_once __DIR__ . "/dummy.inc";
});
require __DIR__ . "/warning.inc";
dummy();
?>
--EXPECT--
OK: dummy

View file

@ -0,0 +1,25 @@
--TEST--
GH-17422 (OPcache bypasses the user-defined error handler for deprecations) - File cache
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.file_cache="{TMP}"
opcache.file_cache_only=1
opcache.record_warnings=1
--EXTENSIONS--
opcache
--FILE--
<?php
set_error_handler(static function (int $errno, string $errstr, string $errfile, int $errline) {
echo "set_error_handler: {$errstr}", PHP_EOL;
});
require __DIR__ . "/warning.inc";
warning();
?>
--EXPECT--
set_error_handler: "continue" targeting switch is equivalent to "break"
OK: warning

View file

@ -0,0 +1,18 @@
--TEST--
GH-17422 (OPcache bypasses the user-defined error handler for deprecations) - Fatal after warning
--FILE--
<?php
set_error_handler(static function (int $errno, string $errstr, string $errfile, int $errline) {
echo "set_error_handler: {$errstr}", PHP_EOL;
});
require __DIR__ . "/warning-fatal.inc";
warning();
?>
--EXPECTF--
Warning: "continue" targeting switch is equivalent to "break" in %s on line %d
Fatal error: Cannot redeclare function warning() (previously declared in %s:%d) in %s on line %d

View file

@ -0,0 +1,14 @@
--TEST--
GH-17422 (OPcache bypasses the user-defined error handler for deprecations) - Early binding warning
--FILE--
<?php
set_error_handler(static function (int $errno, string $errstr, string $errfile, int $errline) {
echo "set_error_handler: {$errstr}", PHP_EOL;
});
require __DIR__ . "/early-bind-warning.inc";
?>
--EXPECTF--
set_error_handler: Return type of C::getTimezone() should either be compatible with DateTime::getTimezone(): DateTimeZone|false, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice

View file

@ -0,0 +1,16 @@
--TEST--
GH-17422 (OPcache bypasses the user-defined error handler for deprecations) - Early binding error after warning
--FILE--
<?php
set_error_handler(static function (int $errno, string $errstr, string $errfile, int $errline) {
echo "set_error_handler: {$errstr}", PHP_EOL;
});
require __DIR__ . "/early-bind-warning-error.inc";
?>
--EXPECTF--
Deprecated: Return type of C::getTimezone() should either be compatible with DateTime::getTimezone(): DateTimeZone|false, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in %s on line %d
Fatal error: Declaration of C::getTimestamp(C $arg): int must be compatible with DateTime::getTimestamp(): int in %s on line %d

View file

@ -0,0 +1,14 @@
--TEST--
GH-17422 (OPcache bypasses the user-defined error handler for deprecations) - Inheritance warning
--FILE--
<?php
set_error_handler(static function (int $errno, string $errstr, string $errfile, int $errline) {
echo "set_error_handler: {$errstr}", PHP_EOL;
});
require __DIR__ . "/link-warning.inc";
?>
--EXPECTF--
set_error_handler: Return type of C::getTimezone() should either be compatible with DateTime::getTimezone(): DateTimeZone|false, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice

View file

@ -0,0 +1,16 @@
--TEST--
GH-17422 (OPcache bypasses the user-defined error handler for deprecations) - Inheritance error after warning
--FILE--
<?php
set_error_handler(static function (int $errno, string $errstr, string $errfile, int $errline) {
echo "set_error_handler: {$errstr}", PHP_EOL;
});
require __DIR__ . "/link-warning-error.inc";
?>
--EXPECTF--
Deprecated: Return type of C::getTimezone() should either be compatible with DateTime::getTimezone(): DateTimeZone|false, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in %s on line %d
Fatal error: Declaration of C::getTimestamp(C $arg): int must be compatible with DateTime::getTimestamp(): int %s on line %d

View file

@ -0,0 +1,4 @@
<?php
function dummy() {
echo "OK: ", __FUNCTION__, PHP_EOL;
}

View file

@ -0,0 +1,6 @@
<?php
class C extends DateTime {
public function getTimezone() {}
public function getTimestamp(C $arg): int {}
}

View file

@ -0,0 +1,5 @@
<?php
class C extends DateTime {
public function getTimezone() {}
}

View file

@ -0,0 +1,8 @@
<?php
interface DisableEarlyBinding {}
class C extends DateTime implements DisableEarlyBinding {
public function getTimezone() {}
public function getTimestamp(C $arg): int {}
}

View file

@ -0,0 +1,7 @@
<?php
interface DisableEarlyBinding {}
class C extends DateTime implements DisableEarlyBinding {
public function getTimezone() {}
}

View file

@ -0,0 +1,9 @@
<?php
function warning() {
switch (1) {
case 1:
echo "OK: ", __FUNCTION__, PHP_EOL;
continue;
}
}
function warning() {}

View file

@ -0,0 +1,8 @@
<?php
function warning() {
switch (1) {
case 1:
echo "OK: ", __FUNCTION__, PHP_EOL;
continue;
}
}

View file

@ -1394,11 +1394,11 @@ static void zend_accel_persist_class_table(HashTable *class_table)
zend_error_info **zend_persist_warnings(uint32_t num_warnings, zend_error_info **warnings) { zend_error_info **zend_persist_warnings(uint32_t num_warnings, zend_error_info **warnings) {
if (warnings) { if (warnings) {
warnings = zend_shared_memdup_free(warnings, num_warnings * sizeof(zend_error_info *)); warnings = zend_shared_memdup(warnings, num_warnings * sizeof(zend_error_info *));
for (uint32_t i = 0; i < num_warnings; i++) { for (uint32_t i = 0; i < num_warnings; i++) {
warnings[i] = zend_shared_memdup_free(warnings[i], sizeof(zend_error_info));
zend_accel_store_string(warnings[i]->filename); zend_accel_store_string(warnings[i]->filename);
zend_accel_store_string(warnings[i]->message); zend_accel_store_string(warnings[i]->message);
warnings[i] = zend_shared_memdup(warnings[i], sizeof(zend_error_info));
} }
} }
return warnings; return warnings;

View file

@ -2054,6 +2054,7 @@ function generate_tmp_php_ini()
/* Fallback is implied, if filecache is enabled. */ /* Fallback is implied, if filecache is enabled. */
INI.WriteLine("opcache.file_cache=" + dir); INI.WriteLine("opcache.file_cache=" + dir);
INI.WriteLine("opcache.record_warnings=1");
INI.WriteLine("opcache.enable=1"); INI.WriteLine("opcache.enable=1");
INI.WriteLine("opcache.enable_cli=1"); INI.WriteLine("opcache.enable_cli=1");
} }