Commit a22c56c969f for php.net
commit a22c56c969f1fc34bfc9195dc2cfc70a15186148
Author: Calvin Buckley <calvinb@php.net>
Date: Mon Jun 1 18:01:09 2026 -0300
Add `error_include_args` INI option to display function args in docref (#12276)
Displays arguments for errors from built-in PHP functions using docref API.
RFC: https://wiki.php.net/rfc/display_error_function_args
Co-authored-by: Tim Düsterhus <tim@bastelstu.be>
diff --git a/Zend/tests/display_error_function_args.phpt b/Zend/tests/display_error_function_args.phpt
new file mode 100644
index 00000000000..c28a4a2808b
--- /dev/null
+++ b/Zend/tests/display_error_function_args.phpt
@@ -0,0 +1,30 @@
+--TEST--
+Displaying function arguments in errors
+--INI--
+error_include_args=On
+--FILE--
+<?php
+
+// A function that sets its own parameters in docref call, to compare
+unlink('/');
+
+// Something with sensitive parameters that exists in a minimal build,
+// and also doesn't set anything in the docref call. cost is set to 4
+// to keep the test fast
+$flags = ["salt" => "123456789012345678901" . chr(0), "cost" => 4];
+password_hash("test", PASSWORD_BCRYPT, $flags);
+
+ini_set("error_include_args", "Off");
+
+unlink('/');
+password_hash("test", PASSWORD_BCRYPT, $flags);
+
+?>
+--EXPECTF--
+Warning: unlink('/'): %s in %s on line %d
+
+Warning: password_hash(Object(SensitiveParameterValue), '2y', Array): The "salt" option has been ignored, since providing a custom salt is no longer supported in %s on line %d
+
+Warning: unlink(/): %s in %s on line %d
+
+Warning: password_hash(): The "salt" option has been ignored, since providing a custom salt is no longer supported in %s on line %d
diff --git a/Zend/zend_exceptions.c b/Zend/zend_exceptions.c
index d23fb647af9..a1301b8c20b 100644
--- a/Zend/zend_exceptions.c
+++ b/Zend/zend_exceptions.c
@@ -506,7 +506,7 @@ ZEND_METHOD(ErrorException, getSeverity)
} \
} while (0)
-static void _build_trace_args(zval *arg, smart_str *str) /* {{{ */
+static void build_trace_args(zval *arg, smart_str *str) /* {{{ */
{
/* the trivial way would be to do
* convert_to_string(arg);
@@ -516,24 +516,21 @@ static void _build_trace_args(zval *arg, smart_str *str) /* {{{ */
ZVAL_DEREF(arg);
- if (smart_str_append_zval(str, arg, EG(exception_string_param_max_len)) == SUCCESS) {
- smart_str_appends(str, ", ");
- } else {
+ if (smart_str_append_zval(str, arg, EG(exception_string_param_max_len)) != SUCCESS) {
switch (Z_TYPE_P(arg)) {
case IS_RESOURCE:
smart_str_appends(str, "Resource id #");
smart_str_append_long(str, Z_RES_HANDLE_P(arg));
- smart_str_appends(str, ", ");
break;
case IS_ARRAY:
- smart_str_appends(str, "Array, ");
+ smart_str_appends(str, "Array");
break;
case IS_OBJECT: {
zend_string *class_name = Z_OBJ_HANDLER_P(arg, get_class_name)(Z_OBJ_P(arg));
smart_str_appends(str, "Object(");
/* cut off on NULL byte ... class@anonymous */
smart_str_appends(str, ZSTR_VAL(class_name));
- smart_str_appends(str, "), ");
+ smart_str_appends(str, ")");
zend_string_release_ex(class_name, 0);
break;
}
@@ -542,7 +539,30 @@ static void _build_trace_args(zval *arg, smart_str *str) /* {{{ */
}
/* }}} */
-static void _build_trace_string(smart_str *str, const HashTable *ht, uint32_t num) /* {{{ */
+static void build_trace_args_list(zval *tmp, smart_str *str) /* {{{ */
+{
+ if (UNEXPECTED(Z_TYPE_P(tmp) != IS_ARRAY)) {
+ /* only happens w/ reflection abuse (Zend/tests/bug63762.phpt) */
+ zend_error(E_WARNING, "args element is not an array");
+ return;
+ }
+
+ bool first = true;
+ ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(tmp), zend_string *name, zval *arg) {
+ if (!first) {
+ smart_str_appends(str, ", ");
+ }
+ first = false;
+ if (name) {
+ smart_str_append(str, name);
+ smart_str_appends(str, ": ");
+ }
+ build_trace_args(arg, str);
+ } ZEND_HASH_FOREACH_END();
+}
+/* }}} */
+
+static void build_trace_string(smart_str *str, const HashTable *ht, uint32_t num) /* {{{ */
{
zval *file, *tmp;
@@ -588,27 +608,40 @@ static void _build_trace_string(smart_str *str, const HashTable *ht, uint32_t nu
smart_str_appendc(str, '(');
tmp = zend_hash_find_known_hash(ht, ZSTR_KNOWN(ZEND_STR_ARGS));
if (tmp) {
- if (EXPECTED(Z_TYPE_P(tmp) == IS_ARRAY)) {
- size_t last_len = ZSTR_LEN(str->s);
- zend_string *name;
- zval *arg;
-
- ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(tmp), name, arg) {
- if (name) {
- smart_str_append(str, name);
- smart_str_appends(str, ": ");
- }
- _build_trace_args(arg, str);
- } ZEND_HASH_FOREACH_END();
+ build_trace_args_list(tmp, str);
+ }
+ smart_str_appends(str, ")\n");
+}
+/* }}} */
- if (last_len != ZSTR_LEN(str->s)) {
- ZSTR_LEN(str->s) -= 2; /* remove last ', ' */
- }
- } else {
- zend_error(E_WARNING, "args element is not an array");
+/* {{{ Gets the function arguments printed as a string from a backtrace frame. */
+ZEND_API zend_string *zend_trace_function_args_to_string(const HashTable *frame) {
+ smart_str str = {0};
+
+ zval *tmp = zend_hash_find_known_hash(frame, ZSTR_KNOWN(ZEND_STR_ARGS));
+ if (tmp) {
+ build_trace_args_list(tmp, &str);
+ }
+
+ return smart_str_extract(&str);
+}
+/* }}} */
+
+/* {{{ Gets the currently executing function's arguments as a string. Used by php_verror. */
+ZEND_API zend_string *zend_trace_current_function_args_string(void) {
+ zend_string *dynamic_params = NULL;
+ /* get a backtrace to snarf function args */
+ zval backtrace;
+ zend_fetch_debug_backtrace(&backtrace, /* skip_last */ 0, /* options */ 0, /* limit */ 1);
+ /* can fail esp if low memory condition */
+ if (Z_TYPE(backtrace) == IS_ARRAY) {
+ zval *first_frame = zend_hash_index_find(Z_ARRVAL(backtrace), 0);
+ if (first_frame) {
+ dynamic_params = zend_trace_function_args_to_string(Z_ARRVAL_P(first_frame));
}
}
- smart_str_appends(str, ")\n");
+ zval_ptr_dtor(&backtrace);
+ return dynamic_params;
}
/* }}} */
@@ -624,7 +657,7 @@ ZEND_API zend_string *zend_trace_to_string(const HashTable *trace, bool include_
continue;
}
- _build_trace_string(&str, Z_ARRVAL_P(frame), num++);
+ build_trace_string(&str, Z_ARRVAL_P(frame), num++);
} ZEND_HASH_FOREACH_END();
if (include_main) {
diff --git a/Zend/zend_exceptions.h b/Zend/zend_exceptions.h
index f9b47259801..7ef9ef01639 100644
--- a/Zend/zend_exceptions.h
+++ b/Zend/zend_exceptions.h
@@ -65,6 +65,8 @@ ZEND_API zend_result zend_update_exception_properties(zend_execute_data *execute
/* show an exception using zend_error(severity,...), severity should be E_ERROR */
ZEND_API ZEND_COLD zend_result zend_exception_error(zend_object *exception, int severity);
ZEND_NORETURN void zend_exception_uncaught_error(const char *prefix, ...) ZEND_ATTRIBUTE_FORMAT(printf, 1, 2);
+ZEND_API zend_string *zend_trace_function_args_to_string(const HashTable *frame);
+ZEND_API zend_string *zend_trace_current_function_args_string(void);
ZEND_API zend_string *zend_trace_to_string(const HashTable *trace, bool include_main);
ZEND_API ZEND_COLD zend_object *zend_create_unwind_exit(void);
diff --git a/ext/openssl/tests/ServerClientTestCase.inc b/ext/openssl/tests/ServerClientTestCase.inc
index f0336fdd392..c5db41d4841 100644
--- a/ext/openssl/tests/ServerClientTestCase.inc
+++ b/ext/openssl/tests/ServerClientTestCase.inc
@@ -100,7 +100,8 @@ class ServerClientTestCase
$ini = php_ini_loaded_file();
$cmd = sprintf(
'%s %s "%s" %s',
- PHP_BINARY, $ini ? "-n -c $ini" : "",
+ // XXX: TEST_PHP_EXTRA_ARGS for run-test values won't work here?
+ PHP_BINARY, $ini ? "-n -c $ini -d error_include_args=0" : "",
__FILE__,
WORKER_ARGV_VALUE
);
diff --git a/main/main.c b/main/main.c
index cc3f1cae258..6bda55ac874 100644
--- a/main/main.c
+++ b/main/main.c
@@ -62,6 +62,7 @@
#include "win32/php_registry.h"
#include "ext/standard/flock_compat.h"
#endif
+#include "Zend/zend_builtin_functions.h"
#include "Zend/zend_exceptions.h"
#if PHP_SIGCHILD
@@ -801,6 +802,7 @@ PHP_INI_BEGIN()
STD_PHP_INI_ENTRY_EX("display_errors", "1", PHP_INI_ALL, OnUpdateDisplayErrors, display_errors, php_core_globals, core_globals, display_errors_mode)
STD_PHP_INI_BOOLEAN("display_startup_errors", "1", PHP_INI_ALL, OnUpdateBool, display_startup_errors, php_core_globals, core_globals)
STD_PHP_INI_BOOLEAN("enable_dl", "1", PHP_INI_SYSTEM, OnUpdateBool, enable_dl, php_core_globals, core_globals)
+ STD_PHP_INI_BOOLEAN("error_include_args", "0", PHP_INI_ALL, OnUpdateBool, error_include_args, php_core_globals, core_globals)
STD_PHP_INI_BOOLEAN("expose_php", "1", PHP_INI_SYSTEM, OnUpdateBool, expose_php, php_core_globals, core_globals)
STD_PHP_INI_ENTRY("docref_root", "", PHP_INI_ALL, OnUpdateString, docref_root, php_core_globals, core_globals)
STD_PHP_INI_ENTRY("docref_ext", "", PHP_INI_ALL, OnUpdateString, docref_ext, php_core_globals, core_globals)
@@ -1132,7 +1134,14 @@ PHPAPI ZEND_COLD void php_verror(const char *docref, const char *params, int typ
/* if we still have memory then format the origin */
if (is_function) {
- origin_len = spprintf(&origin, 0, "%s%s%s(%s)", class_name, space, function, params);
+ zend_string *dynamic_params = NULL;
+ if (PG(error_include_args)) {
+ dynamic_params = zend_trace_current_function_args_string();
+ }
+ origin_len = spprintf(&origin, 0, "%s%s%s(%s)", class_name, space, function, dynamic_params ? ZSTR_VAL(dynamic_params) : params);
+ if (dynamic_params) {
+ zend_string_release(dynamic_params);
+ }
} else {
origin_len = strlen(function);
origin = estrndup(function, origin_len);
diff --git a/main/php_globals.h b/main/php_globals.h
index f6f57e0045c..8a032e9edb1 100644
--- a/main/php_globals.h
+++ b/main/php_globals.h
@@ -59,6 +59,7 @@ struct _php_core_globals {
uint8_t display_errors;
bool display_startup_errors;
+ bool error_include_args;
bool log_errors;
bool ignore_repeated_errors;
bool ignore_repeated_source;
diff --git a/php.ini-development b/php.ini-development
index 78ae50708d5..afabe74ba0e 100644
--- a/php.ini-development
+++ b/php.ini-development
@@ -611,6 +611,12 @@ ignore_repeated_source = Off
; Production Value: On
;fatal_error_backtraces = On
+; This directive controls whether PHP will print the actual arguments of a
+; function upon an error. If this is off (or there was an error fetching the
+; arguments), the function providing the error may optionally provide some
+; additional information after the problem function's name.
+;error_include_args = Off
+
;;;;;;;;;;;;;;;;;
; Data Handling ;
;;;;;;;;;;;;;;;;;
diff --git a/php.ini-production b/php.ini-production
index eb6880fe75d..04a7b699dad 100644
--- a/php.ini-production
+++ b/php.ini-production
@@ -613,6 +613,12 @@ ignore_repeated_source = Off
; Production Value: On
;fatal_error_backtraces = On
+; This directive controls whether PHP will print the actual arguments of a
+; function upon an error. If this is off (or there was an error fetching the
+; arguments), the function providing the error may optionally provide some
+; additional information after the problem function's name.
+;error_include_args = Off
+
;;;;;;;;;;;;;;;;;
; Data Handling ;
;;;;;;;;;;;;;;;;;
diff --git a/run-tests.php b/run-tests.php
index c08d07cdd7c..f5c7be8b4f4 100755
--- a/run-tests.php
+++ b/run-tests.php
@@ -273,6 +273,7 @@ function main(): void
'fatal_error_backtraces=Off',
'display_errors=1',
'display_startup_errors=1',
+ 'error_include_args=0',
'log_errors=0',
'html_errors=0',
'track_errors=0',
diff --git a/sapi/cli/tests/php_cli_server.inc b/sapi/cli/tests/php_cli_server.inc
index 3022022f894..3ad6ced5cb4 100644
--- a/sapi/cli/tests/php_cli_server.inc
+++ b/sapi/cli/tests/php_cli_server.inc
@@ -24,7 +24,8 @@ function php_cli_server_start(
file_put_contents($doc_root . '/' . ($router ?: 'index.php'), '<?php ' . $code . ' ?>');
}
- $cmd = [$php_executable, '-t', $doc_root, '-n', ...$cmd_args, '-S', 'localhost:0'];
+ // XXX: This should ideally use the same INI overrides as run-tests
+ $cmd = [$php_executable, '-d', 'error_include_args=0', '-t', $doc_root, '-n', ...$cmd_args, '-S', 'localhost:0'];
if (!is_null($router)) {
$cmd[] = $router;
}