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'))