Skip to content

Commit 485d3ac

Browse files
committed
Make zend_call_function() failure handling consistent
This API had rather peculiar behavior in case the provided function is not callable. For some types of failures, it would silently return FAILURE (e.g. a function does not exist), while for others (e.g. a class does not exist) it would generate a warning. Depending on what the calling code does, this can either result in silent failure or duplicate errors. This commit switches the contract such that zend_call_function() always (*) succeeds, though that success might be in the form of throwing an exception. Calling a non-callable will now consistently throw an exception. There are some rare callers that do want to ignore missing methods, for legacy APIs that are specific with optional methods. For these use cases a new zend_call_method_if_exists() API is provided. Calling code generally does not need to explicitly check for and report zend_call_function() failures -- it can rely on zend_call_function() having already done so. However, existing code that does check for failure should continue to work fine. (*) The only exception to this is if EG(active) being false during late engine shutdown. This is not relevant to most code, but code running in destructors and similar may need to be aware of the possibility.
1 parent 0b743d5 commit 485d3ac

File tree

7 files changed

+105
-137
lines changed

7 files changed

+105
-137
lines changed

Zend/zend_API.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,9 @@ ZEND_API void zend_fcall_info_argn(zend_fcall_info *fci, uint32_t argc, ...);
646646
*/
647647
ZEND_API zend_result zend_fcall_info_call(zend_fcall_info *fci, zend_fcall_info_cache *fcc, zval *retval, zval *args);
648648

649+
/* Can only return FAILURE if EG(active) is false during late engine shutdown.
650+
* If the call or call setup throws, EG(exception) will be set and the retval
651+
* will be UNDEF. Otherwise, the retval will be a non-UNDEF value. */
649652
ZEND_API zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_cache);
650653

651654
/* Call the provided zend_function with the given params.
@@ -679,6 +682,13 @@ static zend_always_inline void zend_call_known_instance_method_with_1_params(
679682
ZEND_API void zend_call_known_instance_method_with_2_params(
680683
zend_function *fn, zend_object *object, zval *retval_ptr, zval *param1, zval *param2);
681684

685+
/* Call method if it exists. Return FAILURE if method does not exist or call failed.
686+
* If FAILURE is returned, retval will be UNDEF. As such, destroying retval unconditionally
687+
* is legal. */
688+
ZEND_API zend_result zend_call_method_if_exists(
689+
zend_object *object, zend_string *method_name, zval *retval,
690+
uint32_t param_count, zval *params);
691+
682692
ZEND_API zend_result zend_set_hash_symbol(zval *symbol, const char *name, size_t name_length, bool is_ref, int num_symbol_tables, ...);
683693

684694
ZEND_API zend_result zend_delete_global_variable(zend_string *name);

Zend/zend_execute_API.c

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -719,7 +719,7 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_
719719
}
720720

721721
if (EG(exception)) {
722-
return FAILURE; /* we would result in an instable executor otherwise */
722+
return SUCCESS; /* we would result in an instable executor otherwise */
723723
}
724724

725725
ZEND_ASSERT(fci->size == sizeof(zend_fcall_info));
@@ -731,15 +731,14 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_
731731
fci_cache = &fci_cache_local;
732732
}
733733

734-
if (!zend_is_callable_ex(&fci->function_name, fci->object, IS_CALLABLE_CHECK_SILENT, NULL, fci_cache, &error)) {
735-
if (error) {
736-
zend_string *callable_name
737-
= zend_get_callable_name_ex(&fci->function_name, fci->object);
738-
zend_error(E_WARNING, "Invalid callback %s, %s", ZSTR_VAL(callable_name), error);
739-
efree(error);
740-
zend_string_release_ex(callable_name, 0);
741-
}
742-
return FAILURE;
734+
if (!zend_is_callable_ex(&fci->function_name, fci->object, 0, NULL, fci_cache, &error)) {
735+
ZEND_ASSERT(error && "Should have error if not callable");
736+
zend_string *callable_name
737+
= zend_get_callable_name_ex(&fci->function_name, fci->object);
738+
zend_throw_error(NULL, "Invalid callback %s, %s", ZSTR_VAL(callable_name), error);
739+
efree(error);
740+
zend_string_release_ex(callable_name, 0);
741+
return SUCCESS;
743742
}
744743

745744
ZEND_ASSERT(!error);
@@ -762,7 +761,7 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_
762761

763762
if (UNEXPECTED(EG(exception))) {
764763
zend_vm_stack_free_call_frame(call);
765-
return FAILURE;
764+
return SUCCESS;
766765
}
767766
}
768767

@@ -789,7 +788,7 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_
789788
cleanup_args:
790789
zend_vm_stack_free_args(call);
791790
zend_vm_stack_free_call_frame(call);
792-
return FAILURE;
791+
return SUCCESS;
793792
}
794793
}
795794
}
@@ -1010,6 +1009,28 @@ ZEND_API void zend_call_known_instance_method_with_2_params(
10101009
zend_call_known_instance_method(fn, object, retval_ptr, 2, params);
10111010
}
10121011

1012+
ZEND_API zend_result zend_call_method_if_exists(
1013+
zend_object *object, zend_string *method_name, zval *retval,
1014+
uint32_t param_count, zval *params)
1015+
{
1016+
zend_fcall_info fci;
1017+
fci.size = sizeof(zend_fcall_info);
1018+
fci.object = object;
1019+
ZVAL_STR(&fci.function_name, method_name);
1020+
fci.retval = retval;
1021+
fci.param_count = param_count;
1022+
fci.params = params;
1023+
fci.named_params = NULL;
1024+
1025+
zend_fcall_info_cache fcc;
1026+
if (!zend_is_callable_ex(&fci.function_name, fci.object, 0, NULL, &fcc, NULL)) {
1027+
ZVAL_UNDEF(retval);
1028+
return FAILURE;
1029+
}
1030+
1031+
return zend_call_function(&fci, &fcc);
1032+
}
1033+
10131034
/* 0-9 a-z A-Z _ \ 0x80-0xff */
10141035
static const uint32_t valid_chars[8] = {
10151036
0x00000000,

ext/standard/tests/assert/assert_variation.phpt

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,15 @@ var_dump($r2=assert(0 != 0));
4747
echo"\n";
4848

4949

50-
echo "Reset the name of the callback routine to a class method and check that it works\n";
50+
echo "Reset the name of the callback routine to a class method\n";
5151
var_dump($rc=assert_options(ASSERT_CALLBACK, "c1"));
5252
echo "assert_options(ASSERT_CALLBACK) => [".assert_options(ASSERT_CALLBACK)."]\n";
5353
echo "ini.get(\"assert.callback\") => [".ini_get("assert.callback")."]\n";
54-
var_dump($r2=assert(0 != 0));
54+
try {
55+
var_dump($r2=assert(0 != 0));
56+
} catch (Error $e) {
57+
echo $e->getMessage(), "\n";
58+
}
5559
echo"\n";
5660

5761
echo "Reset callback options to use a class method \n";
@@ -94,11 +98,11 @@ ini.get("assert.callback") => [f2]
9498
f3 called
9599
bool(false)
96100

97-
Reset the name of the callback routine to a class method and check that it works
101+
Reset the name of the callback routine to a class method
98102
string(2) "f3"
99103
assert_options(ASSERT_CALLBACK) => [c1]
100104
ini.get("assert.callback") => [f2]
101-
bool(false)
105+
Invalid callback c1, function "c1" not found or invalid function name
102106

103107
Reset callback options to use a class method
104108
string(2) "c1"
@@ -110,7 +114,7 @@ array(2) {
110114
}
111115
ini.get("assert.callback") => [f2]
112116

113-
Class assertion failed 52, "assert(0 != 0)"
117+
Class assertion failed 56, "assert(0 != 0)"
114118
bool(false)
115119

116120
Reset callback options to use an object method
@@ -122,14 +126,14 @@ array(2) {
122126
}
123127
array(2) {
124128
[0]=>
125-
&object(c1)#1 (0) {
129+
&object(c1)#2 (0) {
126130
}
127131
[1]=>
128132
string(6) "assert"
129133
}
130134
ini.get("assert.callback") => [f2]
131135

132-
Class assertion failed 60, "assert(0 != 0)"
136+
Class assertion failed 64, "assert(0 != 0)"
133137
bool(false)
134138

135139
Set callback to something silly

ext/standard/tests/serialize/serialization_objects_008.phpt

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,11 @@ Bad unserialize_callback_func
33
--FILE--
44
<?php
55
ini_set('unserialize_callback_func','Nonexistent');
6-
$o = unserialize('O:3:"FOO":0:{}');
7-
var_dump($o);
8-
echo "Done";
9-
?>
10-
--EXPECTF--
11-
Warning: unserialize(): defined (Nonexistent) but not found in %s on line %d
12-
object(__PHP_Incomplete_Class)#%d (1) {
13-
["__PHP_Incomplete_Class_Name"]=>
14-
string(3) "FOO"
6+
try {
7+
unserialize('O:3:"FOO":0:{}');
8+
} catch (Error $e) {
9+
echo $e->getMessage(), "\n";
1510
}
16-
Done
11+
?>
12+
--EXPECT--
13+
Invalid callback Nonexistent, function "Nonexistent" not found or invalid function name

ext/standard/user_filters.c

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -111,24 +111,18 @@ PHP_RSHUTDOWN_FUNCTION(user_filters)
111111
static void userfilter_dtor(php_stream_filter *thisfilter)
112112
{
113113
zval *obj = &thisfilter->abstract;
114-
zval func_name;
115114
zval retval;
116115

117116
if (Z_ISUNDEF_P(obj)) {
118117
/* If there's no object associated then there's nothing to dispose of */
119118
return;
120119
}
121120

122-
ZVAL_STRINGL(&func_name, "onclose", sizeof("onclose")-1);
123-
124-
call_user_function(NULL,
125-
obj,
126-
&func_name,
127-
&retval,
128-
0, NULL);
121+
zend_string *func_name = zend_string_init("onclose", sizeof("onclose")-1, 0);
122+
zend_call_method_if_exists(Z_OBJ_P(obj), func_name, &retval, 0, NULL);
123+
zend_string_release(func_name);
129124

130125
zval_ptr_dtor(&retval);
131-
zval_ptr_dtor(&func_name);
132126

133127
/* kill the object */
134128
zval_ptr_dtor(obj);
@@ -243,7 +237,6 @@ static php_stream_filter *user_filter_factory_create(const char *filtername,
243237
struct php_user_filter_data *fdat = NULL;
244238
php_stream_filter *filter;
245239
zval obj;
246-
zval func_name;
247240
zval retval;
248241
size_t len;
249242

@@ -319,15 +312,9 @@ static php_stream_filter *user_filter_factory_create(const char *filtername,
319312
}
320313

321314
/* invoke the constructor */
322-
ZVAL_STRINGL(&func_name, "oncreate", sizeof("oncreate")-1);
323-
324-
call_user_function(NULL,
325-
&obj,
326-
&func_name,
327-
&retval,
328-
0, NULL);
329-
330-
zval_ptr_dtor(&func_name);
315+
zend_string *func_name = zend_string_init("oncreate", sizeof("oncreate")-1, 0);
316+
zend_call_method_if_exists(Z_OBJ(obj), func_name, &retval, 0, NULL);
317+
zend_string_release(func_name);
331318

332319
if (Z_TYPE(retval) != IS_UNDEF) {
333320
if (Z_TYPE(retval) == IS_FALSE) {

ext/xml/tests/bug72085.phpt

Lines changed: 7 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,11 @@ xml
66
<?php
77
$var1 = xml_parser_create_ns();
88
xml_set_element_handler($var1, new Exception(""), 4096);
9-
xml_parse($var1, str_repeat("<a>", 10));
9+
try {
10+
xml_parse($var1, str_repeat("<a>", 10));
11+
} catch (Error $e) {
12+
echo $e->getMessage(), "\n";
13+
}
1014
?>
11-
--EXPECTF--
12-
Warning: Invalid callback Exception::__invoke, no array or string given in %s on line %d
13-
14-
Warning: xml_parse(): Unable to call handler in %s on line %d
15-
16-
Warning: Invalid callback Exception::__invoke, no array or string given in %s on line %d
17-
18-
Warning: xml_parse(): Unable to call handler in %s on line %d
19-
20-
Warning: Invalid callback Exception::__invoke, no array or string given in %s on line %d
21-
22-
Warning: xml_parse(): Unable to call handler in %s on line %d
23-
24-
Warning: Invalid callback Exception::__invoke, no array or string given in %s on line %d
25-
26-
Warning: xml_parse(): Unable to call handler in %s on line %d
27-
28-
Warning: Invalid callback Exception::__invoke, no array or string given in %s on line %d
29-
30-
Warning: xml_parse(): Unable to call handler in %s on line %d
31-
32-
Warning: Invalid callback Exception::__invoke, no array or string given in %s on line %d
33-
34-
Warning: xml_parse(): Unable to call handler in %s on line %d
35-
36-
Warning: Invalid callback Exception::__invoke, no array or string given in %s on line %d
37-
38-
Warning: xml_parse(): Unable to call handler in %s on line %d
39-
40-
Warning: Invalid callback Exception::__invoke, no array or string given in %s on line %d
41-
42-
Warning: xml_parse(): Unable to call handler in %s on line %d
43-
44-
Warning: Invalid callback Exception::__invoke, no array or string given in %s on line %d
45-
46-
Warning: xml_parse(): Unable to call handler in %s on line %d
47-
48-
Warning: Invalid callback Exception::__invoke, no array or string given in %s on line %d
49-
50-
Warning: xml_parse(): Unable to call handler in %s on line %d
15+
--EXPECT--
16+
Invalid callback Exception::__invoke, no array or string given

0 commit comments

Comments
 (0)