Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 72 additions & 67 deletions Zend/zend_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -829,7 +829,7 @@ ZEND_API bool ZEND_FASTCALL zend_parse_arg_str_or_long_slow(zval *arg, zend_stri
}
/* }}} */

static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec, char **error, uint32_t arg_num) /* {{{ */
static zend_expected_type zend_parse_arg_impl(zval *arg, va_list *va, const char **spec, char **error, uint32_t arg_num) /* {{{ */
{
const char *spec_walk = *spec;
char c = *spec_walk++;
Expand Down Expand Up @@ -863,7 +863,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
}

if (!zend_parse_arg_long(arg, p, is_null, check_null, arg_num)) {
return check_null ? "?int" : "int";
return check_null ? Z_EXPECTED_LONG_OR_NULL : Z_EXPECTED_LONG;
}
}
break;
Expand All @@ -878,7 +878,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
}

if (!zend_parse_arg_double(arg, p, is_null, check_null, arg_num)) {
return check_null ? "?float" : "float";
return check_null ? Z_EXPECTED_DOUBLE_OR_NULL : Z_EXPECTED_DOUBLE;
}
}
break;
Expand All @@ -888,7 +888,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
zval **p = va_arg(*va, zval **);

if (!zend_parse_arg_number(arg, p, check_null, arg_num)) {
return check_null ? "int|float|null" : "int|float";
return check_null ? Z_EXPECTED_NUMBER_OR_NULL : Z_EXPECTED_NUMBER;
}
}
break;
Expand All @@ -898,7 +898,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
char **p = va_arg(*va, char **);
size_t *pl = va_arg(*va, size_t *);
if (!zend_parse_arg_string(arg, p, pl, check_null, arg_num)) {
return check_null ? "?string" : "string";
return check_null ? Z_EXPECTED_STRING_OR_NULL : Z_EXPECTED_STRING;
}
}
break;
Expand All @@ -908,12 +908,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
char **p = va_arg(*va, char **);
size_t *pl = va_arg(*va, size_t *);
if (!zend_parse_arg_path(arg, p, pl, check_null, arg_num)) {
if (Z_TYPE_P(arg) == IS_STRING) {
zend_spprintf(error, 0, "must not contain any null bytes");
return "";
} else {
return check_null ? "?string" : "string";
}
Comment on lines -911 to -916

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I understand correct that this was just so that the user is informed about NUL bytes in their path string? Because the NUL bytes are already handled in zend_str_has_nul_byte but the error message would be generic, right? Now, you handle it with zend_wrong_parameter_type_error.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. This is the purpose of this ZPP specifier, and has always been this way.

You get the same error message either way, fast ZPP only uses zend_wrong_parameter_type_error to generate error messages.

@kamil-tekiela kamil-tekiela Aug 7, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, but one thing I don't understand. Previously if (Z_TYPE_P(arg) == IS_STRING) { was after ZVAL_DEREF(arg) but now in the zend_wrong_parameter_type_error, you have this check but I cannot find any ZVAL_DEREF(arg) up the call stack. Am I missing something?

Do we have a unit test for something like this:

$s = "abc\0def";
$ref = &$s;
fopen($ref, 'r'); 

EDIT: So I asked Claude to explain this to me and it told me that it's not possible for zend_parse_arg/zend_parse_arg_impl to receive a reference, so there is no need for dereference checks. I can't say that I understand why it's in one place and not in the other, but at least it doesn't cause a regression.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not interested in a "review" from Claude.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify, to avoid any misunderstanding, the review is mine. I only used Claude to help me understand the code. I am genuinely asking why we have ZVAL_DEREF(arg) but not in the other place.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a ZVAL_DEREF() at the top of this function. I don't understand what you are struggling with.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if I was not clear. Let me try to express my concern better.

On line 841 is a ZVAL_DEREF(arg); which you did not remove. I thought that it was crucial for if (Z_TYPE_P(arg) == IS_STRING) { to work when a reference was passed as an argument. Now, the check is effectively moved out of zend_parse_arg_impl and into zend_wrong_parameter_type_error, which is called directly from zend_parse_arg. On line 282, the check is there again, but this time, there is no ZVAL_DEREF(arg); in zend_wrong_parameter_type_error nor in zend_parse_arg.

I tested, and I don't think anything's broken, so it's probably just me being confused. This is why I asked the question. Shouldn't the ZVAL_DEREF(arg); live in zend_parse_arg or be duplicated in zend_wrong_parameter_type_error? I guess what I am trying to understand is whether ZVAL_DEREF(arg) changes the argument or only the local copy? As I understand it, it only changes the local copy, so IMHO, it probably should be moved from line 841 to line 1077.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I confirm the issue.

It's not an issue for fopen() because the arg is not by-ref, so the VM derefs during argument passing.

However it would be an issue if we had a function taking a path by-ref, as zend_wrong_parameter_type_error() sees a reference in this case. This leads to an error message like:

Argument #1 ($path) must be of type string, string given

I tested by adding a function in ext/zend_test.

@arnaud-lb arnaud-lb Aug 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c
index a880c09fc1f..fe670890f7d 100644
--- a/ext/zend_test/test.c
+++ b/ext/zend_test/test.c
@@ -1969,3 +1969,14 @@ static PHP_FUNCTION(zend_test_gh19792)
        zend_error(E_WARNING, "a warning");
        zend_throw_error(NULL, "an exception");
 }
+
+static PHP_FUNCTION(zend_test_recv_path_by_ref_old_zpp)
+{
+       zend_string *path;
+
+       if (zend_parse_parameters(ZEND_NUM_ARGS(), "p", &path) == FAILURE) {
+               RETURN_THROWS();
+       }
+
+       RETURN_STR(path);
+}
diff --git a/ext/zend_test/test.stub.php b/ext/zend_test/test.stub.php
index 7c96ea176a8..6c8ffd4ca08 100644
--- a/ext/zend_test/test.stub.php
+++ b/ext/zend_test/test.stub.php
@@ -392,6 +392,8 @@ function zend_test_uri_parser(string $uri, string $parser): array { }
 
     /** @compile-time-eval */
     function zend_test_gh19792(): void {}
+
+    function zend_test_recv_path_by_ref_old_zpp(string &$path): void {}
 }
 
 namespace ZendTestNS {
$ php -r '$a = "foo\0bar"; zend_test_recv_path_by_ref_old_zpp($a);'

Fatal error: Uncaught TypeError: zend_test_recv_path_by_ref_old_zpp(): Argument #1 ($path) must be of type string, string given in Command line code:1

return check_null ? Z_EXPECTED_PATH_OR_NULL : Z_EXPECTED_PATH;
}
}
break;
Expand All @@ -922,12 +917,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
{
zend_string **str = va_arg(*va, zend_string **);
if (!zend_parse_arg_path_str(arg, str, check_null, arg_num)) {
if (Z_TYPE_P(arg) == IS_STRING) {
zend_spprintf(error, 0, "must not contain any null bytes");
return "";
} else {
return check_null ? "?string" : "string";
}
return check_null ? Z_EXPECTED_PATH_OR_NULL : Z_EXPECTED_PATH;
}
}
break;
Expand All @@ -936,7 +926,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
{
zend_string **str = va_arg(*va, zend_string **);
if (!zend_parse_arg_str(arg, str, check_null, arg_num)) {
return check_null ? "?string" : "string";
return check_null ? Z_EXPECTED_STRING_OR_NULL : Z_EXPECTED_STRING;
}
}
break;
Expand All @@ -951,7 +941,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
}

if (!zend_parse_arg_bool(arg, p, is_null, check_null, arg_num)) {
return check_null ? "?bool" : "bool";
return check_null ? Z_EXPECTED_BOOL_OR_NULL : Z_EXPECTED_BOOL;
}
}
break;
Expand All @@ -961,7 +951,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
zval **p = va_arg(*va, zval **);

if (!zend_parse_arg_resource(arg, p, check_null)) {
return check_null ? "resource or null" : "resource";
return check_null ? Z_EXPECTED_RESOURCE_OR_NULL : Z_EXPECTED_RESOURCE;
}
}
break;
Expand All @@ -972,7 +962,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
zval **p = va_arg(*va, zval **);

if (!zend_parse_arg_array(arg, p, check_null, c == 'A')) {
return check_null ? "?array" : "array";
return check_null ? Z_EXPECTED_ARRAY_OR_NULL : Z_EXPECTED_ARRAY;
}
}
break;
Expand All @@ -983,7 +973,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
HashTable **p = va_arg(*va, HashTable **);

if (!zend_parse_arg_array_ht(arg, p, check_null, c == 'H', separate)) {
return check_null ? "?array" : "array";
return check_null ? Z_EXPECTED_ARRAY_OR_NULL : Z_EXPECTED_ARRAY;
}
}
break;
Expand All @@ -993,7 +983,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
zval **p = va_arg(*va, zval **);

if (!zend_parse_arg_object(arg, p, NULL, check_null)) {
return check_null ? "?object" : "object";
return check_null ? Z_EXPECTED_OBJECT_OR_NULL : Z_EXPECTED_OBJECT;
}
}
break;
Expand All @@ -1005,50 +995,42 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec

if (!zend_parse_arg_object(arg, p, ce, check_null)) {
if (ce) {
if (check_null) {
zend_spprintf(error, 0, "must be of type ?%s, %s given", ZSTR_VAL(ce->name), zend_zval_value_name(arg));
return "";
} else {
return ZSTR_VAL(ce->name);
}
} else {
return check_null ? "?object" : "object";
*error = ZSTR_VAL(ce->name);
}
return check_null ? Z_EXPECTED_OBJECT_OR_NULL : Z_EXPECTED_OBJECT;
}
}
break;

case 'C':
{
zend_class_entry *lookup, **pce = va_arg(*va, zend_class_entry **);
zend_class_entry *ce_base = *pce;
zend_class_entry **pce = va_arg(*va, zend_class_entry **);
const zend_class_entry *ce_base = *pce;

if (check_null && Z_TYPE_P(arg) == IS_NULL) {
*pce = NULL;
break;
}
if (!try_convert_to_string(arg)) {
*pce = NULL;
return ""; /* try_convert_to_string() throws an exception */
}

if ((lookup = zend_lookup_class(Z_STR_P(arg))) == NULL) {
zend_string *class_name = NULL;
if (!zend_parse_arg_str(arg, &class_name, check_null, arg_num)) {
*pce = NULL;
} else {
*pce = lookup;
return check_null ? Z_EXPECTED_STRING_OR_NULL : Z_EXPECTED_STRING;
}

*pce = zend_lookup_class(class_name);
if (ce_base) {
if ((!*pce || !instanceof_function(*pce, ce_base))) {
zend_spprintf(error, 0, "must be a class name derived from %s%s, %s given",
ZSTR_VAL(ce_base->name), check_null ? " or null" : "", Z_STRVAL_P(arg));
*pce = NULL;
return "";
return check_null ? Z_EXPECTED_OBJECT_OR_CLASS_NAME_OR_NULL : Z_EXPECTED_OBJECT_OR_CLASS_NAME;
}
}
if (!*pce) {
zend_spprintf(error, 0, "must be a valid class name%s, %s given",
check_null ? " or null" : "", Z_STRVAL_P(arg));
return "";
return check_null ? Z_EXPECTED_OBJECT_OR_CLASS_NAME_OR_NULL : Z_EXPECTED_OBJECT_OR_CLASS_NAME;
}
break;

Expand All @@ -1060,19 +1042,11 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
{
zend_fcall_info *fci = va_arg(*va, zend_fcall_info *);
zend_fcall_info_cache *fcc = va_arg(*va, zend_fcall_info_cache *);
char *is_callable_error = NULL;
if (EXPECTED(zend_parse_arg_func(arg, fci, fcc, check_null, &is_callable_error, c == 'f'))) {
ZEND_ASSERT(!is_callable_error);
if (EXPECTED(zend_parse_arg_func(arg, fci, fcc, check_null, error, c == 'f'))) {
ZEND_ASSERT(!*error);
Comment on lines +1045 to +1046

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit / not for this PR: Both zend_parse_arg_func() and zend_is_callable_at_frame() zero *error. I'm wondering if we could move this responsibility to the caller. In the case of zend_parse_arg_impl(), *error is always NULL before calling zend_parse_arg_func() and zend_is_callable_at_frame().

break;
}

if (is_callable_error) {
zend_spprintf(error, 0, "must be a valid callback%s, %s", check_null ? " or null" : "", is_callable_error);
efree(is_callable_error);
return "";
} else {
return check_null ? "a valid callback or null" : "a valid callback";
}
return check_null ? Z_EXPECTED_FUNC_OR_NULL : Z_EXPECTED_FUNC;
}

case 'z':
Expand All @@ -1088,37 +1062,68 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
ZEND_ASSERT(0 && "ZPP modifier no longer supported");
ZEND_FALLTHROUGH;
default:
return "unknown";
ZEND_ASSERT(false && "Unknown ZPP modifier");
}

*spec = spec_walk;

return NULL;
return Z_EXPECTED_LAST;
}
/* }}} */

static zend_result zend_parse_arg(uint32_t arg_num, zval *arg, va_list *va, const char **spec, int flags) /* {{{ */
{
const char *expected_type = NULL;
char *error = NULL;

expected_type = zend_parse_arg_impl(arg, va, spec, &error, arg_num);
if (expected_type) {
zend_expected_type expected_type = zend_parse_arg_impl(arg, va, spec, &error, arg_num);
if (expected_type != Z_EXPECTED_LAST) {
if (EG(exception)) {
return FAILURE;
}
if (!(flags & ZEND_PARSE_PARAMS_QUIET) && (*expected_type || error)) {

if (!(flags & ZEND_PARSE_PARAMS_QUIET)) {
/* More complex error, can only happen for:
* Objects of a specific class
* Z_EXPECTED_OBJECT
* Z_EXPECTED_OBJECT_OR_NULL
* Class names
* Z_EXPECTED_OBJECT_OR_CLASS_NAME
* Z_EXPECTED_OBJECT_OR_CLASS_NAME_OR_NULL
* Functions
* Z_EXPECTED_FUNC
* Z_EXPECTED_FUNC_OR_NULL
*/
if (error) {
if (strcmp(error, "must not contain any null bytes") == 0) {
zend_argument_value_error(arg_num, "%s", error);
} else {
zend_argument_type_error(arg_num, "%s", error);
switch (expected_type) {
case Z_EXPECTED_OBJECT:
/* DO NOT FREE error: it's a pointer to ZSTR_VAL(ce->name) */

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit / not for this PR: We should unify ownership of error and maybe make it a zend_string*.

zend_wrong_parameter_class_error(arg_num, error, arg);
break;
case Z_EXPECTED_OBJECT_OR_NULL:
/* DO NOT FREE error: it's a pointer to ZSTR_VAL(ce->name) */
zend_wrong_parameter_class_or_null_error(arg_num, error, arg);
break;
case Z_EXPECTED_FUNC:
/* error is freed by zend_wrong_callback_error() */
zend_wrong_callback_error(arg_num, error);
break;
case Z_EXPECTED_FUNC_OR_NULL:
/* error is freed by zend_wrong_callback_or_null_error() */
zend_wrong_callback_or_null_error(arg_num, error);
break;
case Z_EXPECTED_OBJECT_OR_CLASS_NAME:
case Z_EXPECTED_OBJECT_OR_CLASS_NAME_OR_NULL:
zend_argument_type_error(arg_num, "%s", error);
efree(error);
break;
default:
ZEND_UNREACHABLE();
}
efree(error);
} else {
zend_argument_type_error(arg_num, "must be of type %s, %s given", expected_type, zend_zval_value_name(arg));
}
} else if (error) {
zend_wrong_parameter_type_error(arg_num, expected_type, arg);
} else if (error
/* DO NOT FREE error when it's a pointer to ZSTR_VAL(ce->name) */
&& expected_type != Z_EXPECTED_OBJECT && expected_type != Z_EXPECTED_OBJECT_OR_NULL) {
efree(error);
}

Expand Down
2 changes: 1 addition & 1 deletion ext/mysqli/tests/mysqli_fetch_object_oo.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ require_once 'skipifconnectfailure.inc';
require_once 'clean_table.inc';
?>
--EXPECT--
Error: Object of class mysqli could not be converted to string
TypeError: mysqli_result::fetch_object(): Argument #1 ($class) must be of type string, mysqli given
ArgumentCountError: mysqli_result::fetch_object() expects at most 2 arguments, 3 given
TypeError: mysqli_result::fetch_object(): Argument #2 ($constructor_args) must be of type array, null given
ArgumentCountError: Too few arguments to function mysqli_fetch_object_construct::__construct(), 1 passed and exactly 2 expected
Expand Down
Loading