diff --git a/NEWS b/NEWS index b6d27bb15aa0b..1813c24dcd393 100644 --- a/NEWS +++ b/NEWS @@ -16,6 +16,7 @@ PHP NEWS if available. (timwolla) . Implement GH-15680 (Enhance zend_dump_op_array to properly represent non-printable characters in string literals). (nielsdos, WangYihang) + . Add support for backtraces for fatal errors. (enorris) - Curl: . Added curl_multi_get_handles(). (timwolla) diff --git a/UPGRADING b/UPGRADING index 3d0c1101ed5ad..65db4791a2ae5 100644 --- a/UPGRADING +++ b/UPGRADING @@ -68,6 +68,9 @@ PHP 8.5 UPGRADE NOTES . Closure is now a proper subtype of callable . Added support for Closures in constant expressions. RFC: https://wiki.php.net/rfc/closures_in_const_expr + . Fatal Errors (such as an exceeded maximum execution time) now include a + backtrace. + RFC: https://wiki.php.net/rfc/error_backtraces_v2 - Curl: . Added support for share handles that are persisted across multiple PHP @@ -243,6 +246,11 @@ PHP 8.5 UPGRADE NOTES 11. Changes to INI File Handling ======================================== +- Core: + . Added fatal_error_backtraces to control whether fatal errors should include + a backtrace. + RFC: https://wiki.php.net/rfc/error_backtraces_v2 + - Opcache: . Added opcache.file_cache_read_only to support a read-only opcache.file_cache directory, for use with read-only file systems diff --git a/Zend/tests/fatal_error_backtraces_001.phpt b/Zend/tests/fatal_error_backtraces_001.phpt new file mode 100644 index 0000000000000..0a7d1d8f1c683 --- /dev/null +++ b/Zend/tests/fatal_error_backtraces_001.phpt @@ -0,0 +1,14 @@ +--TEST-- +Fatal error backtrace +--INI-- +fatal_error_backtraces=On +--FILE-- + +--EXPECTF-- +Fatal error: Cannot redeclare class Foo (%s) in %s : eval()'d code on line %d +Stack trace: +#0 %sfatal_error_backtraces_001.php(%d): eval() +#1 {main} diff --git a/Zend/tests/fatal_error_backtraces_002.phpt b/Zend/tests/fatal_error_backtraces_002.phpt new file mode 100644 index 0000000000000..c72505cb18132 --- /dev/null +++ b/Zend/tests/fatal_error_backtraces_002.phpt @@ -0,0 +1,19 @@ +--TEST-- +Fatal error backtrace w/ sensitive parameters +--INI-- +fatal_error_backtraces=On +--FILE-- + +--EXPECTF-- +Fatal error: Cannot redeclare class Foo (%s) in %s : eval()'d code on line %d +Stack trace: +#0 %sfatal_error_backtraces_002.php(%d): eval() +#1 %sfatal_error_backtraces_002.php(%d): trigger_fatal(Object(SensitiveParameterValue)) +#2 {main} diff --git a/Zend/tests/fatal_error_backtraces_003.phpt b/Zend/tests/fatal_error_backtraces_003.phpt new file mode 100644 index 0000000000000..ae6cf8ce37f9c --- /dev/null +++ b/Zend/tests/fatal_error_backtraces_003.phpt @@ -0,0 +1,20 @@ +--TEST-- +Fatal error backtrace w/ zend.exception_ignore_args +--INI-- +fatal_error_backtraces=On +zend.exception_ignore_args=On +--FILE-- + +--EXPECTF-- +Fatal error: Cannot redeclare class Foo (%s) in %s : eval()'d code on line %d +Stack trace: +#0 %sfatal_error_backtraces_003.php(%d): eval() +#1 %sfatal_error_backtraces_003.php(%d): trigger_fatal() +#2 {main} diff --git a/Zend/tests/new_oom.phpt b/Zend/tests/new_oom.phpt index a424e12b4eab9..6d4ba3d760b40 100644 --- a/Zend/tests/new_oom.phpt +++ b/Zend/tests/new_oom.phpt @@ -13,7 +13,7 @@ $php = PHP_BINARY; foreach (get_declared_classes() as $class) { $output = shell_exec("$php --no-php-ini $file $class 2>&1"); - if ($output && preg_match('(^\nFatal error: Allowed memory size of [0-9]+ bytes exhausted[^\r\n]* \(tried to allocate [0-9]+ bytes\) in [^\r\n]+ on line [0-9]+$)', $output) !== 1) { + if ($output && preg_match('(^\nFatal error: Allowed memory size of [0-9]+ bytes exhausted[^\r\n]* \(tried to allocate [0-9]+ bytes\) in [^\r\n]+ on line [0-9]+\nStack trace:\n(#[0-9]+ [^\r\n]+\n)+$)', $output) !== 1) { echo "Class $class failed\n"; echo $output, "\n"; } diff --git a/Zend/zend.c b/Zend/zend.c index b4a084b1f95c7..60eab332fa158 100644 --- a/Zend/zend.c +++ b/Zend/zend.c @@ -260,6 +260,7 @@ static ZEND_INI_MH(OnUpdateFiberStackSize) /* {{{ */ ZEND_INI_BEGIN() ZEND_INI_ENTRY("error_reporting", NULL, ZEND_INI_ALL, OnUpdateErrorReporting) + STD_ZEND_INI_BOOLEAN("fatal_error_backtraces", "1", ZEND_INI_ALL, OnUpdateBool, fatal_error_backtrace_on, zend_executor_globals, executor_globals) STD_ZEND_INI_ENTRY("zend.assertions", "1", ZEND_INI_ALL, OnUpdateAssertions, assertions, zend_executor_globals, executor_globals) ZEND_INI_ENTRY3_EX("zend.enable_gc", "1", ZEND_INI_ALL, OnUpdateGCEnabled, NULL, NULL, NULL, zend_gc_enabled_displayer_cb) STD_ZEND_INI_BOOLEAN("zend.multibyte", "0", ZEND_INI_PERDIR, OnUpdateBool, multibyte, zend_compiler_globals, compiler_globals) @@ -1463,6 +1464,10 @@ ZEND_API ZEND_COLD void zend_error_zstr_at( EG(errors)[EG(num_errors)-1] = info; } + // Always clear the last backtrace. + zval_ptr_dtor(&EG(last_fatal_error_backtrace)); + ZVAL_UNDEF(&EG(last_fatal_error_backtrace)); + /* Report about uncaught exception in case of fatal errors */ if (EG(exception)) { zend_execute_data *ex; @@ -1484,6 +1489,8 @@ ZEND_API ZEND_COLD void zend_error_zstr_at( ex->opline = opline; } } + } else if (EG(fatal_error_backtrace_on) && (type & E_FATAL_ERRORS)) { + zend_fetch_debug_backtrace(&EG(last_fatal_error_backtrace), 0, EG(exception_ignore_args) ? DEBUG_BACKTRACE_IGNORE_ARGS : 0, 0); } zend_observer_error_notify(type, error_filename, error_lineno, message); diff --git a/Zend/zend_exceptions.c b/Zend/zend_exceptions.c index ac29dc581e6ba..f9d0ae8ea8173 100644 --- a/Zend/zend_exceptions.c +++ b/Zend/zend_exceptions.c @@ -903,6 +903,10 @@ ZEND_API ZEND_COLD zend_result zend_exception_error(zend_object *ex, int severit ZVAL_OBJ(&exception, ex); ce_exception = ex->ce; EG(exception) = NULL; + + zval_ptr_dtor(&EG(last_fatal_error_backtrace)); + ZVAL_UNDEF(&EG(last_fatal_error_backtrace)); + if (ce_exception == zend_ce_parse_error || ce_exception == zend_ce_compile_error) { zend_string *message = zval_get_string(GET_PROPERTY(&exception, ZEND_STR_MESSAGE)); zend_string *file = zval_get_string(GET_PROPERTY_SILENT(&exception, ZEND_STR_FILE)); diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index 8b3d2618112ba..07e3d95bd60fe 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -140,6 +140,8 @@ void init_executor(void) /* {{{ */ original_sigsegv_handler = signal(SIGSEGV, zend_handle_sigsegv); #endif + ZVAL_UNDEF(&EG(last_fatal_error_backtrace)); + EG(symtable_cache_ptr) = EG(symtable_cache); EG(symtable_cache_limit) = EG(symtable_cache) + SYMTABLE_CACHE_SIZE; EG(no_extensions) = 0; @@ -307,6 +309,9 @@ ZEND_API void zend_shutdown_executor_values(bool fast_shutdown) } ZEND_HASH_MAP_FOREACH_END_DEL(); } + zval_ptr_dtor(&EG(last_fatal_error_backtrace)); + ZVAL_UNDEF(&EG(last_fatal_error_backtrace)); + /* Release static properties and static variables prior to the final GC run, * as they may hold GC roots. */ ZEND_HASH_MAP_REVERSE_FOREACH_VAL(EG(function_table), zv) { diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h index 62a97d753634a..079bfb99caccf 100644 --- a/Zend/zend_globals.h +++ b/Zend/zend_globals.h @@ -182,6 +182,10 @@ struct _zend_executor_globals { JMP_BUF *bailout; int error_reporting; + + bool fatal_error_backtrace_on; + zval last_fatal_error_backtrace; + int exit_status; HashTable *function_table; /* function symbol table */ diff --git a/ext/opcache/tests/gh8846.phpt b/ext/opcache/tests/gh8846.phpt index 98e94a401cb7a..1e9dd68f191cf 100644 --- a/ext/opcache/tests/gh8846.phpt +++ b/ext/opcache/tests/gh8846.phpt @@ -34,6 +34,9 @@ echo file_get_contents('http://' . PHP_CLI_SERVER_ADDRESS . '/gh8846-index.php?s bool(true)
Fatal error: Cannot redeclare class Foo (previously declared in %sgh8846-1.inc:2) in %sgh8846-2.inc on line %d
+Stack trace: +#0 %sgh8846-index.php(%d): include() +#1 {main} bool(true) Ok diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index 4d79d9df8532f..4f9ed736b7134 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -1436,6 +1436,11 @@ PHP_FUNCTION(error_get_last) ZVAL_LONG(&tmp, PG(last_error_lineno)); zend_hash_update(Z_ARR_P(return_value), ZSTR_KNOWN(ZEND_STR_LINE), &tmp); + + if (!Z_ISUNDEF(EG(last_fatal_error_backtrace))) { + ZVAL_COPY(&tmp, &EG(last_fatal_error_backtrace)); + zend_hash_update(Z_ARR_P(return_value), ZSTR_KNOWN(ZEND_STR_TRACE), &tmp); + } } } /* }}} */ @@ -1457,6 +1462,9 @@ PHP_FUNCTION(error_clear_last) PG(last_error_file) = NULL; } } + + zval_ptr_dtor(&EG(last_fatal_error_backtrace)); + ZVAL_UNDEF(&EG(last_fatal_error_backtrace)); } /* }}} */ diff --git a/ext/standard/tests/general_functions/error_get_last_002.phpt b/ext/standard/tests/general_functions/error_get_last_002.phpt new file mode 100644 index 0000000000000..45013ba68d66a --- /dev/null +++ b/ext/standard/tests/general_functions/error_get_last_002.phpt @@ -0,0 +1,59 @@ +--TEST-- +error_get_last() w/ fatal error +--INI-- +fatal_error_backtraces=On +--FILE-- + +--EXPECTF-- +Fatal error: Cannot redeclare class Foo (%s) in %s on line %d +Stack trace: +#0 %serror_get_last_002.php(%d): eval() +#1 %serror_get_last_002.php(%d): trigger_fatal_error_with_stacktrace() +#2 {main} +array(5) { + ["type"]=> + int(64) + ["message"]=> + string(%d) "Cannot redeclare class Foo %s" + ["file"]=> + string(%d) "%serror_get_last_002.php(%d) : eval()'d code" + ["line"]=> + int(%d) + ["trace"]=> + array(2) { + [0]=> + array(3) { + ["file"]=> + string(%d) "%serror_get_last_002.php" + ["line"]=> + int(%d) + ["function"]=> + string(%d) "eval" + } + [1]=> + array(4) { + ["file"]=> + string(%d) "%serror_get_last_002.php" + ["line"]=> + int(%d) + ["function"]=> + string(%d) "trigger_fatal_error_with_stacktrace" + ["args"]=> + array(0) { + } + } + } +} +Done diff --git a/main/main.c b/main/main.c index 5e95df9ac03bb..4075be8b377e3 100644 --- a/main/main.c +++ b/main/main.c @@ -1282,6 +1282,7 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c { bool display; int type = orig_type & E_ALL; + zend_string *backtrace = ZSTR_EMPTY_ALLOC(); /* check for repeated errors to be ignored */ if (PG(ignore_repeated_errors) && PG(last_error_message)) { @@ -1321,6 +1322,10 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c } } + if (!Z_ISUNDEF(EG(last_fatal_error_backtrace))) { + 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(); @@ -1389,14 +1394,14 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c syslog(LOG_ALERT, "PHP %s: %s (%s)", error_type_str, ZSTR_VAL(message), GetCommandLine()); } #endif - spprintf(&log_buffer, 0, "PHP %s: %s in %s on line %" PRIu32, error_type_str, ZSTR_VAL(message), ZSTR_VAL(error_filename), error_lineno); + spprintf(&log_buffer, 0, "PHP %s: %s in %s on line %" PRIu32 "%s%s", error_type_str, ZSTR_VAL(message), ZSTR_VAL(error_filename), error_lineno, ZSTR_LEN(backtrace) ? "\nStack trace:\n" : "", ZSTR_VAL(backtrace)); php_log_err_with_severity(log_buffer, syslog_type_int); efree(log_buffer); } if (PG(display_errors) && ((module_initialized && !PG(during_request_startup)) || (PG(display_startup_errors)))) { if (PG(xmlrpc_errors)) { - php_printf("faultCode" ZEND_LONG_FMT "faultString%s:%s in %s on line %" PRIu32 "", PG(xmlrpc_error_number), error_type_str, ZSTR_VAL(message), ZSTR_VAL(error_filename), error_lineno); + php_printf("faultCode" ZEND_LONG_FMT "faultString%s:%s in %s on line %" PRIu32 "%s%s", PG(xmlrpc_error_number), error_type_str, ZSTR_VAL(message), ZSTR_VAL(error_filename), error_lineno, ZSTR_LEN(backtrace) ? "\nStack trace:\n" : "", ZSTR_VAL(backtrace)); } else { char *prepend_string = INI_STR("error_prepend_string"); char *append_string = INI_STR("error_append_string"); @@ -1407,7 +1412,7 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c php_printf("%s
\n%s: %s in %s on line %" PRIu32 "
\n%s", STR_PRINT(prepend_string), error_type_str, ZSTR_VAL(buf), ZSTR_VAL(error_filename), error_lineno, STR_PRINT(append_string)); zend_string_free(buf); } else { - php_printf_unchecked("%s
\n%s: %S in %s on line %" PRIu32 "
\n%s", STR_PRINT(prepend_string), error_type_str, message, ZSTR_VAL(error_filename), error_lineno, STR_PRINT(append_string)); + php_printf_unchecked("%s
\n%s: %S in %s on line %" PRIu32 "
%s%s\n%s", STR_PRINT(prepend_string), error_type_str, message, ZSTR_VAL(error_filename), error_lineno, ZSTR_LEN(backtrace) ? "\nStack trace:\n" : "", ZSTR_VAL(backtrace), STR_PRINT(append_string)); } } else { /* Write CLI/CGI errors to stderr if display_errors = "stderr" */ @@ -1416,18 +1421,20 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c ) { fprintf(stderr, "%s: ", error_type_str); fwrite(ZSTR_VAL(message), sizeof(char), ZSTR_LEN(message), stderr); - fprintf(stderr, " in %s on line %" PRIu32 "\n", ZSTR_VAL(error_filename), error_lineno); + fprintf(stderr, " in %s on line %" PRIu32 "%s%s\n", ZSTR_VAL(error_filename), error_lineno, ZSTR_LEN(backtrace) ? "\nStack trace:\n" : "", ZSTR_VAL(backtrace)); #ifdef PHP_WIN32 fflush(stderr); #endif } else { - php_printf_unchecked("%s\n%s: %S in %s on line %" PRIu32 "\n%s", STR_PRINT(prepend_string), error_type_str, message, ZSTR_VAL(error_filename), error_lineno, STR_PRINT(append_string)); + php_printf_unchecked("%s\n%s: %S in %s on line %" PRIu32 "%s%s\n%s", STR_PRINT(prepend_string), error_type_str, message, ZSTR_VAL(error_filename), error_lineno, ZSTR_LEN(backtrace) ? "\nStack trace:\n" : "", ZSTR_VAL(backtrace), STR_PRINT(append_string)); } } } } } + zend_string_release(backtrace); + /* Bail out if we can't recover */ switch (type) { case E_CORE_ERROR: diff --git a/run-tests.php b/run-tests.php index 0ff11c2c95db7..ce658eec1311a 100755 --- a/run-tests.php +++ b/run-tests.php @@ -272,6 +272,7 @@ function main(): void 'disable_functions=', 'output_buffering=Off', 'error_reporting=' . E_ALL, + 'fatal_error_backtraces=Off', 'display_errors=1', 'display_startup_errors=1', 'log_errors=0',