Skip to content

Commit 001ca15

Browse files
committed
Move the dummy function of call_attribute_constructor onto the VM stack
Signed-off-by: Bob Weinand <[email protected]>
1 parent afa2b3c commit 001ca15

File tree

1 file changed

+111
-0
lines changed

1 file changed

+111
-0
lines changed

ext/ddtrace.c

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include <php_ini.h>
2626
#include <pthread.h>
2727
#include <stdatomic.h>
28+
#include <sys/mman.h>
2829

2930
#include <ext/standard/info.h>
3031
#include <ext/standard/php_string.h>
@@ -146,6 +147,104 @@ static void ddtrace_sort_modules(void *base, size_t count, size_t siz, compare_f
146147
}
147148
#endif
148149

150+
#if PHP_VERSION_ID >= 80000 && PHP_VERSION_ID < 80200
151+
// On PHP 8.0.0-8.0.16 and 8.1.0-8.1.2 call_attribute_constructor would stack allocate a dummy frame, which could have become inaccessible upon access.
152+
// Thus, we implement the fix which was applied to PHP itself as well: we move the stack allocated data to the VM stack.
153+
// See also https://github.com/php/php-src/commit/f7c3f6e7e25471da9cfb2ba082a77cc3c85bc6ed
154+
static void dd_patched_zend_call_known_function(
155+
zend_function *fn, zend_object *object, zend_class_entry *called_scope, zval *retval_ptr,
156+
uint32_t param_count, zval *params, HashTable *named_params)
157+
{
158+
zval retval;
159+
zend_fcall_info fci;
160+
zend_fcall_info_cache fcic;
161+
162+
// If current_execute_data is on the stack, move it to the VM stack
163+
zend_execute_data *execute_data = EG(current_execute_data);
164+
if ((uintptr_t)&retval > (uintptr_t)EX(func) && (uintptr_t)&retval - 0xfffff < (uintptr_t)EX(func)) {
165+
zend_execute_data *call = zend_vm_stack_push_call_frame_ex(
166+
ZEND_MM_ALIGNED_SIZE_EX(sizeof(zend_execute_data), sizeof(zval)) +
167+
ZEND_MM_ALIGNED_SIZE_EX(sizeof(zend_op), sizeof(zval)) +
168+
ZEND_MM_ALIGNED_SIZE_EX(sizeof(zend_function), sizeof(zval)),
169+
0, EX(func), 0, NULL);
170+
171+
memcpy(call, execute_data, sizeof(zend_execute_data));
172+
zend_op *opline = (zend_op *)(call + 1);
173+
memcpy(opline, EX(opline), sizeof(zend_op));
174+
zend_function *func = (zend_function *)(opline + 1);
175+
func->common.fn_flags |= ZEND_ACC_CALL_VIA_TRAMPOLINE; // See https://github.com/php/php-src/commit/2f6a06ccb0ef78e6122bb9e67f9b8b1ad07776e1
176+
memcpy((zend_op *)(call + 1) + 1, EX(func), sizeof(zend_function));
177+
178+
call->opline = opline;
179+
call->func = func;
180+
181+
EG(current_execute_data) = call;
182+
}
183+
184+
// here follows the original implementation of zend_call_known_function
185+
186+
fci.size = sizeof(fci);
187+
fci.object = object;
188+
fci.retval = retval_ptr ? retval_ptr : &retval;
189+
fci.param_count = param_count;
190+
fci.params = params;
191+
fci.named_params = named_params;
192+
ZVAL_UNDEF(&fci.function_name); /* Unused */
193+
194+
fcic.function_handler = fn;
195+
fcic.object = object;
196+
fcic.called_scope = called_scope;
197+
198+
zend_result result = zend_call_function(&fci, &fcic);
199+
if (UNEXPECTED(result == FAILURE)) {
200+
if (!EG(exception)) {
201+
zend_error_noreturn(E_CORE_ERROR, "Couldn't execute method %s%s%s",
202+
fn->common.scope ? ZSTR_VAL(fn->common.scope->name) : "",
203+
fn->common.scope ? "::" : "", ZSTR_VAL(fn->common.function_name));
204+
}
205+
}
206+
207+
if (!retval_ptr) {
208+
zval_ptr_dtor(&retval);
209+
}
210+
}
211+
212+
// We need to hijack zend_call_known_function as that's what's being called by call_attribute_constructor, and call_attribute_constructor itself is not exported.
213+
static void dd_patch_zend_call_known_function(void) {
214+
size_t page_size = sysconf(_SC_PAGESIZE);
215+
void *page = (void *)(~(page_size - 1) & (uintptr_t)zend_call_known_function);
216+
// 20 is the largest size of a trampoline we have to inject
217+
if ((((uintptr_t)zend_call_known_function + 20) & page_size) < 20) {
218+
page_size <<= 1; // if overlapping pages, use two
219+
}
220+
if (mprotect(page, page_size, PROT_READ | PROT_WRITE) != 0) { // Some architectures enforce W^X (either write _or_ execute, but not both).
221+
return; // Make absolutely sure we can write
222+
}
223+
224+
#ifdef __aarch64__
225+
// x13 is a scratch register
226+
uint32_t absolute_jump_instrs[] = {
227+
0x1000006D, // adr x13, 12 (load address from memory after this)
228+
0xF94001AD, // ldr x13, [x13]
229+
0xD61F01A0, // br x13
230+
};
231+
// The magical 12 is sizeof(absolute_jump_instrs) and hardcoded in the assembly above.
232+
memcpy(zend_call_known_function, absolute_jump_instrs, 12);
233+
*(void **)(12 + (uintptr_t)zend_call_known_function) = dd_patched_zend_call_known_function;
234+
#else
235+
// $r10 doesn't really have special meaning
236+
uint8_t absolute_jump_instrs[] = {
237+
0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov $r10, imm_addr
238+
0x41, 0xFF, 0xE2 // jmp $r10
239+
};
240+
*(void **)&absolute_jump_instrs[2] = dd_patched_zend_call_known_function;
241+
memcpy(zend_call_known_function, absolute_jump_instrs, sizeof(absolute_jump_instrs));
242+
#endif
243+
244+
mprotect(page, page_size, PROT_READ | PROT_EXEC);
245+
}
246+
#endif
247+
149248
// put this into startup so that other extensions running code as part of rinit do not crash
150249
static int ddtrace_startup(zend_extension *extension) {
151250
UNUSED(extension);
@@ -170,6 +269,18 @@ static int ddtrace_startup(zend_extension *extension) {
170269
zai_interceptor_startup();
171270
#endif
172271

272+
#if PHP_VERSION_ID >= 80000 && PHP_VERSION_ID < 80200
273+
#if PHP_VERSION_ID < 80100
274+
#define BUG_STACK_ALLOCATED_CALL_PATCH_VERSION 16
275+
#else
276+
#define BUG_STACK_ALLOCATED_CALL_PATCH_VERSION 3
277+
#endif
278+
zend_long patch_version = Z_LVAL_P(zend_get_constant_str(ZEND_STRL("PHP_RELEASE_VERSION")));
279+
if (patch_version < BUG_STACK_ALLOCATED_CALL_PATCH_VERSION) {
280+
dd_patch_zend_call_known_function();
281+
}
282+
#endif
283+
173284
ddtrace_excluded_modules_startup();
174285
// We deliberately leave handler replacement during startup, even though this uses some config
175286
// This touches global state, which, while unlikely, may play badly when interacting with other extensions, if done post-startup

0 commit comments

Comments
 (0)