Skip to content

Commit 2abc87a

Browse files
committed
This commit adds an INI setting, 'fatal_error_backtraces', which users
can enable or disable to control whether or not PHP produces backtraces for fatal errors. It defaults to enabled, meaning that any non-recoverable error will now have a backtrace associated with it. For example, a script timeout will now look like: Fatal error: Maximum execution time of 1 second exceeded in example.php on line 23 Stack trace: #0 example.php(23): usleep(10000) php#1 example.php(24): recurse() php#2 example.php(24): recurse() ... It respects the `zend.exception_ignore_args` INI setting and the SensitiveParameter attributes, so users can ensure that sensitive arguments do not end up in the backtrace.
1 parent e40543a commit 2abc87a

10 files changed

+161
-7
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
--TEST--
2+
Fatal error backtrace
3+
--FILE--
4+
<?php
5+
6+
ini_set('fatal_error_backtraces', true);
7+
8+
$argv[1] = "stdClass";
9+
10+
include __DIR__ . '/new_oom.inc';
11+
12+
?>
13+
--EXPECTF--
14+
Fatal error: Allowed memory size of %d bytes exhausted at %s:%d (tried to allocate %d bytes) in %snew_oom.inc on line %d
15+
Stack trace:
16+
#0 %snew_oom.inc(%d): ReflectionClass->newInstanceWithoutConstructor()
17+
#1 %sfatal_error_backtraces_001.php(%d): include('%s')
18+
#2 {main}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
--TEST--
2+
Fatal error backtrace w/ sensitive parameters
3+
--FILE--
4+
<?php
5+
6+
ini_set('fatal_error_backtraces', true);
7+
8+
function oom(#[\SensitiveParameter] $unused) {
9+
$argv[1] = "stdClass";
10+
11+
include __DIR__ . '/new_oom.inc';
12+
}
13+
14+
oom("foo");
15+
16+
?>
17+
--EXPECTF--
18+
Fatal error: Allowed memory size of %d bytes exhausted at %s:%d (tried to allocate %d bytes) in %snew_oom.inc on line %d
19+
Stack trace:
20+
#0 %snew_oom.inc(%d): ReflectionClass->newInstanceWithoutConstructor()
21+
#1 %sfatal_error_backtraces_002.php(%d): include(%s)
22+
#2 %sfatal_error_backtraces_002.php(%d): oom(Object(SensitiveParameterValue))
23+
#3 {main}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
--TEST--
2+
Fatal error backtrace w/ zend.exception_ignore_args
3+
--FILE--
4+
<?php
5+
6+
ini_set('fatal_error_backtraces', true);
7+
ini_set('zend.exception_ignore_args', true);
8+
9+
function oom($unused) {
10+
$argv[1] = "stdClass";
11+
12+
include __DIR__ . '/new_oom.inc';
13+
}
14+
15+
oom("foo");
16+
17+
?>
18+
--EXPECTF--
19+
Fatal error: Allowed memory size of %d bytes exhausted at %s:%d (tried to allocate %d bytes) in %snew_oom.inc on line %d
20+
Stack trace:
21+
#0 %snew_oom.inc(%d): ReflectionClass->newInstanceWithoutConstructor()
22+
#1 %sfatal_error_backtraces_003.php(%d): include(%s)
23+
#2 %sfatal_error_backtraces_003.php(%d): oom()
24+
#3 {main}

Zend/zend.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,13 @@ static ZEND_INI_MH(OnUpdateErrorReporting) /* {{{ */
119119
}
120120
/* }}} */
121121

122+
static ZEND_INI_MH(OnUpdateFatalErrorBacktraces)
123+
{
124+
EG(fatal_error_backtraces) = zend_ini_parse_bool(new_value);
125+
126+
return SUCCESS;
127+
}
128+
122129
static ZEND_INI_MH(OnUpdateGCEnabled) /* {{{ */
123130
{
124131
bool val;
@@ -260,6 +267,7 @@ static ZEND_INI_MH(OnUpdateFiberStackSize) /* {{{ */
260267

261268
ZEND_INI_BEGIN()
262269
ZEND_INI_ENTRY("error_reporting", NULL, ZEND_INI_ALL, OnUpdateErrorReporting)
270+
ZEND_INI_ENTRY("fatal_error_backtraces", "1", ZEND_INI_ALL, OnUpdateFatalErrorBacktraces)
263271
STD_ZEND_INI_ENTRY("zend.assertions", "1", ZEND_INI_ALL, OnUpdateAssertions, assertions, zend_executor_globals, executor_globals)
264272
ZEND_INI_ENTRY3_EX("zend.enable_gc", "1", ZEND_INI_ALL, OnUpdateGCEnabled, NULL, NULL, NULL, zend_gc_enabled_displayer_cb)
265273
STD_ZEND_INI_BOOLEAN("zend.multibyte", "0", ZEND_INI_PERDIR, OnUpdateBool, multibyte, zend_compiler_globals, compiler_globals)
@@ -811,6 +819,7 @@ static void executor_globals_ctor(zend_executor_globals *executor_globals) /* {{
811819
executor_globals->in_autoload = NULL;
812820
executor_globals->current_execute_data = NULL;
813821
executor_globals->current_module = NULL;
822+
ZVAL_UNDEF(&executor_globals->error_backtrace);
814823
executor_globals->exit_status = 0;
815824
#if XPFPA_HAVE_CW
816825
executor_globals->saved_fpu_cw = 0;
@@ -1048,6 +1057,7 @@ void zend_startup(zend_utility_functions *utility_functions) /* {{{ */
10481057
CG(map_ptr_size) = 0;
10491058
CG(map_ptr_last) = 0;
10501059
#endif /* ZTS */
1060+
10511061
EG(error_reporting) = E_ALL & ~E_NOTICE;
10521062

10531063
zend_interned_strings_init();
@@ -1463,6 +1473,9 @@ ZEND_API ZEND_COLD void zend_error_zstr_at(
14631473
EG(errors)[EG(num_errors)-1] = info;
14641474
}
14651475

1476+
// Always clear the last backtrace.
1477+
zval_ptr_dtor(&EG(error_backtrace));
1478+
14661479
/* Report about uncaught exception in case of fatal errors */
14671480
if (EG(exception)) {
14681481
zend_execute_data *ex;
@@ -1484,6 +1497,8 @@ ZEND_API ZEND_COLD void zend_error_zstr_at(
14841497
ex->opline = opline;
14851498
}
14861499
}
1500+
} else if (EG(fatal_error_backtraces) && (type & E_FATAL_ERRORS)) {
1501+
zend_fetch_debug_backtrace(&EG(error_backtrace), 0, EG(exception_ignore_args) ? DEBUG_BACKTRACE_IGNORE_ARGS : 0, 0);
14871502
}
14881503

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

Zend/zend_execute_API.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,9 @@ ZEND_API void zend_shutdown_executor_values(bool fast_shutdown)
307307
} ZEND_HASH_MAP_FOREACH_END_DEL();
308308
}
309309

310+
zval_ptr_dtor(&EG(error_backtrace));
311+
ZVAL_UNDEF(&EG(error_backtrace));
312+
310313
/* Release static properties and static variables prior to the final GC run,
311314
* as they may hold GC roots. */
312315
ZEND_HASH_MAP_REVERSE_FOREACH_VAL(EG(function_table), zv) {

Zend/zend_globals.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,11 @@ struct _zend_executor_globals {
181181

182182
JMP_BUF *bailout;
183183

184-
int error_reporting;
184+
int error_reporting;
185+
186+
int fatal_error_backtraces;
187+
zval error_backtrace;
188+
185189
int exit_status;
186190

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

ext/standard/basic_functions.c

Lines changed: 8 additions & 0 deletions
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(error_backtrace))) {
1441+
ZVAL_COPY(&tmp, &EG(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(error_backtrace));
1467+
ZVAL_UNDEF(&EG(error_backtrace));
14601468
}
14611469
/* }}} */
14621470

ext/standard/tests/general_functions/error_get_last.phpt

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,18 @@ $a = $b;
1515

1616
var_dump(error_get_last());
1717

18-
echo "Done\n";
18+
function trigger_fatal_error_with_stacktrace() {
19+
ini_set('fatal_error_backtraces', true);
20+
21+
eval("class Foo {}; class Foo {}");
22+
}
23+
24+
register_shutdown_function(function() {
25+
var_dump(error_get_last());
26+
echo "Done\n";
27+
});
28+
29+
trigger_fatal_error_with_stacktrace();
1930
?>
2031
--EXPECTF--
2132
NULL
@@ -33,4 +44,44 @@ array(4) {
3344
["line"]=>
3445
int(11)
3546
}
47+
48+
Fatal error: Cannot redeclare class Foo (previously declared in /Users/enorris/workspace/php-src/ext/standard/tests/general_functions/error_get_last.php(18) : eval()'d code:1) in /Users/enorris/workspace/php-src/ext/standard/tests/general_functions/error_get_last.php(18) : eval()'d code on line 1
49+
Stack trace:
50+
#0 %serror_get_last.php(%d): eval()
51+
#1 %serror_get_last.php(%d): trigger_fatal_error_with_stacktrace()
52+
#2 {main}
53+
array(5) {
54+
["type"]=>
55+
int(64)
56+
["message"]=>
57+
string(%d) "Cannot redeclare class Foo %s"
58+
["file"]=>
59+
string(%d) "%serror_get_last.php(%d) : eval()'d code"
60+
["line"]=>
61+
int(%d)
62+
["trace"]=>
63+
array(2) {
64+
[0]=>
65+
array(3) {
66+
["file"]=>
67+
string(%d) "%serror_get_last.php"
68+
["line"]=>
69+
int(%d)
70+
["function"]=>
71+
string(%d) "eval"
72+
}
73+
[1]=>
74+
array(4) {
75+
["file"]=>
76+
string(%d) "%serror_get_last.php"
77+
["line"]=>
78+
int(%d)
79+
["function"]=>
80+
string(%d) "trigger_fatal_error_with_stacktrace"
81+
["args"]=>
82+
array(0) {
83+
}
84+
}
85+
}
86+
}
3687
Done

main/main.c

Lines changed: 12 additions & 5 deletions
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(error_backtrace))) {
1326+
backtrace = zend_trace_to_string(Z_ARRVAL(EG(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

Lines changed: 1 addition & 0 deletions
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)