Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move the dummy function of call_attribute_constructor onto the VM stack #2446

Merged
merged 2 commits into from
Jan 17, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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).
bwoebi marked this conversation as resolved.
Show resolved Hide resolved
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
Loading