Skip to content

Commit 0a14ab1

Browse files
ericnorrisnielsdos
andauthored
RFC: Error Backtraces v2 (#17056)
see https://wiki.php.net/rfc/error_backtraces_v2 Co-authored-by: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
1 parent 5bf6e2e commit 0a14ab1

15 files changed

+166
-6
lines changed

NEWS

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ PHP NEWS
1616
if available. (timwolla)
1717
. Implement GH-15680 (Enhance zend_dump_op_array to properly represent
1818
non-printable characters in string literals). (nielsdos, WangYihang)
19+
. Add support for backtraces for fatal errors. (enorris)
1920

2021
- Curl:
2122
. Added curl_multi_get_handles(). (timwolla)

UPGRADING

+8
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ PHP 8.5 UPGRADE NOTES
6868
. Closure is now a proper subtype of callable
6969
. Added support for Closures in constant expressions.
7070
RFC: https://wiki.php.net/rfc/closures_in_const_expr
71+
. Fatal Errors (such as an exceeded maximum execution time) now include a
72+
backtrace.
73+
RFC: https://wiki.php.net/rfc/error_backtraces_v2
7174

7275
- Curl:
7376
. Added support for share handles that are persisted across multiple PHP
@@ -243,6 +246,11 @@ PHP 8.5 UPGRADE NOTES
243246
11. Changes to INI File Handling
244247
========================================
245248

249+
- Core:
250+
. Added fatal_error_backtraces to control whether fatal errors should include
251+
a backtrace.
252+
RFC: https://wiki.php.net/rfc/error_backtraces_v2
253+
246254
- Opcache:
247255
. Added opcache.file_cache_read_only to support a read-only
248256
opcache.file_cache directory, for use with read-only file systems
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
--TEST--
2+
Fatal error backtrace
3+
--INI--
4+
fatal_error_backtraces=On
5+
--FILE--
6+
<?php
7+
8+
eval("class Foo {}; class Foo {}");
9+
?>
10+
--EXPECTF--
11+
Fatal error: Cannot redeclare class Foo (%s) in %s : eval()'d code on line %d
12+
Stack trace:
13+
#0 %sfatal_error_backtraces_001.php(%d): eval()
14+
#1 {main}
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
--TEST--
2+
Fatal error backtrace w/ sensitive parameters
3+
--INI--
4+
fatal_error_backtraces=On
5+
--FILE--
6+
<?php
7+
8+
function trigger_fatal(#[\SensitiveParameter] $unused) {
9+
eval("class Foo {}; class Foo {}");
10+
}
11+
12+
trigger_fatal("bar");
13+
?>
14+
--EXPECTF--
15+
Fatal error: Cannot redeclare class Foo (%s) in %s : eval()'d code on line %d
16+
Stack trace:
17+
#0 %sfatal_error_backtraces_002.php(%d): eval()
18+
#1 %sfatal_error_backtraces_002.php(%d): trigger_fatal(Object(SensitiveParameterValue))
19+
#2 {main}
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
--TEST--
2+
Fatal error backtrace w/ zend.exception_ignore_args
3+
--INI--
4+
fatal_error_backtraces=On
5+
zend.exception_ignore_args=On
6+
--FILE--
7+
<?php
8+
9+
function trigger_fatal($unused) {
10+
eval("class Foo {}; class Foo {}");
11+
}
12+
13+
trigger_fatal("bar");
14+
?>
15+
--EXPECTF--
16+
Fatal error: Cannot redeclare class Foo (%s) in %s : eval()'d code on line %d
17+
Stack trace:
18+
#0 %sfatal_error_backtraces_003.php(%d): eval()
19+
#1 %sfatal_error_backtraces_003.php(%d): trigger_fatal()
20+
#2 {main}

Zend/tests/new_oom.phpt

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ $php = PHP_BINARY;
1313

1414
foreach (get_declared_classes() as $class) {
1515
$output = shell_exec("$php --no-php-ini $file $class 2>&1");
16-
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) {
16+
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) {
1717
echo "Class $class failed\n";
1818
echo $output, "\n";
1919
}

Zend/zend.c

+7
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ static ZEND_INI_MH(OnUpdateFiberStackSize) /* {{{ */
260260

261261
ZEND_INI_BEGIN()
262262
ZEND_INI_ENTRY("error_reporting", NULL, ZEND_INI_ALL, OnUpdateErrorReporting)
263+
STD_ZEND_INI_BOOLEAN("fatal_error_backtraces", "1", ZEND_INI_ALL, OnUpdateBool, fatal_error_backtrace_on, zend_executor_globals, executor_globals)
263264
STD_ZEND_INI_ENTRY("zend.assertions", "1", ZEND_INI_ALL, OnUpdateAssertions, assertions, zend_executor_globals, executor_globals)
264265
ZEND_INI_ENTRY3_EX("zend.enable_gc", "1", ZEND_INI_ALL, OnUpdateGCEnabled, NULL, NULL, NULL, zend_gc_enabled_displayer_cb)
265266
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(
14631464
EG(errors)[EG(num_errors)-1] = info;
14641465
}
14651466

1467+
// Always clear the last backtrace.
1468+
zval_ptr_dtor(&EG(last_fatal_error_backtrace));
1469+
ZVAL_UNDEF(&EG(last_fatal_error_backtrace));
1470+
14661471
/* Report about uncaught exception in case of fatal errors */
14671472
if (EG(exception)) {
14681473
zend_execute_data *ex;
@@ -1484,6 +1489,8 @@ ZEND_API ZEND_COLD void zend_error_zstr_at(
14841489
ex->opline = opline;
14851490
}
14861491
}
1492+
} else if (EG(fatal_error_backtrace_on) && (type & E_FATAL_ERRORS)) {
1493+
zend_fetch_debug_backtrace(&EG(last_fatal_error_backtrace), 0, EG(exception_ignore_args) ? DEBUG_BACKTRACE_IGNORE_ARGS : 0, 0);
14871494
}
14881495

14891496
zend_observer_error_notify(type, error_filename, error_lineno, message);

Zend/zend_exceptions.c

+4
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,10 @@ ZEND_API ZEND_COLD zend_result zend_exception_error(zend_object *ex, int severit
903903
ZVAL_OBJ(&exception, ex);
904904
ce_exception = ex->ce;
905905
EG(exception) = NULL;
906+
907+
zval_ptr_dtor(&EG(last_fatal_error_backtrace));
908+
ZVAL_UNDEF(&EG(last_fatal_error_backtrace));
909+
906910
if (ce_exception == zend_ce_parse_error || ce_exception == zend_ce_compile_error) {
907911
zend_string *message = zval_get_string(GET_PROPERTY(&exception, ZEND_STR_MESSAGE));
908912
zend_string *file = zval_get_string(GET_PROPERTY_SILENT(&exception, ZEND_STR_FILE));

Zend/zend_execute_API.c

+5
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ void init_executor(void) /* {{{ */
140140
original_sigsegv_handler = signal(SIGSEGV, zend_handle_sigsegv);
141141
#endif
142142

143+
ZVAL_UNDEF(&EG(last_fatal_error_backtrace));
144+
143145
EG(symtable_cache_ptr) = EG(symtable_cache);
144146
EG(symtable_cache_limit) = EG(symtable_cache) + SYMTABLE_CACHE_SIZE;
145147
EG(no_extensions) = 0;
@@ -307,6 +309,9 @@ ZEND_API void zend_shutdown_executor_values(bool fast_shutdown)
307309
} ZEND_HASH_MAP_FOREACH_END_DEL();
308310
}
309311

312+
zval_ptr_dtor(&EG(last_fatal_error_backtrace));
313+
ZVAL_UNDEF(&EG(last_fatal_error_backtrace));
314+
310315
/* Release static properties and static variables prior to the final GC run,
311316
* as they may hold GC roots. */
312317
ZEND_HASH_MAP_REVERSE_FOREACH_VAL(EG(function_table), zv) {

Zend/zend_globals.h

+4
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,10 @@ struct _zend_executor_globals {
182182
JMP_BUF *bailout;
183183

184184
int error_reporting;
185+
186+
bool fatal_error_backtrace_on;
187+
zval last_fatal_error_backtrace;
188+
185189
int exit_status;
186190

187191
HashTable *function_table; /* function symbol table */

ext/opcache/tests/gh8846.phpt

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ echo file_get_contents('http://' . PHP_CLI_SERVER_ADDRESS . '/gh8846-index.php?s
3434
bool(true)
3535
<br />
3636
<b>Fatal error</b>: Cannot redeclare class Foo (previously declared in %sgh8846-1.inc:2) in <b>%sgh8846-2.inc</b> on line <b>%d</b><br />
37+
Stack trace:
38+
#0 %sgh8846-index.php(%d): include()
39+
#1 {main}
3740

3841
bool(true)
3942
Ok

ext/standard/basic_functions.c

+8
Original file line numberDiff line numberDiff line change
@@ -1436,6 +1436,11 @@ PHP_FUNCTION(error_get_last)
14361436

14371437
ZVAL_LONG(&tmp, PG(last_error_lineno));
14381438
zend_hash_update(Z_ARR_P(return_value), ZSTR_KNOWN(ZEND_STR_LINE), &tmp);
1439+
1440+
if (!Z_ISUNDEF(EG(last_fatal_error_backtrace))) {
1441+
ZVAL_COPY(&tmp, &EG(last_fatal_error_backtrace));
1442+
zend_hash_update(Z_ARR_P(return_value), ZSTR_KNOWN(ZEND_STR_TRACE), &tmp);
1443+
}
14391444
}
14401445
}
14411446
/* }}} */
@@ -1457,6 +1462,9 @@ PHP_FUNCTION(error_clear_last)
14571462
PG(last_error_file) = NULL;
14581463
}
14591464
}
1465+
1466+
zval_ptr_dtor(&EG(last_fatal_error_backtrace));
1467+
ZVAL_UNDEF(&EG(last_fatal_error_backtrace));
14601468
}
14611469
/* }}} */
14621470

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
--TEST--
2+
error_get_last() w/ fatal error
3+
--INI--
4+
fatal_error_backtraces=On
5+
--FILE--
6+
<?php
7+
8+
function trigger_fatal_error_with_stacktrace() {
9+
eval("class Foo {}; class Foo {}");
10+
}
11+
12+
register_shutdown_function(function() {
13+
var_dump(error_get_last());
14+
echo "Done\n";
15+
});
16+
17+
trigger_fatal_error_with_stacktrace();
18+
?>
19+
--EXPECTF--
20+
Fatal error: Cannot redeclare class Foo (%s) in %s on line %d
21+
Stack trace:
22+
#0 %serror_get_last_002.php(%d): eval()
23+
#1 %serror_get_last_002.php(%d): trigger_fatal_error_with_stacktrace()
24+
#2 {main}
25+
array(5) {
26+
["type"]=>
27+
int(64)
28+
["message"]=>
29+
string(%d) "Cannot redeclare class Foo %s"
30+
["file"]=>
31+
string(%d) "%serror_get_last_002.php(%d) : eval()'d code"
32+
["line"]=>
33+
int(%d)
34+
["trace"]=>
35+
array(2) {
36+
[0]=>
37+
array(3) {
38+
["file"]=>
39+
string(%d) "%serror_get_last_002.php"
40+
["line"]=>
41+
int(%d)
42+
["function"]=>
43+
string(%d) "eval"
44+
}
45+
[1]=>
46+
array(4) {
47+
["file"]=>
48+
string(%d) "%serror_get_last_002.php"
49+
["line"]=>
50+
int(%d)
51+
["function"]=>
52+
string(%d) "trigger_fatal_error_with_stacktrace"
53+
["args"]=>
54+
array(0) {
55+
}
56+
}
57+
}
58+
}
59+
Done

main/main.c

+12-5
Original file line numberDiff line numberDiff line change
@@ -1282,6 +1282,7 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c
12821282
{
12831283
bool display;
12841284
int type = orig_type & E_ALL;
1285+
zend_string *backtrace = ZSTR_EMPTY_ALLOC();
12851286

12861287
/* check for repeated errors to be ignored */
12871288
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
13211322
}
13221323
}
13231324

1325+
if (!Z_ISUNDEF(EG(last_fatal_error_backtrace))) {
1326+
backtrace = zend_trace_to_string(Z_ARRVAL(EG(last_fatal_error_backtrace)), /* include_main */ true);
1327+
}
1328+
13241329
/* store the error if it has changed */
13251330
if (display) {
13261331
clear_last_error();
@@ -1389,14 +1394,14 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c
13891394
syslog(LOG_ALERT, "PHP %s: %s (%s)", error_type_str, ZSTR_VAL(message), GetCommandLine());
13901395
}
13911396
#endif
1392-
spprintf(&log_buffer, 0, "PHP %s: %s in %s on line %" PRIu32, error_type_str, ZSTR_VAL(message), ZSTR_VAL(error_filename), error_lineno);
1397+
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));
13931398
php_log_err_with_severity(log_buffer, syslog_type_int);
13941399
efree(log_buffer);
13951400
}
13961401

13971402
if (PG(display_errors) && ((module_initialized && !PG(during_request_startup)) || (PG(display_startup_errors)))) {
13981403
if (PG(xmlrpc_errors)) {
1399-
php_printf("<?xml version=\"1.0\"?><methodResponse><fault><value><struct><member><name>faultCode</name><value><int>" ZEND_LONG_FMT "</int></value></member><member><name>faultString</name><value><string>%s:%s in %s on line %" PRIu32 "</string></value></member></struct></value></fault></methodResponse>", PG(xmlrpc_error_number), error_type_str, ZSTR_VAL(message), ZSTR_VAL(error_filename), error_lineno);
1404+
php_printf("<?xml version=\"1.0\"?><methodResponse><fault><value><struct><member><name>faultCode</name><value><int>" ZEND_LONG_FMT "</int></value></member><member><name>faultString</name><value><string>%s:%s in %s on line %" PRIu32 "%s%s</string></value></member></struct></value></fault></methodResponse>", 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));
14001405
} else {
14011406
char *prepend_string = INI_STR("error_prepend_string");
14021407
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
14071412
php_printf("%s<br />\n<b>%s</b>: %s in <b>%s</b> on line <b>%" PRIu32 "</b><br />\n%s", STR_PRINT(prepend_string), error_type_str, ZSTR_VAL(buf), ZSTR_VAL(error_filename), error_lineno, STR_PRINT(append_string));
14081413
zend_string_free(buf);
14091414
} else {
1410-
php_printf_unchecked("%s<br />\n<b>%s</b>: %S in <b>%s</b> on line <b>%" PRIu32 "</b><br />\n%s", STR_PRINT(prepend_string), error_type_str, message, ZSTR_VAL(error_filename), error_lineno, STR_PRINT(append_string));
1415+
php_printf_unchecked("%s<br />\n<b>%s</b>: %S in <b>%s</b> on line <b>%" PRIu32 "</b><br />%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));
14111416
}
14121417
} else {
14131418
/* 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
14161421
) {
14171422
fprintf(stderr, "%s: ", error_type_str);
14181423
fwrite(ZSTR_VAL(message), sizeof(char), ZSTR_LEN(message), stderr);
1419-
fprintf(stderr, " in %s on line %" PRIu32 "\n", ZSTR_VAL(error_filename), error_lineno);
1424+
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));
14201425
#ifdef PHP_WIN32
14211426
fflush(stderr);
14221427
#endif
14231428
} else {
1424-
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));
1429+
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));
14251430
}
14261431
}
14271432
}
14281433
}
14291434
}
14301435

1436+
zend_string_release(backtrace);
1437+
14311438
/* Bail out if we can't recover */
14321439
switch (type) {
14331440
case E_CORE_ERROR:

run-tests.php

+1
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ function main(): void
272272
'disable_functions=',
273273
'output_buffering=Off',
274274
'error_reporting=' . E_ALL,
275+
'fatal_error_backtraces=Off',
275276
'display_errors=1',
276277
'display_startup_errors=1',
277278
'log_errors=0',

0 commit comments

Comments
 (0)