From cf3b1347ad29f4203237c0be7e65395e987d81ed Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Tue, 16 Jun 2026 18:07:31 -0400 Subject: [PATCH] zend_ast: Escape control bytes in exported string literals The AST pretty-printer rendered string literals in single quotes, appending each byte verbatim. A literal containing a NUL kept the byte in the resulting string, but assert() hands that string to zend_throw_exception() as a const char*, so the failure message was truncated at the first NUL (the closing of the expression was lost). Render literals that contain a control byte double-quoted via zend_ast_export_qstr(), which escapes those bytes as octal. The embedded NUL no longer survives, so the message is complete, and the byte is shown in escaped form. Strings without control bytes are unchanged. Fixes GH-22290 --- NEWS | 2 ++ Zend/zend_ast.c | 26 +++++++++++++---- ext/standard/tests/assert/gh22290.phpt | 39 ++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 ext/standard/tests/assert/gh22290.phpt diff --git a/NEWS b/NEWS index afb45fca45ca..c3119d03c724 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,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 9df2320d5666..a39f8d30820c 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 000000000000..e519a60f5572 --- /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-- +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'))