Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
28 changes: 28 additions & 0 deletions Zend/tests/closures/gh19653.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
--TEST--
GH-19653 (Closure named argument unpacking between temporary closures can cause a crash)
--CREDITS--
ivan-u7n
--FILE--
<?php


function func1(string $a1 = 'a1', string $a2 = 'a2', string $a3 = 'a3') {
echo __FUNCTION__ . "() a1=$a1 a2=$a2 a3=$a3\n";
}

function usage1(?Closure $func1 = null) {
echo __FUNCTION__ . "() ";
($func1 ?? func1(...))(a3: 'm3+');
}
usage1();

$func1 = function (string ...$args) {
echo "[function] ";
func1(...$args, a2: 'm2+');
};
usage1(func1: $func1);

?>
--EXPECT--
usage1() func1() a1=a1 a2=a2 a3=m3+
usage1() [function] func1() a1=a1 a2=m2+ a3=m3+
13 changes: 9 additions & 4 deletions Zend/zend_execute.c
Original file line number Diff line number Diff line change
Expand Up @@ -5058,7 +5058,12 @@ static zend_never_inline zend_result ZEND_FASTCALL zend_quick_check_constant(

static zend_always_inline uint32_t zend_get_arg_offset_by_name(
zend_function *fbc, zend_string *arg_name, void **cache_slot) {
if (EXPECTED(*cache_slot == fbc)) {
/* Due to closures, the `fbc` address isn't unique if the memory address is reused.
* So for user closures we need to distinguish using a unique key, while internal functions can't disappear
* and therefore can use the `fbc` address as unique key. */
void *unique_id = EXPECTED(fbc->type == ZEND_USER_FUNCTION) ? (void *) fbc->op_array.opcodes : (void *) fbc;
Copy link
Member

Choose a reason for hiding this comment

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

fbc->op_array.opcodes is not unique unless ZEND_ACC_IMMUTABLE is set. E.g.

function usage1(Closure $c) {
    $c(a: 1);
}

usage1(eval('return function($a) { var_dump($a); };'));
usage1(eval('return function($b) { var_dump($b); };'));

Copy link
Member Author

Choose a reason for hiding this comment

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

Good (but unfortunate) find.
This should be a rare though. What about avoiding the use of a cache slot in the case ZEND_ACC_IMMUTABLE is unset?

Copy link
Member

@arnaud-lb arnaud-lb Sep 2, 2025

Choose a reason for hiding this comment

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

Yes this sounds good

Copy link
Member

@bwoebi bwoebi Sep 2, 2025

Choose a reason for hiding this comment

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

This also only is problematic for files and file-bound op_arrays. I.e. for anything named that's perfectly fine.

As a suggestion to avoid branching on fbc->type, you might use fbc->common.arg_info as key instead.

Avoiding the use of a cache slot when ZEND_ACC_IMMUTABLE is unset essentially disables the optimization for every closure call though, which is unfortunate (as they aren't immutable at all, only the prototype is).

Copy link
Member

@bwoebi bwoebi Sep 2, 2025

Choose a reason for hiding this comment

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

Ideally there would be a fn_flag which would indicate whether something is persistent or not.

Copy link
Member Author

Choose a reason for hiding this comment

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

fbc->common.arg_info can be shared among internal functions, no?

Copy link
Member Author

Choose a reason for hiding this comment

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

I suppose then it doesn't matter because functions always ahve the same argument ordering then

Copy link
Member Author

Choose a reason for hiding this comment

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

Avoiding the use of a cache slot when ZEND_ACC_IMMUTABLE is unset essentially disables the optimization for every closure call though, which is unfortunate (as they aren't immutable at all, only the prototype is).

Yeah okay this is no good.

Copy link
Member

Choose a reason for hiding this comment

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

We can check that fbc->op_array.refcount is NULL as a proxy for "op array is persistent", but that requires branching.

Copy link
Member Author

Choose a reason for hiding this comment

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

We can do the branching on the "slow" part, i.e. when we store the data into the cache slot, and use the arg_info as unique key. I believe that should work.
Just committed that.


if (EXPECTED(*cache_slot == unique_id)) {
return *(uintptr_t *)(cache_slot + 1);
}

Expand All @@ -5069,7 +5074,7 @@ static zend_always_inline uint32_t zend_get_arg_offset_by_name(
for (uint32_t i = 0; i < num_args; i++) {
zend_arg_info *arg_info = &fbc->op_array.arg_info[i];
if (zend_string_equals(arg_name, arg_info->name)) {
*cache_slot = fbc;
*cache_slot = unique_id;
*(uintptr_t *)(cache_slot + 1) = i;
return i;
}
Expand All @@ -5079,15 +5084,15 @@ static zend_always_inline uint32_t zend_get_arg_offset_by_name(
zend_internal_arg_info *arg_info = &fbc->internal_function.arg_info[i];
size_t len = strlen(arg_info->name);
if (zend_string_equals_cstr(arg_name, arg_info->name, len)) {
*cache_slot = fbc;
*cache_slot = unique_id;
*(uintptr_t *)(cache_slot + 1) = i;
return i;
}
}
}

if (fbc->common.fn_flags & ZEND_ACC_VARIADIC) {
*cache_slot = fbc;
*cache_slot = unique_id;
*(uintptr_t *)(cache_slot + 1) = fbc->common.num_args;
return fbc->common.num_args;
}
Expand Down
Loading