Skip to content

Commit 1e4375d

Browse files
committed
Fix GH-22878: Use-after-free of callable via autoloader
Validating an array or string callable runs user code before its borrowed method name and object are used: a string class name can trigger an autoloader, and a compound "Class::method" name emits an E_DEPRECATED that reaches a user error handler. Either can free or mutate the callable, leaving the method string and $this dangling. Copy the method string before the reentrant lookup, and hold the callable array across INIT_USER_CALL's validation and frame build so the object survives to the call. This also covers call_user_func_array(), $cb(), and referenced or reference-wrapped array members. Fixes GH-22878
1 parent 68d605f commit 1e4375d

6 files changed

Lines changed: 133 additions & 4 deletions

File tree

NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ PHP NEWS
22
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
33
?? ??? ????, PHP 8.4.24
44

5+
- Core:
6+
. Fixed bug GH-22878 (Use-after-free of callable via autoloader). (iliaal)
7+
58
- Date:
69
. Fixed leak on double DatePeriod::__construct() call. (ilutov)
710

Zend/tests/gh22878.phpt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
--TEST--
2+
GH-22878 (Use-after-free of callable via autoloader)
3+
--FILE--
4+
<?php
5+
spl_autoload_register(function (string $class): void {
6+
$GLOBALS['cb'] = null;
7+
eval("class $class { public static function __callStatic(\$name, \$args) { echo strlen(\$name), \"\\n\"; } }");
8+
});
9+
10+
$method = str_repeat('m', 256);
11+
$cb = ['GH22878ArrayCuf', $method];
12+
unset($method);
13+
call_user_func($cb);
14+
15+
$method = str_repeat('m', 256);
16+
$cb = ['GH22878ArrayCufa', $method];
17+
unset($method);
18+
call_user_func_array($cb, []);
19+
20+
$method = str_repeat('m', 256);
21+
$cb = ['GH22878ArrayDynamic', $method];
22+
unset($method);
23+
$cb();
24+
25+
$method = str_repeat('m', 256);
26+
$cb = 'GH22878Str::' . $method;
27+
unset($method);
28+
call_user_func($cb);
29+
30+
$cb = 'GH22878Lit::literalMethod';
31+
call_user_func($cb);
32+
33+
echo "done\n";
34+
?>
35+
--EXPECT--
36+
256
37+
256
38+
256
39+
256
40+
13
41+
done

Zend/zend_API.c

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4179,6 +4179,9 @@ ZEND_API bool zend_is_callable_at_frame(
41794179
bool ret;
41804180
zend_fcall_info_cache fcc_local;
41814181
bool strict_class = 0;
4182+
zval callable_copy;
4183+
4184+
ZVAL_UNDEF(&callable_copy);
41824185

41834186
if (fcc == NULL) {
41844187
fcc = &fcc_local;
@@ -4206,11 +4209,17 @@ ZEND_API bool zend_is_callable_at_frame(
42064209
return 1;
42074210
}
42084211

4212+
ZVAL_COPY(&callable_copy, callable);
4213+
callable = &callable_copy;
4214+
42094215
check_func:
42104216
ret = zend_is_callable_check_func(callable, frame, fcc, strict_class, error, check_flags & IS_CALLABLE_SUPPRESS_DEPRECATIONS);
42114217
if (fcc == &fcc_local) {
42124218
zend_release_fcall_info_cache(fcc);
42134219
}
4220+
if (!Z_ISUNDEF(callable_copy)) {
4221+
zval_ptr_dtor(&callable_copy);
4222+
}
42144223
return ret;
42154224

42164225
case IS_ARRAY:
@@ -4244,7 +4253,11 @@ ZEND_API bool zend_is_callable_at_frame(
42444253
return 1;
42454254
}
42464255

4256+
ZVAL_COPY(&callable_copy, method);
4257+
callable = &callable_copy;
4258+
42474259
if (!zend_is_callable_check_class(Z_STR_P(obj), get_scope(frame), frame, fcc, &strict_class, error, check_flags & IS_CALLABLE_SUPPRESS_DEPRECATIONS)) {
4260+
zval_ptr_dtor(&callable_copy);
42484261
return 0;
42494262
}
42504263
} else {
@@ -4256,9 +4269,11 @@ ZEND_API bool zend_is_callable_at_frame(
42564269
fcc->called_scope = fcc->calling_scope;
42574270
return 1;
42584271
}
4272+
4273+
ZVAL_COPY(&callable_copy, method);
4274+
callable = &callable_copy;
42594275
}
42604276

4261-
callable = method;
42624277
goto check_func;
42634278
}
42644279
return 0;

Zend/zend_execute.c

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5008,23 +5008,27 @@ static zend_never_inline zend_execute_data *zend_init_dynamic_call_array(zend_ar
50085008
}
50095009

50105010
if (Z_TYPE_P(obj) == IS_STRING) {
5011+
zend_string *method_name = zend_string_copy(Z_STR_P(method));
50115012
zend_class_entry *called_scope = zend_fetch_class_by_name(Z_STR_P(obj), NULL, ZEND_FETCH_CLASS_DEFAULT | ZEND_FETCH_CLASS_EXCEPTION);
50125013

50135014
if (UNEXPECTED(called_scope == NULL)) {
5015+
zend_string_release(method_name);
50145016
return NULL;
50155017
}
50165018

50175019
if (called_scope->get_static_method) {
5018-
fbc = called_scope->get_static_method(called_scope, Z_STR_P(method));
5020+
fbc = called_scope->get_static_method(called_scope, method_name);
50195021
} else {
5020-
fbc = zend_std_get_static_method(called_scope, Z_STR_P(method), NULL);
5022+
fbc = zend_std_get_static_method(called_scope, method_name, NULL);
50215023
}
50225024
if (UNEXPECTED(fbc == NULL)) {
50235025
if (EXPECTED(!EG(exception))) {
5024-
zend_undefined_method(called_scope, Z_STR_P(method));
5026+
zend_undefined_method(called_scope, method_name);
50255027
}
5028+
zend_string_release(method_name);
50265029
return NULL;
50275030
}
5031+
zend_string_release(method_name);
50285032
if (!(fbc->common.fn_flags & ZEND_ACC_STATIC)) {
50295033
zend_non_static_method_call(fbc);
50305034
if (fbc->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) {

Zend/zend_vm_def.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3906,9 +3906,16 @@ ZEND_VM_HANDLER(118, ZEND_INIT_USER_CALL, CONST, CONST|TMPVAR|CV, NUM)
39063906
void *object_or_called_scope;
39073907
zend_execute_data *call;
39083908
uint32_t call_info = ZEND_CALL_NESTED_FUNCTION | ZEND_CALL_DYNAMIC;
3909+
zval held_callable;
39093910

39103911
SAVE_OPLINE();
39113912
function_name = GET_OP2_ZVAL_PTR(BP_VAR_R);
3913+
ZVAL_UNDEF(&held_callable);
3914+
ZVAL_DEREF(function_name);
3915+
if (UNEXPECTED(Z_TYPE_P(function_name) == IS_ARRAY)) {
3916+
ZVAL_COPY(&held_callable, function_name);
3917+
function_name = &held_callable;
3918+
}
39123919
if (zend_is_callable_ex(function_name, NULL, 0, NULL, &fcc, &error)) {
39133920
ZEND_ASSERT(!error);
39143921

@@ -3917,6 +3924,9 @@ ZEND_VM_HANDLER(118, ZEND_INIT_USER_CALL, CONST, CONST|TMPVAR|CV, NUM)
39173924
* For the CONST and CV case we reuse the same exception block below
39183925
* to make sure we don't increase VM size too much. */
39193926
if (!(OP2_TYPE & (IS_TMP_VAR|IS_VAR)) && UNEXPECTED(EG(exception))) {
3927+
if (UNEXPECTED(!Z_ISUNDEF(held_callable))) {
3928+
zval_ptr_dtor(&held_callable);
3929+
}
39203930
FREE_OP2();
39213931
HANDLE_EXCEPTION();
39223932
}
@@ -3940,6 +3950,9 @@ ZEND_VM_HANDLER(118, ZEND_INIT_USER_CALL, CONST, CONST|TMPVAR|CV, NUM)
39403950
call_info |= ZEND_CALL_RELEASE_THIS | ZEND_CALL_HAS_THIS;
39413951
}
39423952

3953+
if (UNEXPECTED(!Z_ISUNDEF(held_callable))) {
3954+
zval_ptr_dtor(&held_callable);
3955+
}
39433956
FREE_OP2();
39443957
if ((OP2_TYPE & (IS_TMP_VAR|IS_VAR)) && UNEXPECTED(EG(exception))) {
39453958
if (call_info & ZEND_CALL_CLOSURE) {
@@ -3954,6 +3967,9 @@ ZEND_VM_HANDLER(118, ZEND_INIT_USER_CALL, CONST, CONST|TMPVAR|CV, NUM)
39543967
init_func_run_time_cache(&func->op_array);
39553968
}
39563969
} else {
3970+
if (UNEXPECTED(!Z_ISUNDEF(held_callable))) {
3971+
zval_ptr_dtor(&held_callable);
3972+
}
39573973
zend_type_error("%s(): Argument #1 ($callback) must be a valid callback, %s", Z_STRVAL_P(RT_CONSTANT(opline, opline->op1)), error);
39583974
efree(error);
39593975
FREE_OP2();

Zend/zend_vm_execute.h

Lines changed: 50 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)