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;
     }