Skip to content

Commit 320db3b

Browse files
committed
feat: add backtraces to errors (v2)
This commit adds an INI setting, 'error_backtrace_recording', which users can set to an error mask to enable backtraces for those errors. It defaults to E_FATAL_ERRORS, 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 b01f5e3 commit 320db3b

10 files changed

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

Zend/zend.c

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

122+
static ZEND_INI_MH(OnUpdateErrorBacktraceRecording)
123+
{
124+
if (!new_value) {
125+
EG(error_backtrace_recording) = E_FATAL_ERRORS;
126+
} else {
127+
EG(error_backtrace_recording) = atoi(ZSTR_VAL(new_value));
128+
}
129+
130+
return SUCCESS;
131+
}
132+
122133
static ZEND_INI_MH(OnUpdateGCEnabled) /* {{{ */
123134
{
124135
bool val;
@@ -260,6 +271,7 @@ static ZEND_INI_MH(OnUpdateFiberStackSize) /* {{{ */
260271

261272
ZEND_INI_BEGIN()
262273
ZEND_INI_ENTRY("error_reporting", NULL, ZEND_INI_ALL, OnUpdateErrorReporting)
274+
ZEND_INI_ENTRY("error_backtrace_recording", NULL, ZEND_INI_ALL, OnUpdateErrorBacktraceRecording)
263275
STD_ZEND_INI_ENTRY("zend.assertions", "1", ZEND_INI_ALL, OnUpdateAssertions, assertions, zend_executor_globals, executor_globals)
264276
ZEND_INI_ENTRY3_EX("zend.enable_gc", "1", ZEND_INI_ALL, OnUpdateGCEnabled, NULL, NULL, NULL, zend_gc_enabled_displayer_cb)
265277
STD_ZEND_INI_BOOLEAN("zend.multibyte", "0", ZEND_INI_PERDIR, OnUpdateBool, multibyte, zend_compiler_globals, compiler_globals)
@@ -811,6 +823,7 @@ static void executor_globals_ctor(zend_executor_globals *executor_globals) /* {{
811823
executor_globals->in_autoload = NULL;
812824
executor_globals->current_execute_data = NULL;
813825
executor_globals->current_module = NULL;
826+
ZVAL_UNDEF(&executor_globals->error_backtrace);
814827
executor_globals->exit_status = 0;
815828
#if XPFPA_HAVE_CW
816829
executor_globals->saved_fpu_cw = 0;
@@ -1048,7 +1061,9 @@ void zend_startup(zend_utility_functions *utility_functions) /* {{{ */
10481061
CG(map_ptr_size) = 0;
10491062
CG(map_ptr_last) = 0;
10501063
#endif /* ZTS */
1064+
10511065
EG(error_reporting) = E_ALL & ~E_NOTICE;
1066+
EG(error_backtrace_recording) = E_FATAL_ERRORS;
10521067

10531068
zend_interned_strings_init();
10541069
zend_startup_builtin_functions();
@@ -1484,6 +1499,17 @@ ZEND_API ZEND_COLD void zend_error_zstr_at(
14841499
ex->opline = opline;
14851500
}
14861501
}
1502+
} else if (EG(error_backtrace_recording) & type) {
1503+
if (!Z_ISUNDEF(EG(error_backtrace))) {
1504+
zval_ptr_dtor(&EG(error_backtrace));
1505+
}
1506+
1507+
zend_fetch_debug_backtrace(
1508+
&EG(error_backtrace),
1509+
0,
1510+
EG(exception_ignore_args) ? DEBUG_BACKTRACE_IGNORE_ARGS : 0,
1511+
0
1512+
);
14871513
}
14881514

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

Zend/zend_constants_arginfo.h

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Zend/zend_execute_API.c

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

310+
if (!Z_ISUNDEF(EG(error_backtrace))) {
311+
zval_ptr_dtor(&EG(error_backtrace));
312+
ZVAL_UNDEF(&EG(error_backtrace));
313+
}
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

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

182182
JMP_BUF *bailout;
183183

184-
int error_reporting;
184+
int error_reporting;
185+
int error_backtrace_recording;
186+
zval error_backtrace;
187+
185188
int exit_status;
186189

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

ext/standard/basic_functions.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,6 +1435,11 @@ PHP_FUNCTION(error_get_last)
14351435

14361436
ZVAL_LONG(&tmp, PG(last_error_lineno));
14371437
zend_hash_update(Z_ARR_P(return_value), ZSTR_KNOWN(ZEND_STR_LINE), &tmp);
1438+
1439+
if (!Z_ISUNDEF(EG(error_backtrace))) {
1440+
ZVAL_COPY(&tmp, &EG(error_backtrace));
1441+
zend_hash_update(Z_ARR_P(return_value), ZSTR_KNOWN(ZEND_STR_TRACE), &tmp);
1442+
}
14381443
}
14391444
}
14401445
/* }}} */
@@ -1456,6 +1461,11 @@ PHP_FUNCTION(error_clear_last)
14561461
PG(last_error_file) = NULL;
14571462
}
14581463
}
1464+
1465+
if (!Z_ISUNDEF(EG(error_backtrace))) {
1466+
zval_ptr_dtor(&EG(error_backtrace));
1467+
ZVAL_UNDEF(&EG(error_backtrace));
1468+
}
14591469
}
14601470
/* }}} */
14611471

ext/standard/tests/general_functions/error_get_last.phpt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ $a = $b;
1515

1616
var_dump(error_get_last());
1717

18+
function trigger_warning_with_stack_trace() {
19+
ini_set('error_backtrace_recording', E_WARNING);
20+
21+
$a = $b;
22+
23+
var_dump(error_get_last());
24+
}
25+
26+
trigger_warning_with_stack_trace();
27+
1828
echo "Done\n";
1929
?>
2030
--EXPECTF--
@@ -33,4 +43,34 @@ array(4) {
3343
["line"]=>
3444
int(11)
3545
}
46+
47+
Warning: Undefined variable $b in %serror_get_last.php on line %d
48+
Stack trace:
49+
#0 %serror_get_last.php(%d): trigger_warning_with_stack_trace()
50+
#1 {main}
51+
array(5) {
52+
["type"]=>
53+
int(2)
54+
["message"]=>
55+
string(21) "Undefined variable $b"
56+
["file"]=>
57+
string(%d) "%serror_get_last.php"
58+
["line"]=>
59+
int(%d)
60+
["trace"]=>
61+
array(1) {
62+
[0]=>
63+
array(4) {
64+
["file"]=>
65+
string(%d) "%serror_get_last.php"
66+
["line"]=>
67+
int(%d)
68+
["function"]=>
69+
string(%d) "trigger_warning_with_stack_trace"
70+
["args"]=>
71+
array(0) {
72+
}
73+
}
74+
}
75+
}
3676
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 = zend_empty_string;
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:

0 commit comments

Comments
 (0)