From ba89862bf7580435f2d192bb8774d8fdd3face81 Mon Sep 17 00:00:00 2001 From: Alessio Attilio Date: Mon, 26 Oct 2026 21:23:35 +0100 Subject: [PATCH 1/5] Fix repeated error suppression across error types --- .../ignore_repeated_errors_interleaved.phpt | 15 ++++++ main/main.c | 49 ++++++++++++++----- main/php_globals.h | 3 ++ 3 files changed, 55 insertions(+), 12 deletions(-) create mode 100644 Zend/tests/ignore_repeated_errors_interleaved.phpt diff --git a/Zend/tests/ignore_repeated_errors_interleaved.phpt b/Zend/tests/ignore_repeated_errors_interleaved.phpt new file mode 100644 index 000000000000..7dd764aa1871 --- /dev/null +++ b/Zend/tests/ignore_repeated_errors_interleaved.phpt @@ -0,0 +1,15 @@ +--TEST-- +Test ignore_repeated_errors with interleaved error types +--INI-- +ignore_repeated_errors=1 +error_reporting=-1 +--FILE-- + +--EXPECTF-- +Warning: Undefined variable $missing in %s on line %d + +Deprecated: trim(): Passing null to parameter #1 ($string) of type string is deprecated in %s on line %d diff --git a/main/main.c b/main/main.c index ee7d1cca69b5..df8c49e8e204 100644 --- a/main/main.c +++ b/main/main.c @@ -1286,7 +1286,7 @@ PHPAPI void php_html_puts(const char *str, size_t size) } /* }}} */ -static void clear_last_error(void) { +static void clear_last_error(bool clear_repeated_errors) { if (PG(last_error_message)) { zend_string_release(PG(last_error_message)); PG(last_error_message) = NULL; @@ -1295,6 +1295,16 @@ static void clear_last_error(void) { zend_string_release(PG(last_error_file)); PG(last_error_file) = NULL; } + if (clear_repeated_errors) { + for (uint32_t i = 0; i < 32; i++) { + if (PG(repeated_error_messages)[i]) { + zend_string_release(PG(repeated_error_messages)[i]); + PG(repeated_error_messages)[i] = NULL; + zend_string_release(PG(repeated_error_files)[i]); + PG(repeated_error_files)[i] = NULL; + } + } + } } #if ZEND_DEBUG @@ -1328,16 +1338,20 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c { bool display; int type = orig_type & E_ALL; + uint32_t repeated_error_index = 0; zend_string *backtrace = ZSTR_EMPTY_ALLOC(); + zend_string *repeated_error_file = error_filename ? error_filename : ZSTR_KNOWN(ZEND_STR_UNKNOWN_CAPITALIZED); + + while (repeated_error_index < 31 && !(type & (1U << repeated_error_index))) { + repeated_error_index++; + } /* check for repeated errors to be ignored */ - if (PG(ignore_repeated_errors) && PG(last_error_message)) { - /* no check for PG(last_error_file) is needed since it cannot - * be NULL if PG(last_error_message) is not NULL */ - if (!zend_string_equals(PG(last_error_message), message) + if (PG(ignore_repeated_errors) && PG(repeated_error_messages)[repeated_error_index]) { + if (!zend_string_equals(PG(repeated_error_messages)[repeated_error_index], message) || (!PG(ignore_repeated_source) - && ((PG(last_error_lineno) != error_lineno) - || !zend_string_equals(PG(last_error_file), error_filename)))) { + && ((PG(repeated_error_linenos)[repeated_error_index] != (int)error_lineno) + || !zend_string_equals(PG(repeated_error_files)[repeated_error_index], repeated_error_file)))) { display = 1; } else { display = 0; @@ -1374,7 +1388,7 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c /* store the error if it has changed */ if (display) { - clear_last_error(); + clear_last_error(false); if (!error_filename) { error_filename = ZSTR_KNOWN(ZEND_STR_UNKNOWN_CAPITALIZED); } @@ -1384,6 +1398,17 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c PG(last_error_lineno) = error_lineno; } + if (!PG(repeated_error_messages)[repeated_error_index] + || display) { + if (PG(repeated_error_messages)[repeated_error_index]) { + zend_string_release(PG(repeated_error_messages)[repeated_error_index]); + zend_string_release(PG(repeated_error_files)[repeated_error_index]); + } + PG(repeated_error_linenos)[repeated_error_index] = error_lineno; + PG(repeated_error_messages)[repeated_error_index] = zend_string_copy(message); + PG(repeated_error_files)[repeated_error_index] = zend_string_copy(repeated_error_file); + } + /* Discarding the output buffers frees every handler on the stack, which is * not safe while one of them is running */ if (zend_alloc_in_memory_limit_error_reporting() @@ -1708,7 +1733,7 @@ static zval *php_get_configuration_directive_for_zend(zend_string *name) /* {{{ php_free_request_globals */ static void php_free_request_globals(void) { - clear_last_error(); + clear_last_error(true); if (PG(php_sys_temp_dir)) { efree(PG(php_sys_temp_dir)); PG(php_sys_temp_dir) = NULL; @@ -2039,7 +2064,7 @@ void php_request_shutdown(void *dummy) /* 16. Free Willy (here be crashes) */ zend_arena_destroy(CG(arena)); zend_interned_strings_deactivate(); - clear_last_error(); + clear_last_error(true); zend_try { shutdown_memory_manager(CG(unclean_shutdown) || !report_memleaks, 0); } zend_end_try(); @@ -2456,7 +2481,7 @@ zend_result php_module_startup(sapi_module_struct *sf, zend_module_entry *additi module_startup = false; /* Don't leak errors from startup into the per-request phase. */ - clear_last_error(); + clear_last_error(true); shutdown_memory_manager(1, 0); virtual_cwd_activate(); @@ -2519,7 +2544,7 @@ void php_module_shutdown(void) /* close down the ini config */ php_shutdown_config(); - clear_last_error(); + clear_last_error(true); #ifndef ZTS zend_ini_shutdown(); diff --git a/main/php_globals.h b/main/php_globals.h index e3cfe38a63fd..c45000494d2b 100644 --- a/main/php_globals.h +++ b/main/php_globals.h @@ -139,6 +139,9 @@ struct _php_core_globals { uint32_t last_error_lineno; zend_string *last_error_message; zend_string *last_error_file; + int repeated_error_linenos[32]; + zend_string *repeated_error_messages[32]; + zend_string *repeated_error_files[32]; char *php_sys_temp_dir; From e2cb0af441906b3aca2b7f1ef083a745eb0681c6 Mon Sep 17 00:00:00 2001 From: Alessio Attilio Date: Mon, 2 Nov 2026 22:07:23 +0100 Subject: [PATCH 2/5] Fix repeated errors after error_clear_last --- .../ignore_repeated_errors_clear_last.phpt | 29 +++++++++++++++++++ ext/standard/basic_functions.c | 15 ++-------- main/main.c | 21 ++++++++------ main/php_main.h | 1 + 4 files changed, 45 insertions(+), 21 deletions(-) create mode 100644 Zend/tests/ignore_repeated_errors_clear_last.phpt diff --git a/Zend/tests/ignore_repeated_errors_clear_last.phpt b/Zend/tests/ignore_repeated_errors_clear_last.phpt new file mode 100644 index 000000000000..13112976351f --- /dev/null +++ b/Zend/tests/ignore_repeated_errors_clear_last.phpt @@ -0,0 +1,29 @@ +--TEST-- +Test ignore_repeated_errors with error_clear_last() and error_get_last() +--INI-- +ignore_repeated_errors=1 +error_reporting=-1 +html_errors=0 +--FILE-- + +--EXPECTF-- +Warning: A in %s on line %d + +Warning: A in %s on line %d +string(1) "A" +string(1) "A" diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index db302ab724ab..3d6bf8eabfb9 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -1471,18 +1471,9 @@ PHP_FUNCTION(error_clear_last) { ZEND_PARSE_PARAMETERS_NONE(); - if (PG(last_error_message)) { - PG(last_error_type) = 0; - PG(last_error_lineno) = 0; - - zend_string_release(PG(last_error_message)); - PG(last_error_message) = NULL; - - if (PG(last_error_file)) { - zend_string_release(PG(last_error_file)); - PG(last_error_file) = NULL; - } - } + PG(last_error_type) = 0; + PG(last_error_lineno) = 0; + php_clear_last_error(true); zval_ptr_dtor(&EG(last_fatal_error_backtrace)); ZVAL_UNDEF(&EG(last_fatal_error_backtrace)); diff --git a/main/main.c b/main/main.c index df8c49e8e204..842231eac71b 100644 --- a/main/main.c +++ b/main/main.c @@ -1286,7 +1286,7 @@ PHPAPI void php_html_puts(const char *str, size_t size) } /* }}} */ -static void clear_last_error(bool clear_repeated_errors) { +PHPAPI void php_clear_last_error(bool clear_repeated_errors) { if (PG(last_error_message)) { zend_string_release(PG(last_error_message)); PG(last_error_message) = NULL; @@ -1386,11 +1386,14 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c backtrace = zend_trace_to_string(Z_ARRVAL(EG(last_fatal_error_backtrace)), /* include_main */ true); } - /* store the error if it has changed */ - if (display) { - clear_last_error(false); + /* Keep error_get_last() up to date even when this occurrence is suppressed. */ + if (display || !PG(last_error_message) + || !zend_string_equals(PG(last_error_message), message) + || PG(last_error_lineno) != error_lineno + || !zend_string_equals(PG(last_error_file), repeated_error_file)) { + php_clear_last_error(false); if (!error_filename) { - error_filename = ZSTR_KNOWN(ZEND_STR_UNKNOWN_CAPITALIZED); + error_filename = repeated_error_file; } PG(last_error_type) = type; PG(last_error_message) = zend_string_copy(message); @@ -1733,7 +1736,7 @@ static zval *php_get_configuration_directive_for_zend(zend_string *name) /* {{{ php_free_request_globals */ static void php_free_request_globals(void) { - clear_last_error(true); + php_clear_last_error(true); if (PG(php_sys_temp_dir)) { efree(PG(php_sys_temp_dir)); PG(php_sys_temp_dir) = NULL; @@ -2064,7 +2067,7 @@ void php_request_shutdown(void *dummy) /* 16. Free Willy (here be crashes) */ zend_arena_destroy(CG(arena)); zend_interned_strings_deactivate(); - clear_last_error(true); + php_clear_last_error(true); zend_try { shutdown_memory_manager(CG(unclean_shutdown) || !report_memleaks, 0); } zend_end_try(); @@ -2481,7 +2484,7 @@ zend_result php_module_startup(sapi_module_struct *sf, zend_module_entry *additi module_startup = false; /* Don't leak errors from startup into the per-request phase. */ - clear_last_error(true); + php_clear_last_error(true); shutdown_memory_manager(1, 0); virtual_cwd_activate(); @@ -2544,7 +2547,7 @@ void php_module_shutdown(void) /* close down the ini config */ php_shutdown_config(); - clear_last_error(true); + php_clear_last_error(true); #ifndef ZTS zend_ini_shutdown(); diff --git a/main/php_main.h b/main/php_main.h index de2ace1c25ed..537cfd3bb701 100644 --- a/main/php_main.h +++ b/main/php_main.h @@ -48,6 +48,7 @@ PHPAPI char *php_get_version(sapi_module_struct *sapi_module); PHPAPI void php_print_version(sapi_module_struct *sapi_module); PHPAPI void php_child_init(void); +PHPAPI void php_clear_last_error(bool clear_repeated_errors); PHPAPI zend_result php_request_startup(void); PHPAPI void php_request_shutdown(void *dummy); PHPAPI zend_result php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_module); From 82114593882b6da4c1c8e9e7e74cd9caf26b020c Mon Sep 17 00:00:00 2001 From: Alessio Attilio Date: Wed, 11 Nov 2026 18:40:34 +0100 Subject: [PATCH 3/5] Track only the last reported repeated error --- .../ignore_repeated_errors_clear_last.phpt | 14 +++++++ main/main.c | 41 ++++++++----------- main/php_globals.h | 6 +-- 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/Zend/tests/ignore_repeated_errors_clear_last.phpt b/Zend/tests/ignore_repeated_errors_clear_last.phpt index 13112976351f..6087b2cc22d1 100644 --- a/Zend/tests/ignore_repeated_errors_clear_last.phpt +++ b/Zend/tests/ignore_repeated_errors_clear_last.phpt @@ -9,6 +9,9 @@ html_errors=0 function warningA() { trigger_error('A', E_USER_WARNING); } +function noticeN() { + trigger_error('N', E_USER_NOTICE); +} warningA(); error_clear_last(); @@ -20,6 +23,11 @@ warningA(); trigger_error('D', E_USER_DEPRECATED); warningA(); var_dump(error_get_last()['message']); + +error_clear_last(); +warningA(); +noticeN(); +warningA(); ?> --EXPECTF-- Warning: A in %s on line %d @@ -27,3 +35,9 @@ Warning: A in %s on line %d Warning: A in %s on line %d string(1) "A" string(1) "A" + +Warning: A in %s on line %d + +Notice: N in %s on line %d + +Warning: A in %s on line %d diff --git a/main/main.c b/main/main.c index 842231eac71b..e81cc9825313 100644 --- a/main/main.c +++ b/main/main.c @@ -1296,13 +1296,11 @@ PHPAPI void php_clear_last_error(bool clear_repeated_errors) { PG(last_error_file) = NULL; } if (clear_repeated_errors) { - for (uint32_t i = 0; i < 32; i++) { - if (PG(repeated_error_messages)[i]) { - zend_string_release(PG(repeated_error_messages)[i]); - PG(repeated_error_messages)[i] = NULL; - zend_string_release(PG(repeated_error_files)[i]); - PG(repeated_error_files)[i] = NULL; - } + if (PG(repeated_error_message)) { + zend_string_release(PG(repeated_error_message)); + PG(repeated_error_message) = NULL; + zend_string_release(PG(repeated_error_file)); + PG(repeated_error_file) = NULL; } } } @@ -1338,20 +1336,15 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c { bool display; int type = orig_type & E_ALL; - uint32_t repeated_error_index = 0; zend_string *backtrace = ZSTR_EMPTY_ALLOC(); zend_string *repeated_error_file = error_filename ? error_filename : ZSTR_KNOWN(ZEND_STR_UNKNOWN_CAPITALIZED); - while (repeated_error_index < 31 && !(type & (1U << repeated_error_index))) { - repeated_error_index++; - } - /* check for repeated errors to be ignored */ - if (PG(ignore_repeated_errors) && PG(repeated_error_messages)[repeated_error_index]) { - if (!zend_string_equals(PG(repeated_error_messages)[repeated_error_index], message) + if (PG(ignore_repeated_errors) && PG(repeated_error_message)) { + if (!zend_string_equals(PG(repeated_error_message), message) || (!PG(ignore_repeated_source) - && ((PG(repeated_error_linenos)[repeated_error_index] != (int)error_lineno) - || !zend_string_equals(PG(repeated_error_files)[repeated_error_index], repeated_error_file)))) { + && ((PG(repeated_error_lineno) != error_lineno) + || !zend_string_equals(PG(repeated_error_file), repeated_error_file)))) { display = 1; } else { display = 0; @@ -1401,15 +1394,15 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c PG(last_error_lineno) = error_lineno; } - if (!PG(repeated_error_messages)[repeated_error_index] - || display) { - if (PG(repeated_error_messages)[repeated_error_index]) { - zend_string_release(PG(repeated_error_messages)[repeated_error_index]); - zend_string_release(PG(repeated_error_files)[repeated_error_index]); + if ((!PG(ignore_repeated_errors) || display) + && ((EG(error_reporting) & type) || (type & E_CORE))) { + if (PG(repeated_error_message)) { + zend_string_release(PG(repeated_error_message)); + zend_string_release(PG(repeated_error_file)); } - PG(repeated_error_linenos)[repeated_error_index] = error_lineno; - PG(repeated_error_messages)[repeated_error_index] = zend_string_copy(message); - PG(repeated_error_files)[repeated_error_index] = zend_string_copy(repeated_error_file); + PG(repeated_error_lineno) = error_lineno; + PG(repeated_error_message) = zend_string_copy(message); + PG(repeated_error_file) = zend_string_copy(repeated_error_file); } /* Discarding the output buffers frees every handler on the stack, which is diff --git a/main/php_globals.h b/main/php_globals.h index c45000494d2b..7728b5f8c086 100644 --- a/main/php_globals.h +++ b/main/php_globals.h @@ -139,9 +139,9 @@ struct _php_core_globals { uint32_t last_error_lineno; zend_string *last_error_message; zend_string *last_error_file; - int repeated_error_linenos[32]; - zend_string *repeated_error_messages[32]; - zend_string *repeated_error_files[32]; + uint32_t repeated_error_lineno; + zend_string *repeated_error_message; + zend_string *repeated_error_file; char *php_sys_temp_dir; From ff1249babd3f1cc08a57f9e0f1783e7f4355f11e Mon Sep 17 00:00:00 2001 From: Alessio Attilio Date: Wed, 11 Nov 2026 18:54:14 +0100 Subject: [PATCH 4/5] Update interleaved error expectation --- Zend/tests/ignore_repeated_errors_interleaved.phpt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Zend/tests/ignore_repeated_errors_interleaved.phpt b/Zend/tests/ignore_repeated_errors_interleaved.phpt index 7dd764aa1871..2d6ab26d92b2 100644 --- a/Zend/tests/ignore_repeated_errors_interleaved.phpt +++ b/Zend/tests/ignore_repeated_errors_interleaved.phpt @@ -13,3 +13,11 @@ for ($i = 0; $i < 3; $i++) { Warning: Undefined variable $missing in %s on line %d Deprecated: trim(): Passing null to parameter #1 ($string) of type string is deprecated in %s on line %d + +Warning: Undefined variable $missing in %s on line %d + +Deprecated: trim(): Passing null to parameter #1 ($string) of type string is deprecated in %s on line %d + +Warning: Undefined variable $missing in %s on line %d + +Deprecated: trim(): Passing null to parameter #1 ($string) of type string is deprecated in %s on line %d From b2742cbdf0935f06031f6e27bdef7c428bfc038a Mon Sep 17 00:00:00 2001 From: Alessio Attilio Date: Thu, 12 Nov 2026 20:34:26 +0100 Subject: [PATCH 5/5] Fix last-error source comparison for suppressed repeats Honor ignore_repeated_source when deciding whether a suppressed repeat replaces error_get_last(), matching the repeat-suppression comparison. Keep restoring the last error after an intervening filtered diagnostic. Add coverage for different lines and files, filtered errors, error_clear_last(), and enabling source comparison again. Fixes the regression reported in GH-24757 while addressing GH-19509. --- .../ignore_repeated_source_last_error.phpt | 54 +++++++++++++++++++ main/main.c | 8 +-- 2 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 Zend/tests/ignore_repeated_source_last_error.phpt diff --git a/Zend/tests/ignore_repeated_source_last_error.phpt b/Zend/tests/ignore_repeated_source_last_error.phpt new file mode 100644 index 000000000000..df4837809b11 --- /dev/null +++ b/Zend/tests/ignore_repeated_source_last_error.phpt @@ -0,0 +1,54 @@ +--TEST-- +Repeated errors preserve error_get_last() when ignoring their source +--INI-- +ignore_repeated_errors=1 +ignore_repeated_source=1 +error_reporting=-1 +html_errors=0 +--FILE-- + +--EXPECTF-- +Warning: S in %s on line %d +bool(true) +bool(true) +string(1) "D" +bool(true) +string(1) "S" +bool(true) +NULL + +Notice: S in %s on line %d +bool(true) + +Warning: S in %s on line %d +bool(true) diff --git a/main/main.c b/main/main.c index e81cc9825313..33fbf0870878 100644 --- a/main/main.c +++ b/main/main.c @@ -1379,11 +1379,13 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c backtrace = zend_trace_to_string(Z_ARRVAL(EG(last_fatal_error_backtrace)), /* include_main */ true); } - /* Keep error_get_last() up to date even when this occurrence is suppressed. */ + /* A suppressed repeat may replace an intervening filtered error, but must + * use the same source comparison as repeat suppression. */ if (display || !PG(last_error_message) || !zend_string_equals(PG(last_error_message), message) - || PG(last_error_lineno) != error_lineno - || !zend_string_equals(PG(last_error_file), repeated_error_file)) { + || (!PG(ignore_repeated_source) + && (PG(last_error_lineno) != error_lineno + || !zend_string_equals(PG(last_error_file), repeated_error_file)))) { php_clear_last_error(false); if (!error_filename) { error_filename = repeated_error_file;