Skip to content

Commit 19ca448

Browse files
committed
Fix GH-19653: Closure named argument unpacking between temporary closures can cause a crash
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. As for performance: This adds a load+compare for the function type, although we already load and compare the function type down below, so that impact on performance should be small. It also adds a load on the opcodes array, but we access fields from the same cacheline too so that should also have only a small impact.
1 parent 96c0bc5 commit 19ca448

File tree

2 files changed

+37
-4
lines changed

2 files changed

+37
-4
lines changed

Zend/tests/closures/gh19653.phpt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
--TEST--
2+
GH-19653 (Closure named argument unpacking between temporary closures can cause a crash)
3+
--CREDITS--
4+
ivan-u7n
5+
--FILE--
6+
<?php
7+
8+
9+
function func1(string $a1 = 'a1', string $a2 = 'a2', string $a3 = 'a3') {
10+
echo __FUNCTION__ . "() a1=$a1 a2=$a2 a3=$a3\n";
11+
}
12+
13+
function usage1(?Closure $func1 = null) {
14+
echo __FUNCTION__ . "() ";
15+
($func1 ?? func1(...))(a3: 'm3+');
16+
}
17+
usage1();
18+
19+
$func1 = function (string ...$args) {
20+
echo "[function] ";
21+
func1(...$args, a2: 'm2+');
22+
};
23+
usage1(func1: $func1);
24+
25+
?>
26+
--EXPECT--
27+
usage1() func1() a1=a1 a2=a2 a3=m3+
28+
usage1() [function] func1() a1=a1 a2=m2+ a3=m3+

Zend/zend_execute.c

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5058,7 +5058,12 @@ static zend_never_inline zend_result ZEND_FASTCALL zend_quick_check_constant(
50585058

50595059
static zend_always_inline uint32_t zend_get_arg_offset_by_name(
50605060
zend_function *fbc, zend_string *arg_name, void **cache_slot) {
5061-
if (EXPECTED(*cache_slot == fbc)) {
5061+
/* Due to closures, the `fbc` address isn't unique if the memory address is reused.
5062+
* So for user closures we need to distinguish using a unique key, while internal functions can't disappear
5063+
* and therefore can use the `fbc` address as unique key. */
5064+
void *unique_id = EXPECTED(fbc->type == ZEND_USER_FUNCTION) ? (void *) fbc->op_array.opcodes : (void *) fbc;
5065+
5066+
if (EXPECTED(*cache_slot == unique_id)) {
50625067
return *(uintptr_t *)(cache_slot + 1);
50635068
}
50645069

@@ -5069,7 +5074,7 @@ static zend_always_inline uint32_t zend_get_arg_offset_by_name(
50695074
for (uint32_t i = 0; i < num_args; i++) {
50705075
zend_arg_info *arg_info = &fbc->op_array.arg_info[i];
50715076
if (zend_string_equals(arg_name, arg_info->name)) {
5072-
*cache_slot = fbc;
5077+
*cache_slot = unique_id;
50735078
*(uintptr_t *)(cache_slot + 1) = i;
50745079
return i;
50755080
}
@@ -5079,15 +5084,15 @@ static zend_always_inline uint32_t zend_get_arg_offset_by_name(
50795084
zend_internal_arg_info *arg_info = &fbc->internal_function.arg_info[i];
50805085
size_t len = strlen(arg_info->name);
50815086
if (zend_string_equals_cstr(arg_name, arg_info->name, len)) {
5082-
*cache_slot = fbc;
5087+
*cache_slot = unique_id;
50835088
*(uintptr_t *)(cache_slot + 1) = i;
50845089
return i;
50855090
}
50865091
}
50875092
}
50885093

50895094
if (fbc->common.fn_flags & ZEND_ACC_VARIADIC) {
5090-
*cache_slot = fbc;
5095+
*cache_slot = unique_id;
50915096
*(uintptr_t *)(cache_slot + 1) = fbc->common.num_args;
50925097
return fbc->common.num_args;
50935098
}

0 commit comments

Comments
 (0)