Skip to content

Commit

Permalink
Move the dummy function of call_attribute_constructor onto the VM sta…
Browse files Browse the repository at this point in the history
…ck (#2446)

* Move the dummy function of call_attribute_constructor onto the VM stack

Signed-off-by: Bob Weinand <[email protected]>

* Add log line for mprotect failure

Signed-off-by: Bob Weinand <[email protected]>

---------

Signed-off-by: Bob Weinand <[email protected]>
  • Loading branch information
bwoebi authored Jan 17, 2024
1 parent 8e268d8 commit 6fc482a
Showing 1 changed file with 112 additions and 0 deletions.
112 changes: 112 additions & 0 deletions ext/ddtrace.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <php_ini.h>
#include <pthread.h>
#include <stdatomic.h>
#include <sys/mman.h>

#include <ext/standard/info.h>
#include <ext/standard/php_string.h>
Expand Down Expand Up @@ -146,6 +147,105 @@ static void ddtrace_sort_modules(void *base, size_t count, size_t siz, compare_f
}
#endif

#if PHP_VERSION_ID >= 80000 && PHP_VERSION_ID < 80200
// 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.
// Thus, we implement the fix which was applied to PHP itself as well: we move the stack allocated data to the VM stack.
// See also https://github.com/php/php-src/commit/f7c3f6e7e25471da9cfb2ba082a77cc3c85bc6ed
static void dd_patched_zend_call_known_function(
zend_function *fn, zend_object *object, zend_class_entry *called_scope, zval *retval_ptr,
uint32_t param_count, zval *params, HashTable *named_params)
{
zval retval;
zend_fcall_info fci;
zend_fcall_info_cache fcic;

// If current_execute_data is on the stack, move it to the VM stack
zend_execute_data *execute_data = EG(current_execute_data);
if ((uintptr_t)&retval > (uintptr_t)EX(func) && (uintptr_t)&retval - 0xfffff < (uintptr_t)EX(func)) {
zend_execute_data *call = zend_vm_stack_push_call_frame_ex(
ZEND_MM_ALIGNED_SIZE_EX(sizeof(zend_execute_data), sizeof(zval)) +
ZEND_MM_ALIGNED_SIZE_EX(sizeof(zend_op), sizeof(zval)) +
ZEND_MM_ALIGNED_SIZE_EX(sizeof(zend_function), sizeof(zval)),
0, EX(func), 0, NULL);

memcpy(call, execute_data, sizeof(zend_execute_data));
zend_op *opline = (zend_op *)(call + 1);
memcpy(opline, EX(opline), sizeof(zend_op));
zend_function *func = (zend_function *)(opline + 1);
func->common.fn_flags |= ZEND_ACC_CALL_VIA_TRAMPOLINE; // See https://github.com/php/php-src/commit/2f6a06ccb0ef78e6122bb9e67f9b8b1ad07776e1
memcpy((zend_op *)(call + 1) + 1, EX(func), sizeof(zend_function));

call->opline = opline;
call->func = func;

EG(current_execute_data) = call;
}

// here follows the original implementation of zend_call_known_function

fci.size = sizeof(fci);
fci.object = object;
fci.retval = retval_ptr ? retval_ptr : &retval;
fci.param_count = param_count;
fci.params = params;
fci.named_params = named_params;
ZVAL_UNDEF(&fci.function_name); /* Unused */

fcic.function_handler = fn;
fcic.object = object;
fcic.called_scope = called_scope;

zend_result result = zend_call_function(&fci, &fcic);
if (UNEXPECTED(result == FAILURE)) {
if (!EG(exception)) {
zend_error_noreturn(E_CORE_ERROR, "Couldn't execute method %s%s%s",
fn->common.scope ? ZSTR_VAL(fn->common.scope->name) : "",
fn->common.scope ? "::" : "", ZSTR_VAL(fn->common.function_name));
}
}

if (!retval_ptr) {
zval_ptr_dtor(&retval);
}
}

// 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.
static void dd_patch_zend_call_known_function(void) {
size_t page_size = sysconf(_SC_PAGESIZE);
void *page = (void *)(~(page_size - 1) & (uintptr_t)zend_call_known_function);
// 20 is the largest size of a trampoline we have to inject
if ((((uintptr_t)zend_call_known_function + 20) & page_size) < 20) {
page_size <<= 1; // if overlapping pages, use two
}
if (mprotect(page, page_size, PROT_READ | PROT_WRITE) != 0) { // Some architectures enforce W^X (either write _or_ execute, but not both).
LOG(Error, "Could not alter the memory protection for zend_call_known_function. Tracer execution continues, but may crash when encountering attributes.");
return; // Make absolutely sure we can write
}

#ifdef __aarch64__
// x13 is a scratch register
uint32_t absolute_jump_instrs[] = {
0x1000006D, // adr x13, 12 (load address from memory after this)
0xF94001AD, // ldr x13, [x13]
0xD61F01A0, // br x13
};
// The magical 12 is sizeof(absolute_jump_instrs) and hardcoded in the assembly above.
memcpy(zend_call_known_function, absolute_jump_instrs, 12);
*(void **)(12 + (uintptr_t)zend_call_known_function) = dd_patched_zend_call_known_function;
#else
// $r10 doesn't really have special meaning
uint8_t absolute_jump_instrs[] = {
0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov $r10, imm_addr
0x41, 0xFF, 0xE2 // jmp $r10
};
*(void **)&absolute_jump_instrs[2] = dd_patched_zend_call_known_function;
memcpy(zend_call_known_function, absolute_jump_instrs, sizeof(absolute_jump_instrs));
#endif

mprotect(page, page_size, PROT_READ | PROT_EXEC);
}
#endif

// put this into startup so that other extensions running code as part of rinit do not crash
static int ddtrace_startup(zend_extension *extension) {
UNUSED(extension);
Expand All @@ -170,6 +270,18 @@ static int ddtrace_startup(zend_extension *extension) {
zai_interceptor_startup();
#endif

#if PHP_VERSION_ID >= 80000 && PHP_VERSION_ID < 80200
#if PHP_VERSION_ID < 80100
#define BUG_STACK_ALLOCATED_CALL_PATCH_VERSION 16
#else
#define BUG_STACK_ALLOCATED_CALL_PATCH_VERSION 3
#endif
zend_long patch_version = Z_LVAL_P(zend_get_constant_str(ZEND_STRL("PHP_RELEASE_VERSION")));
if (patch_version < BUG_STACK_ALLOCATED_CALL_PATCH_VERSION) {
dd_patch_zend_call_known_function();
}
#endif

ddtrace_excluded_modules_startup();
// We deliberately leave handler replacement during startup, even though this uses some config
// This touches global state, which, while unlikely, may play badly when interacting with other extensions, if done post-startup
Expand Down

0 comments on commit 6fc482a

Please sign in to comment.