From d16717eb61a9b6fa660553b4a623a6ec0185ca3c Mon Sep 17 00:00:00 2001 From: Yurun Date: Thu, 23 Jul 2026 21:40:54 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(stub):=20=E4=BF=AE=E5=A4=8Dheredoc/nowd?= =?UTF-8?q?oc=E5=AD=97=E7=AC=A6=E4=B8=B2=E7=94=9F=E6=88=90=E9=9D=9E?= =?UTF-8?q?=E6=B3=95C++=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gen_stub.php | 17 ++++++++- .../const/class-const-heredoc-nowdoc.phpt | 23 +++++++++++ .../const/heredoc-nowdoc-const-defaults.phpt | 38 +++++++++++++++++++ 3 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 tests/compiler/const/class-const-heredoc-nowdoc.phpt create mode 100644 tests/compiler/const/heredoc-nowdoc-const-defaults.phpt diff --git a/src/gen_stub.php b/src/gen_stub.php index 5b65c0c5..ff1406a0 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -2515,14 +2515,20 @@ class EvaluatedValue return '"' . getTranslator()->escapeString((string) $this->value) . '"'; } elseif ($this->expr instanceof Expr\ConstFetch) { return getTranslator()->getConstValue($this->expr->name->toString()); - } elseif (!($this->expr instanceof String_)) { + } elseif ($this->expr instanceof String_) { + // Heredoc/nowdoc and quoted string literals: emit the decoded + // value directly. Pretty-printing a heredoc/nowdoc would leak + // the `<<escapeString((string) $this->value) . '"'; + } else { // ConstExprEvaluator has already reduced concatenations and // other constant string expressions to their PHP value. Emit // that value as a C string literal instead of rejecting every // non-literal string expression. return '"' . getTranslator()->escapeString((string) $this->value) . '"'; } - $expr = preg_replace("/(^'|'$)/", '"', getTranslator()->escapeString($expr)); } elseif ($this->type->isInt() or $this->type->isFloat()) { return strval($this->value); } elseif ($this->type->isBool()) { @@ -5107,6 +5113,13 @@ function parseFunctionLike( if ($param->default instanceof Expr\ClassConstFetch && $param->default->class->toLowerString() === "self") { $defaultValue = getTranslator()->getClassConstValue($func, $name->className->name, $param->default->name->name); $defaultValue = var_export($defaultValue, true); + } elseif ($param->default instanceof String_ && + in_array($param->default->getAttribute('kind'), [String_::KIND_HEREDOC, String_::KIND_NOWDOC], true) + ) { + // heredoc/nowdoc: prettyPrint 会输出 `<<escapeString((string) $param->default->value) . '"'; } else { $defaultValue = $param->default ? $prettyPrinter->prettyPrintExpr($param->default) : null; } diff --git a/tests/compiler/const/class-const-heredoc-nowdoc.phpt b/tests/compiler/const/class-const-heredoc-nowdoc.phpt new file mode 100644 index 00000000..350c1ad7 --- /dev/null +++ b/tests/compiler/const/class-const-heredoc-nowdoc.phpt @@ -0,0 +1,23 @@ +--TEST-- +class constants with heredoc and nowdoc syntax +--FILE-- + +--EXPECT-- +string(3) "abc" +string(3) "def" diff --git a/tests/compiler/const/heredoc-nowdoc-const-defaults.phpt b/tests/compiler/const/heredoc-nowdoc-const-defaults.phpt new file mode 100644 index 00000000..07868984 --- /dev/null +++ b/tests/compiler/const/heredoc-nowdoc-const-defaults.phpt @@ -0,0 +1,38 @@ +--TEST-- +global constants, property defaults and parameter defaults with heredoc/nowdoc syntax +--FILE-- +p); + var_dump(with_default()); +} +?> +--EXPECT-- +string(3) "abc" +string(3) "def" +string(3) "xyz" +string(3) "abc" From 3211be2833bcba4c97a41280edb0c5f17dd1a335 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 24 Jul 2026 18:22:18 +0800 Subject: [PATCH 2/2] fix(stub): harden heredoc and nowdoc C++ escaping --- src/gen_stub.php | 38 ++++++++----------- .../const/class-const-heredoc-nowdoc.phpt | 10 ++--- .../const/heredoc-nowdoc-const-defaults.phpt | 21 ++++++++-- 3 files changed, 38 insertions(+), 31 deletions(-) diff --git a/src/gen_stub.php b/src/gen_stub.php index ff1406a0..9dcce8fb 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -834,7 +834,10 @@ class ArgInfo { private function getDefaultValueAsArginfoString(): string { if ($this->hasProperDefaultValue()) { - return '"' . addslashes($this->defaultValue) . '"'; + // The default value is a PHP expression embedded in a C string. + // Escape for the outer C layer only; addslashes() leaves line + // breaks and other control bytes untouched, producing invalid C++. + return '"' . getTranslator()->escapeString($this->defaultValue) . '"'; } return "NULL"; @@ -2485,7 +2488,9 @@ class EvaluatedValue if ($forStringDef === '') { $forStringDef = "{$zvalName}_str"; } - $code .= "\tzend_string *$forStringDef = zend_string_init($cExpr, strlen($cExpr), 1);\n"; + // getCExpr() emits a C string literal here. sizeof() preserves + // embedded NUL bytes, unlike strlen(). + $code .= "\tzend_string *$forStringDef = zend_string_init($cExpr, sizeof($cExpr) - 1, 1);\n"; $code .= "\tZVAL_STR(&$zvalName, $forStringDef);\n"; } } elseif ($this->type->isArray()) { @@ -2515,20 +2520,12 @@ class EvaluatedValue return '"' . getTranslator()->escapeString((string) $this->value) . '"'; } elseif ($this->expr instanceof Expr\ConstFetch) { return getTranslator()->getConstValue($this->expr->name->toString()); - } elseif ($this->expr instanceof String_) { - // Heredoc/nowdoc and quoted string literals: emit the decoded - // value directly. Pretty-printing a heredoc/nowdoc would leak - // the `<<escapeString((string) $this->value) . '"'; - } else { - // ConstExprEvaluator has already reduced concatenations and - // other constant string expressions to their PHP value. Emit - // that value as a C string literal instead of rejecting every - // non-literal string expression. - return '"' . getTranslator()->escapeString((string) $this->value) . '"'; } + + // ConstExprEvaluator has already decoded literal syntax and + // reduced constant string expressions. Emitting that value avoids + // leaking heredoc/nowdoc source syntax into generated C++. + return '"' . getTranslator()->escapeString((string) $this->value) . '"'; } elseif ($this->type->isInt() or $this->type->isFloat()) { return strval($this->value); } elseif ($this->type->isBool()) { @@ -5113,13 +5110,10 @@ function parseFunctionLike( if ($param->default instanceof Expr\ClassConstFetch && $param->default->class->toLowerString() === "self") { $defaultValue = getTranslator()->getClassConstValue($func, $name->className->name, $param->default->name->name); $defaultValue = var_export($defaultValue, true); - } elseif ($param->default instanceof String_ && - in_array($param->default->getAttribute('kind'), [String_::KIND_HEREDOC, String_::KIND_NOWDOC], true) - ) { - // heredoc/nowdoc: prettyPrint 会输出 `<<escapeString((string) $param->default->value) . '"'; + } elseif ($param->default instanceof String_) { + // Keep this as a PHP expression. ArgInfo escapes the expression + // separately when embedding it in generated C++. + $defaultValue = var_export($param->default->value, true); } else { $defaultValue = $param->default ? $prettyPrinter->prettyPrintExpr($param->default) : null; } diff --git a/tests/compiler/const/class-const-heredoc-nowdoc.phpt b/tests/compiler/const/class-const-heredoc-nowdoc.phpt index 350c1ad7..28c5d6a5 100644 --- a/tests/compiler/const/class-const-heredoc-nowdoc.phpt +++ b/tests/compiler/const/class-const-heredoc-nowdoc.phpt @@ -6,18 +6,18 @@ class constants with heredoc and nowdoc syntax class Test { const VALUE1 = << --EXPECT-- -string(3) "abc" -string(3) "def" +string(60) "71756f7465202220736c617368205c206e756c2000207461622009203f3f" +string(54) "2476616c7565203f3f202271756f74656422205c6e205c70617468" diff --git a/tests/compiler/const/heredoc-nowdoc-const-defaults.phpt b/tests/compiler/const/heredoc-nowdoc-const-defaults.phpt index 07868984..3dc5192b 100644 --- a/tests/compiler/const/heredoc-nowdoc-const-defaults.phpt +++ b/tests/compiler/const/heredoc-nowdoc-const-defaults.phpt @@ -17,8 +17,14 @@ class WithProp ABC; } -function with_default(string $x = <<p); - var_dump(with_default()); + var_dump(bin2hex(with_default()), bin2hex(with_binary_default())); + + $default = (new ReflectionFunction('with_default'))->getParameters()[0]->getDefaultValue(); + $binaryDefault = (new ReflectionFunction('with_binary_default'))->getParameters()[0]->getDefaultValue(); + var_dump(bin2hex($default), bin2hex($binaryDefault)); } ?> --EXPECT-- string(3) "abc" string(3) "def" string(3) "xyz" -string(3) "abc" +string(54) "2476616c7565203f3f202271756f74656422205c6e205c70617468" +string(6) "410042" +string(54) "2476616c7565203f3f202271756f74656422205c6e205c70617468" +string(6) "410042"