Skip to content

Commit

Permalink
feat: add backtraces to errors (v2)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ericnorris committed Dec 5, 2024
1 parent b01f5e3 commit 320db3b
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 6 deletions.
16 changes: 16 additions & 0 deletions Zend/tests/error_backtrace_recording_001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
--TEST--
Fatal error backtrace
--FILE--
<?php

$argv[1] = "stdClass";

include __DIR__ . '/new_oom.inc';

?>
--EXPECTF--
Fatal error: Allowed memory size of %d bytes exhausted at %s:%d (tried to allocate %d bytes) in %snew_oom.inc on line %d
Stack trace:
#0 %snew_oom.inc(%d): ReflectionClass->newInstanceWithoutConstructor()
#1 %serror_backtrace_recording_001.php(%d): include('%s')
#2 {main}
21 changes: 21 additions & 0 deletions Zend/tests/error_backtrace_recording_002.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
--TEST--
Fatal error backtrace w/ sensitive parameters
--FILE--
<?php

function oom(#[\SensitiveParameter] $unused) {
$argv[1] = "stdClass";

include __DIR__ . '/new_oom.inc';
}

oom("foo");

?>
--EXPECTF--
Fatal error: Allowed memory size of %d bytes exhausted at %s:%d (tried to allocate %d bytes) in %snew_oom.inc on line %d
Stack trace:
#0 %snew_oom.inc(%d): ReflectionClass->newInstanceWithoutConstructor()
#1 %serror_backtrace_recording_002.php(%d): include(%s)
#2 %serror_backtrace_recording_002.php(%d): oom(Object(SensitiveParameterValue))
#3 {main}
23 changes: 23 additions & 0 deletions Zend/tests/error_backtrace_recording_003.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
--TEST--
Fatal error backtrace w/ zend.exception_ignore_args
--FILE--
<?php

ini_set('zend.exception_ignore_args', true);

function oom($unused) {
$argv[1] = "stdClass";

include __DIR__ . '/new_oom.inc';
}

oom("foo");

?>
--EXPECTF--
Fatal error: Allowed memory size of %d bytes exhausted at %s:%d (tried to allocate %d bytes) in %snew_oom.inc on line %d
Stack trace:
#0 %snew_oom.inc(%d): ReflectionClass->newInstanceWithoutConstructor()
#1 %serror_backtrace_recording_003.php(%d): include(%s)
#2 %serror_backtrace_recording_003.php(%d): oom()
#3 {main}
26 changes: 26 additions & 0 deletions Zend/zend.c
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,17 @@ static ZEND_INI_MH(OnUpdateErrorReporting) /* {{{ */
}
/* }}} */

static ZEND_INI_MH(OnUpdateErrorBacktraceRecording)
{
if (!new_value) {
EG(error_backtrace_recording) = E_FATAL_ERRORS;
} else {
EG(error_backtrace_recording) = atoi(ZSTR_VAL(new_value));
}

return SUCCESS;
}

static ZEND_INI_MH(OnUpdateGCEnabled) /* {{{ */
{
bool val;
Expand Down Expand Up @@ -260,6 +271,7 @@ static ZEND_INI_MH(OnUpdateFiberStackSize) /* {{{ */

ZEND_INI_BEGIN()
ZEND_INI_ENTRY("error_reporting", NULL, ZEND_INI_ALL, OnUpdateErrorReporting)
ZEND_INI_ENTRY("error_backtrace_recording", NULL, ZEND_INI_ALL, OnUpdateErrorBacktraceRecording)
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)
Expand Down Expand Up @@ -811,6 +823,7 @@ static void executor_globals_ctor(zend_executor_globals *executor_globals) /* {{
executor_globals->in_autoload = NULL;
executor_globals->current_execute_data = NULL;
executor_globals->current_module = NULL;
ZVAL_UNDEF(&executor_globals->error_backtrace);
executor_globals->exit_status = 0;
#if XPFPA_HAVE_CW
executor_globals->saved_fpu_cw = 0;
Expand Down Expand Up @@ -1048,7 +1061,9 @@ void zend_startup(zend_utility_functions *utility_functions) /* {{{ */
CG(map_ptr_size) = 0;
CG(map_ptr_last) = 0;
#endif /* ZTS */

EG(error_reporting) = E_ALL & ~E_NOTICE;
EG(error_backtrace_recording) = E_FATAL_ERRORS;

zend_interned_strings_init();
zend_startup_builtin_functions();
Expand Down Expand Up @@ -1484,6 +1499,17 @@ ZEND_API ZEND_COLD void zend_error_zstr_at(
ex->opline = opline;
}
}
} else if (EG(error_backtrace_recording) & type) {
if (!Z_ISUNDEF(EG(error_backtrace))) {
zval_ptr_dtor(&EG(error_backtrace));
}

zend_fetch_debug_backtrace(
&EG(error_backtrace),
0,
EG(exception_ignore_args) ? DEBUG_BACKTRACE_IGNORE_ARGS : 0,
0
);
}

zend_observer_error_notify(type, error_filename, error_lineno, message);
Expand Down
1 change: 1 addition & 0 deletions Zend/zend_constants_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Zend/zend_execute_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,11 @@ ZEND_API void zend_shutdown_executor_values(bool fast_shutdown)
} ZEND_HASH_MAP_FOREACH_END_DEL();
}

if (!Z_ISUNDEF(EG(error_backtrace))) {
zval_ptr_dtor(&EG(error_backtrace));
ZVAL_UNDEF(&EG(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) {
Expand Down
5 changes: 4 additions & 1 deletion Zend/zend_globals.h
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,10 @@ struct _zend_executor_globals {

JMP_BUF *bailout;

int error_reporting;
int error_reporting;
int error_backtrace_recording;
zval error_backtrace;

int exit_status;

HashTable *function_table; /* function symbol table */
Expand Down
10 changes: 10 additions & 0 deletions ext/standard/basic_functions.c
Original file line number Diff line number Diff line change
Expand Up @@ -1435,6 +1435,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(error_backtrace))) {
ZVAL_COPY(&tmp, &EG(error_backtrace));
zend_hash_update(Z_ARR_P(return_value), ZSTR_KNOWN(ZEND_STR_TRACE), &tmp);
}
}
}
/* }}} */
Expand All @@ -1456,6 +1461,11 @@ PHP_FUNCTION(error_clear_last)
PG(last_error_file) = NULL;
}
}

if (!Z_ISUNDEF(EG(error_backtrace))) {
zval_ptr_dtor(&EG(error_backtrace));
ZVAL_UNDEF(&EG(error_backtrace));
}
}
/* }}} */

Expand Down
40 changes: 40 additions & 0 deletions ext/standard/tests/general_functions/error_get_last.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ $a = $b;

var_dump(error_get_last());

function trigger_warning_with_stack_trace() {
ini_set('error_backtrace_recording', E_WARNING);

$a = $b;

var_dump(error_get_last());
}

trigger_warning_with_stack_trace();

echo "Done\n";
?>
--EXPECTF--
Expand All @@ -33,4 +43,34 @@ array(4) {
["line"]=>
int(11)
}

Warning: Undefined variable $b in %serror_get_last.php on line %d
Stack trace:
#0 %serror_get_last.php(%d): trigger_warning_with_stack_trace()
#1 {main}
array(5) {
["type"]=>
int(2)
["message"]=>
string(21) "Undefined variable $b"
["file"]=>
string(%d) "%serror_get_last.php"
["line"]=>
int(%d)
["trace"]=>
array(1) {
[0]=>
array(4) {
["file"]=>
string(%d) "%serror_get_last.php"
["line"]=>
int(%d)
["function"]=>
string(%d) "trigger_warning_with_stack_trace"
["args"]=>
array(0) {
}
}
}
}
Done
17 changes: 12 additions & 5 deletions main/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 = zend_empty_string;

/* check for repeated errors to be ignored */
if (PG(ignore_repeated_errors) && PG(last_error_message)) {
Expand Down Expand Up @@ -1321,6 +1322,10 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c
}
}

if (!Z_ISUNDEF(EG(error_backtrace))) {
backtrace = zend_trace_to_string(Z_ARRVAL(EG(error_backtrace)), /* include_main */ true);
}

/* store the error if it has changed */
if (display) {
clear_last_error();
Expand Down Expand Up @@ -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("<?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);
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));
} else {
char *prepend_string = INI_STR("error_prepend_string");
char *append_string = INI_STR("error_append_string");
Expand All @@ -1407,7 +1412,7 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c
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));
zend_string_free(buf);
} else {
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));
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));
}
} else {
/* Write CLI/CGI errors to stderr if display_errors = "stderr" */
Expand All @@ -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:
Expand Down

0 comments on commit 320db3b

Please sign in to comment.