Skip to content

Commit 5a8e1b5

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 02ca74f commit 5a8e1b5

6 files changed

Lines changed: 181 additions & 4 deletions

File tree

NEWS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ PHP NEWS
1111
. Fixed bug GH-18847 (SEGV in zend_fetch_debug_backtrace() when the memory
1212
limit is reached while the tracing JIT enters a call frame). (Arnaud,
1313
iliaal)
14+
. Fixed bug GH-22878 (Use-after-free of callable via autoloader). (iliaal)
1415

1516
- DOM:
1617
. Fixed bug GH-22825 (DOMElement::setAttribute() fails silently when the DTD

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
@@ -4194,6 +4194,9 @@ ZEND_API bool zend_is_callable_at_frame(
41944194
bool ret;
41954195
zend_fcall_info_cache fcc_local;
41964196
bool strict_class = false;
4197+
zval callable_copy;
4198+
4199+
ZVAL_UNDEF(&callable_copy);
41974200

41984201
if (fcc == NULL) {
41994202
fcc = &fcc_local;
@@ -4221,11 +4224,17 @@ ZEND_API bool zend_is_callable_at_frame(
42214224
return 1;
42224225
}
42234226

4227+
ZVAL_COPY(&callable_copy, callable);
4228+
callable = &callable_copy;
4229+
42244230
check_func:
42254231
ret = zend_is_callable_check_func(callable, frame, fcc, strict_class, error, check_flags & IS_CALLABLE_SUPPRESS_DEPRECATIONS);
42264232
if (fcc == &fcc_local) {
42274233
zend_release_fcall_info_cache(fcc);
42284234
}
4235+
if (!Z_ISUNDEF(callable_copy)) {
4236+
zval_ptr_dtor(&callable_copy);
4237+
}
42294238
return ret;
42304239

42314240
case IS_ARRAY:
@@ -4259,7 +4268,11 @@ ZEND_API bool zend_is_callable_at_frame(
42594268
return 1;
42604269
}
42614270

4271+
ZVAL_COPY(&callable_copy, method);
4272+
callable = &callable_copy;
4273+
42624274
if (!zend_is_callable_check_class(Z_STR_P(obj), get_scope(frame), frame, fcc, &strict_class, error, check_flags & IS_CALLABLE_SUPPRESS_DEPRECATIONS)) {
4275+
zval_ptr_dtor(&callable_copy);
42634276
return 0;
42644277
}
42654278
} else {
@@ -4271,9 +4284,11 @@ ZEND_API bool zend_is_callable_at_frame(
42714284
fcc->called_scope = fcc->calling_scope;
42724285
return 1;
42734286
}
4287+
4288+
ZVAL_COPY(&callable_copy, method);
4289+
callable = &callable_copy;
42744290
}
42754291

4276-
callable = method;
42774292
goto check_func;
42784293
}
42794294
return 0;

Zend/zend_execute.c

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5257,23 +5257,27 @@ static zend_never_inline zend_execute_data *zend_init_dynamic_call_array(const z
52575257
}
52585258

52595259
if (Z_TYPE_P(obj) == IS_STRING) {
5260+
zend_string *method_name = zend_string_copy(Z_STR_P(method));
52605261
zend_class_entry *called_scope = zend_fetch_class_by_name(Z_STR_P(obj), NULL, ZEND_FETCH_CLASS_DEFAULT | ZEND_FETCH_CLASS_EXCEPTION);
52615262

52625263
if (UNEXPECTED(called_scope == NULL)) {
5264+
zend_string_release(method_name);
52635265
return NULL;
52645266
}
52655267

52665268
if (called_scope->get_static_method) {
5267-
fbc = called_scope->get_static_method(called_scope, Z_STR_P(method));
5269+
fbc = called_scope->get_static_method(called_scope, method_name);
52685270
} else {
5269-
fbc = zend_std_get_static_method(called_scope, Z_STR_P(method), NULL);
5271+
fbc = zend_std_get_static_method(called_scope, method_name, NULL);
52705272
}
52715273
if (UNEXPECTED(fbc == NULL)) {
52725274
if (EXPECTED(!EG(exception))) {
5273-
zend_undefined_method(called_scope, Z_STR_P(method));
5275+
zend_undefined_method(called_scope, method_name);
52745276
}
5277+
zend_string_release(method_name);
52755278
return NULL;
52765279
}
5280+
zend_string_release(method_name);
52775281
if (!(fbc->common.fn_flags & ZEND_ACC_STATIC)) {
52785282
zend_non_static_method_call(fbc);
52795283
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
@@ -3982,9 +3982,16 @@ ZEND_VM_HANDLER(118, ZEND_INIT_USER_CALL, CONST, CONST|TMP|CV, NUM)
39823982
void *object_or_called_scope;
39833983
zend_execute_data *call;
39843984
uint32_t call_info = ZEND_CALL_NESTED_FUNCTION | ZEND_CALL_DYNAMIC;
3985+
zval held_callable;
39853986

39863987
SAVE_OPLINE();
39873988
function_name = GET_OP2_ZVAL_PTR(BP_VAR_R);
3989+
ZVAL_UNDEF(&held_callable);
3990+
ZVAL_DEREF(function_name);
3991+
if (UNEXPECTED(Z_TYPE_P(function_name) == IS_ARRAY)) {
3992+
ZVAL_COPY(&held_callable, function_name);
3993+
function_name = &held_callable;
3994+
}
39883995
if (zend_is_callable_ex(function_name, NULL, 0, NULL, &fcc, &error)) {
39893996
ZEND_ASSERT(!error);
39903997

@@ -3993,6 +4000,9 @@ ZEND_VM_HANDLER(118, ZEND_INIT_USER_CALL, CONST, CONST|TMP|CV, NUM)
39934000
* For the CONST and CV case we reuse the same exception block below
39944001
* to make sure we don't increase VM size too much. */
39954002
if (!(OP2_TYPE & (IS_TMP_VAR|IS_VAR)) && UNEXPECTED(EG(exception))) {
4003+
if (UNEXPECTED(!Z_ISUNDEF(held_callable))) {
4004+
zval_ptr_dtor(&held_callable);
4005+
}
39964006
FREE_OP2();
39974007
HANDLE_EXCEPTION();
39984008
}
@@ -4016,6 +4026,9 @@ ZEND_VM_HANDLER(118, ZEND_INIT_USER_CALL, CONST, CONST|TMP|CV, NUM)
40164026
call_info |= ZEND_CALL_RELEASE_THIS | ZEND_CALL_HAS_THIS;
40174027
}
40184028

4029+
if (UNEXPECTED(!Z_ISUNDEF(held_callable))) {
4030+
zval_ptr_dtor(&held_callable);
4031+
}
40194032
FREE_OP2();
40204033
if ((OP2_TYPE & (IS_TMP_VAR|IS_VAR)) && UNEXPECTED(EG(exception))) {
40214034
if (call_info & ZEND_CALL_CLOSURE) {
@@ -4030,6 +4043,9 @@ ZEND_VM_HANDLER(118, ZEND_INIT_USER_CALL, CONST, CONST|TMP|CV, NUM)
40304043
init_func_run_time_cache(&func->op_array);
40314044
}
40324045
} else {
4046+
if (UNEXPECTED(!Z_ISUNDEF(held_callable))) {
4047+
zval_ptr_dtor(&held_callable);
4048+
}
40334049
zend_type_error("%s(): Argument #1 ($callback) must be a valid callback, %s", Z_STRVAL_P(RT_CONSTANT(opline, opline->op1)), error);
40344050
efree(error);
40354051
FREE_OP2();

0 commit comments

Comments
 (0)