Commit f3cf2f4527e for php.net
commit f3cf2f4527eb6d60a418ab5155c37c0fc568f9f0
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Tue Jun 16 18:07:31 2026 -0400
zend_ast: Escape control bytes in exported string literals
The AST pretty-printer single-quoted string literals and appended bytes
verbatim, so a NUL in a literal survived into the string assert() passes
to zend_throw_exception() as a const char*, truncating the failure
message at the first NUL. Export literals containing a control byte
double-quoted via zend_ast_export_qstr(), which escapes them as octal;
literals without control bytes are unchanged.
Fixes GH-22290
Closes GH-22350
diff --git a/NEWS b/NEWS
index 1a069963bd5..98f5bf7e718 100644
--- a/NEWS
+++ b/NEWS
@@ -11,6 +11,8 @@ PHP NEWS
- Core:
. Fixed bug GH-22280 (Incorrect compile error for goto to label preceding
try/finally block). (Pratik Bhujel)
+ . Fixed bug GH-22290 (AST pretty printing does not correctly handle strings
+ containing NUL). (iliaal)
- BCMath:
. Fixed issues with oversized allocations and signed overflow in bcround()
diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c
index 9df2320d566..a39f8d30820 100644
--- a/Zend/zend_ast.c
+++ b/Zend/zend_ast.c
@@ -1328,6 +1328,23 @@ static ZEND_COLD void zend_ast_export_qstr(smart_str *str, char quote, zend_stri
}
}
+static ZEND_COLD void zend_ast_export_quoted_str(smart_str *str, zend_string *s)
+{
+ size_t i;
+
+ for (i = 0; i < ZSTR_LEN(s); i++) {
+ if ((unsigned char) ZSTR_VAL(s)[i] < ' ') {
+ smart_str_appendc(str, '"');
+ zend_ast_export_qstr(str, '"', s);
+ smart_str_appendc(str, '"');
+ return;
+ }
+ }
+ smart_str_appendc(str, '\'');
+ zend_ast_export_str(str, s);
+ smart_str_appendc(str, '\'');
+}
+
static ZEND_COLD void zend_ast_export_indent(smart_str *str, int indent)
{
while (indent > 0) {
@@ -1612,9 +1629,7 @@ static ZEND_COLD void zend_ast_export_zval(smart_str *str, zval *zv, int priorit
str, Z_DVAL_P(zv), (int) EG(precision), /* zero_fraction */ true);
break;
case IS_STRING:
- smart_str_appendc(str, '\'');
- zend_ast_export_str(str, Z_STR_P(zv));
- smart_str_appendc(str, '\'');
+ zend_ast_export_quoted_str(str, Z_STR_P(zv));
break;
case IS_ARRAY: {
zend_long idx;
@@ -1629,9 +1644,8 @@ static ZEND_COLD void zend_ast_export_zval(smart_str *str, zval *zv, int priorit
smart_str_appends(str, ", ");
}
if (key) {
- smart_str_appendc(str, '\'');
- zend_ast_export_str(str, key);
- smart_str_appends(str, "' => ");
+ zend_ast_export_quoted_str(str, key);
+ smart_str_appends(str, " => ");
} else {
smart_str_append_long(str, idx);
smart_str_appends(str, " => ");
diff --git a/ext/standard/tests/assert/gh22290.phpt b/ext/standard/tests/assert/gh22290.phpt
new file mode 100644
index 00000000000..e519a60f557
--- /dev/null
+++ b/ext/standard/tests/assert/gh22290.phpt
@@ -0,0 +1,39 @@
+--TEST--
+GH-22290: AST pretty printing does not correctly handle strings containing NUL
+--INI--
+zend.assertions=1
+assert.exception=1
+--FILE--
+<?php
+
+try {
+ $string = "Foo\x00bar";
+ assert(!str_contains($string, "\x00"));
+} catch (AssertionError $e) {
+ echo $e->getMessage(), PHP_EOL;
+}
+
+try {
+ assert(["a\x00b" => 1] === []);
+} catch (AssertionError $e) {
+ echo $e->getMessage(), PHP_EOL;
+}
+
+try {
+ assert("tab\there" === "");
+} catch (AssertionError $e) {
+ echo $e->getMessage(), PHP_EOL;
+}
+
+try {
+ assert(str_contains("plain", "zzz"));
+} catch (AssertionError $e) {
+ echo $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+assert(!str_contains($string, "\000"))
+assert(["a\000b" => 1] === [])
+assert("tab\there" === '')
+assert(str_contains('plain', 'zzz'))