Commit 32402df2b56 for php.net
commit 32402df2b56b94d18bd4882a95b8e75f7a897db6
Author: Arnaud Le Blanc <365207+arnaud-lb@users.noreply.github.com>
Date: Mon Aug 3 16:34:27 2026 +0200
php_printf: introduce %pS to replace custom specifier %S (#22930)
The string formater supports custom format specifiers such as 'S' (zend_string*), but format strings using these specifiers do not pass the compiler's type checks that are performed on functions tagged with ZEND_ATTRIBUTE_FORMAT:
Zend/zend_compile.c: In function 'zend_compile_closure_binding':
Zend/zend_compile.c:8586:62: error: format '%S' expects argument of type 'wchar_t *', but argument 3 has type 'zend_string *' {aka 'struct _zend_string *'} [-Werror=format=]
8586 | zend_error_noreturn(E_COMPILE_ERROR, "Cannot use variable $%S twice", var_name);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~
| |
| zend_string * {aka struct _zend_string *}
As a result we can not use these specifiers without resorting to workarounds:
* Use variants of formatting functions that do not have ZEND_ATTRIBUTE_FORMAT [1]
* Or declare the format string separately [2]
Here I re-introduce %S as %pS. The compiler will only see a %p specifier followed by the ordinary literal character S, so it will be happy about an argument of type zend_string*.
This trick can be applied to more custom specifiers.
[1] https://github.com/php/php-src/blob/0b5d9801ec3b53e84388239a5b9f85d005318b64/Zend/zend_compile.c#L8586-L8587
[2] https://github.com/php/php-src/blob/edc169e7705d5e4411865e9be92b50e82be78f4e/Zend/zend_partial.c#L680
diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS
index 8b4b5912317..7bdbe08ce56 100644
--- a/UPGRADING.INTERNALS
+++ b/UPGRADING.INTERNALS
@@ -14,6 +14,19 @@ PHP 8.6 INTERNALS UPGRADE NOTES
1. Internal API changes
========================
+- Breaking changes:
+ . String formatting functions now support the custom conversion specifiers
+ 'pS' (zend_string*) and 'pp' (same as 'p'). Following the 'p' specifier with
+ an alpha-numeric character other than 'S' or 'p' is now an error.
+
+ Examples:
+
+ zend_string *str;
+ zend_spprintf("%pS", str); // valid, same as "%S"
+ zend_spprintf("%pp", str); // valid, same as "%p"
+ zend_spprintf("%pA", str); // invalid
+ zend_spprintf("%ppA", str); // valid, same as zend_spprintf("%p%c", str, 'A')
+
- Removed:
. The misnamed ZVAL_IS_NULL() has been removed. Use Z_ISNULL() instead.
. The zval_is_true() alias of zend_is_true() has been removed. Call
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index 088573a50ee..a2f126fb101 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -8580,8 +8580,8 @@ static void zend_compile_closure_binding(znode *closure, zend_op_array *op_array
value = zend_hash_add(op_array->static_variables, var_name, &EG(uninitialized_zval));
if (!value) {
- zend_error_noreturn_unchecked(E_COMPILE_ERROR,
- "Cannot use variable $%S twice", var_name);
+ zend_error_noreturn(E_COMPILE_ERROR,
+ "Cannot use variable $%pS twice", var_name);
}
CG(zend_lineno) = zend_ast_get_lineno(var_name_ast);
diff --git a/ext/sqlite3/sqlite3.c b/ext/sqlite3/sqlite3.c
index b81a0677a6c..57271a62942 100644
--- a/ext/sqlite3/sqlite3.c
+++ b/ext/sqlite3/sqlite3.c
@@ -1660,7 +1660,7 @@ static int php_sqlite3_bind_params(php_sqlite3_stmt *stmt_obj) /* {{{ */
break;
default:
- php_sqlite3_error(stmt_obj->db_obj, 0, "Unknown parameter type: %pd for parameter %pd", param->type, param->param_number);
+ php_sqlite3_error(stmt_obj->db_obj, 0, "Unknown parameter type: " ZEND_LONG_FMT " for parameter " ZEND_LONG_FMT, param->type, param->param_number);
return FAILURE;
}
} ZEND_HASH_FOREACH_END();
diff --git a/main/spprintf.c b/main/spprintf.c
index 6553853d810..8d6b80258a7 100644
--- a/main/spprintf.c
+++ b/main/spprintf.c
@@ -362,6 +362,7 @@ static void xbuf_format_converter(void *xbuf, bool is_char, const char *fmt, va_
break;
}
case 'S': {
+format_zend_string:;
zend_string *str = va_arg(ap, zend_string*);
s_len = ZSTR_LEN(str);
s = ZSTR_VAL(str);
@@ -665,6 +666,24 @@ static void xbuf_format_converter(void *xbuf, bool is_char, const char *fmt, va_
* we print "%p" to indicate that we don't handle "%p".
*/
case 'p':
+ /* %p[alnum]+ extensions */
+ switch (*(fmt+1)) {
+ case 'S':
+ /* zend_string* */
+ fmt++;
+ goto format_zend_string;
+ case 'p':
+ /* pointer */
+ fmt++;
+ break;
+ default:
+ if (isalnum(*(fmt+1))) {
+ zend_error_noreturn(E_CORE_ERROR,
+ "Invalid printf specifier \"p%c\"", *(fmt+1));
+ }
+ break;
+ }
+ /* Normal %p */
if (sizeof(char *) <= sizeof(uint64_t)) {
ui_num = (uint64_t)((size_t) va_arg(ap, char *));
s = ap_php_conv_p2(ui_num, 4, 'x',