Skip to content

Commit

Permalink
This commit adds an INI setting, 'fatal_error_backtraces', which users
Browse files Browse the repository at this point in the history
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)
  #1 example.php(24): recurse()
  #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 Jan 7, 2025
1 parent e40543a commit 2abc87a
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 7 deletions.
18 changes: 18 additions & 0 deletions Zend/tests/fatal_error_backtraces_001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
--TEST--
Fatal error backtrace
--FILE--
<?php

ini_set('fatal_error_backtraces', true);

$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 %sfatal_error_backtraces_001.php(%d): include('%s')
#2 {main}
23 changes: 23 additions & 0 deletions Zend/tests/fatal_error_backtraces_002.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
--TEST--
Fatal error backtrace w/ sensitive parameters
--FILE--
<?php

ini_set('fatal_error_backtraces', true);

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 %sfatal_error_backtraces_002.php(%d): include(%s)
#2 %sfatal_error_backtraces_002.php(%d): oom(Object(SensitiveParameterValue))
#3 {main}
24 changes: 24 additions & 0 deletions Zend/tests/fatal_error_backtraces_003.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
--TEST--
Fatal error backtrace w/ zend.exception_ignore_args
--FILE--
<?php

ini_set('fatal_error_backtraces', true);
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 %sfatal_error_backtraces_003.php(%d): include(%s)
#2 %sfatal_error_backtraces_003.php(%d): oom()
#3 {main}
15 changes: 15 additions & 0 deletions Zend/zend.c
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ static ZEND_INI_MH(OnUpdateErrorReporting) /* {{{ */
}
/* }}} */

static ZEND_INI_MH(OnUpdateFatalErrorBacktraces)
{
EG(fatal_error_backtraces) = zend_ini_parse_bool(new_value);

return SUCCESS;
}

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

ZEND_INI_BEGIN()
ZEND_INI_ENTRY("error_reporting", NULL, ZEND_INI_ALL, OnUpdateErrorReporting)
ZEND_INI_ENTRY("fatal_error_backtraces", "1", ZEND_INI_ALL, OnUpdateFatalErrorBacktraces)
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 +819,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,6 +1057,7 @@ 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;

zend_interned_strings_init();
Expand Down Expand Up @@ -1463,6 +1473,9 @@ 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(error_backtrace));

/* Report about uncaught exception in case of fatal errors */
if (EG(exception)) {
zend_execute_data *ex;
Expand All @@ -1484,6 +1497,8 @@ ZEND_API ZEND_COLD void zend_error_zstr_at(
ex->opline = opline;
}
}
} else if (EG(fatal_error_backtraces) && (type & E_FATAL_ERRORS)) {
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
3 changes: 3 additions & 0 deletions Zend/zend_execute_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,9 @@ ZEND_API void zend_shutdown_executor_values(bool fast_shutdown)
} ZEND_HASH_MAP_FOREACH_END_DEL();
}

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
6 changes: 5 additions & 1 deletion Zend/zend_globals.h
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,11 @@ struct _zend_executor_globals {

JMP_BUF *bailout;

int error_reporting;
int error_reporting;

int fatal_error_backtraces;
zval error_backtrace;

int exit_status;

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

zval_ptr_dtor(&EG(error_backtrace));
ZVAL_UNDEF(&EG(error_backtrace));
}
/* }}} */

Expand Down
53 changes: 52 additions & 1 deletion ext/standard/tests/general_functions/error_get_last.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,18 @@ $a = $b;

var_dump(error_get_last());

echo "Done\n";
function trigger_fatal_error_with_stacktrace() {
ini_set('fatal_error_backtraces', true);

eval("class Foo {}; class Foo {}");
}

register_shutdown_function(function() {
var_dump(error_get_last());
echo "Done\n";
});

trigger_fatal_error_with_stacktrace();
?>
--EXPECTF--
NULL
Expand All @@ -33,4 +44,44 @@ array(4) {
["line"]=>
int(11)
}

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
Stack trace:
#0 %serror_get_last.php(%d): eval()
#1 %serror_get_last.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.php(%d) : eval()'d code"
["line"]=>
int(%d)
["trace"]=>
array(2) {
[0]=>
array(3) {
["file"]=>
string(%d) "%serror_get_last.php"
["line"]=>
int(%d)
["function"]=>
string(%d) "eval"
}
[1]=>
array(4) {
["file"]=>
string(%d) "%serror_get_last.php"
["line"]=>
int(%d)
["function"]=>
string(%d) "trigger_fatal_error_with_stacktrace"
["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 = ZSTR_EMPTY_ALLOC();

/* 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
1 change: 1 addition & 0 deletions run-tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit 2abc87a

Please sign in to comment.