Skip to content

Commit 1e7c861

Browse files
authored
fix(agent): wrap zend_try in a func to avoid clobbering (#703)
Calls to `call_user_function` (in PHP 8.0 and 8.1) and `zend_call_method_if_exists` (in PHP 8.2+) need to be wrapped by the `zend_try`/`zend_catch`/`zend_end_try` block, which use [`setjmp`](https://linux.die.net/man/3/setjmp) and [`longjmp`](https://linux.die.net/man/3/longjmp), because according to [`call_user_func()`](https://www.php.net/manual/en/function.call-user-func.php): > Callbacks registered with functions such as `call_user_func()` and [`call_user_func_array()`](https://www.php.net/manual/en/function.call-user-func-array.php) will not be called if there is an uncaught exception thrown in a previous callback. So if we call something that causes an exception, it will block us from future calls that use `call_user_func` or `call_user_func_array`. Valgrind showed the agent and/or the Zend engine wasn’t properly cleaning up after such cases and newer compilers had issues with this when compiling with any optimization and generated the following error: ``` error: variable ‘retval’ might be clobbered by ‘longjmp’ or ‘vfork’) [-Werror=clobbered] ``` PHP developers solve this problem by using an automatic variable to store user function call result - see [here](https://github.com/php/php-src/blob/master/main/streams/userspace.c#L335-L340) for an example in PHP source code how zend_call_method_if_exists is called. This solution wraps `zend_try`/`zend_catch`/`zend_end_try` constructs in a function to localize the `retval` and avoid variable clobbering.
1 parent dca53bd commit 1e7c861

File tree

3 files changed

+70
-23
lines changed

3 files changed

+70
-23
lines changed

agent/php_call.c

Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,56 @@
1010

1111
#include "Zend/zend_exceptions.h"
1212

13+
/*
14+
* zend_try family of macros entail the use of setjmp and longjmp, which can cause clobbering issues with
15+
* non-primitive local variables. Abstracting these constructs into separate functions protects from this.
16+
*/
17+
#if ZEND_MODULE_API_NO >= ZEND_8_2_X_API_NO
18+
static int nr_php_call_try_catch(zend_object* object,
19+
zend_string* method_name,
20+
zval* retval,
21+
zend_uint param_count,
22+
zval* param_values) {
23+
/*
24+
* With PHP 8.2, functions that do not exist will cause a fatal error to
25+
* be thrown. `zend_call_method_if_exists` will attempt to call a function and
26+
* silently fail if it does not exist
27+
*/
28+
int zend_result = FAILURE;
29+
zend_try {
30+
zend_result = zend_call_method_if_exists(object, method_name, retval,
31+
param_count, param_values);
32+
}
33+
zend_catch { zend_result = FAILURE; }
34+
zend_end_try();
35+
return zend_result;
36+
}
37+
#elif ZEND_MODULE_API_NO < ZEND_8_2_X_API_NO \
38+
&& ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO
39+
static int nr_php_call_try_catch(zval* object_ptr,
40+
zval* fname,
41+
zval* retval,
42+
zend_uint param_count,
43+
zval* param_values) {
44+
/*
45+
* With PHP8, `call_user_function_ex` was removed and `call_user_function`
46+
* became the recommended function.
47+
* According to zend internals documentation:
48+
* As of PHP 7.1.0, the function_table argument is not used and should
49+
* always be NULL. See for more details:
50+
* https://www.phpinternalsbook.com/php7/internal_types/functions/callables.html
51+
*/
52+
int zend_result = FAILURE;
53+
zend_try {
54+
zend_result = call_user_function(EG(function_table), object_ptr, fname,
55+
retval, param_count, param_values);
56+
}
57+
zend_catch { zend_result = FAILURE; }
58+
zend_end_try();
59+
return zend_result;
60+
}
61+
#endif
62+
1363
zval* nr_php_call_user_func(zval* object_ptr,
1464
const char* function_name,
1565
zend_uint param_count,
@@ -53,11 +103,6 @@ zval* nr_php_call_user_func(zval* object_ptr,
53103
fname = nr_php_zval_alloc();
54104
nr_php_zval_str(fname, function_name);
55105
#if ZEND_MODULE_API_NO >= ZEND_8_2_X_API_NO /* PHP 8.2+ */
56-
/*
57-
* With PHP 8.2, functions that do not exist will cause a fatal error to
58-
* be thrown. `zend_call_method_if_exists` will attempt to call a function and
59-
* silently fail if it does not exist
60-
*/
61106
if (NULL != object_ptr) {
62107
object = Z_OBJ_P(object_ptr);
63108
} else {
@@ -69,26 +114,20 @@ zval* nr_php_call_user_func(zval* object_ptr,
69114
} else {
70115
return NULL;
71116
}
72-
zend_try {
73-
zend_result = zend_call_method_if_exists(object, method_name, retval,
74-
param_count, param_values);
75-
}
76-
zend_catch { zend_result = FAILURE; }
77-
zend_end_try();
78-
#elif ZEND_MODULE_API_NO < ZEND_8_2_X_API_NO \
79-
&& ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO
117+
80118
/*
81-
* With PHP8, `call_user_function_ex` was removed and `call_user_function`
82-
* became the recommended function. This does't return a FAILURE for
83-
* exceptions and needs to be in a try/catch block in order to clean up
84-
* properly.
119+
* For PHP 8+, in the case of exceptions according to:
120+
* https://www.php.net/manual/en/function.call-user-func.php
121+
* Callbacks registered with functions such as call_user_func() and
122+
* call_user_func_array() will not be called if there is an uncaught exception
123+
* thrown in a previous callback. So if we call something that causes an
124+
* exception, it will block us from future calls that use call_user_func or
125+
* call_user_func_array and hence the need for a try/catch block.
85126
*/
86-
zend_try {
87-
zend_result = call_user_function(EG(function_table), object_ptr, fname,
88-
retval, param_count, param_values);
89-
}
90-
zend_catch { zend_result = FAILURE; }
91-
zend_end_try();
127+
zend_result = nr_php_call_try_catch(object, method_name, retval, param_count, param_values);
128+
#elif ZEND_MODULE_API_NO < ZEND_8_2_X_API_NO \
129+
&& ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO
130+
zend_result = nr_php_call_try_catch(object_ptr, fname, retval, param_count, param_values);
92131

93132
#else
94133
zend_result = call_user_function_ex(EG(function_table), object_ptr, fname,

tests/integration/frameworks/wordpress/test_wordpress_apply_filters.php8.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ function apply_filters($tag, ...$args) {
5252
call_user_func_array($tag, $args);
5353
}
5454

55+
//Simple mock of wordpress's get_theme_roots
56+
function get_theme_roots() {
57+
}
58+
5559
function h($str) {
5660
echo "h: ";
5761
echo $str;

tests/integration/frameworks/wordpress/test_wordpress_do_action.php8.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ function do_action($tag, ...$args) {
5353
call_user_func_array($tag, $args);
5454
}
5555

56+
//Simple mock of wordpress's get_theme_roots
57+
function get_theme_roots() {
58+
}
59+
5660
function h() {
5761
echo "h\n";
5862
throw new Exception("Test Exception");

0 commit comments

Comments
 (0)