diff --git a/profiling/src/php_ffi.c b/profiling/src/php_ffi.c index fd5d07a175..698d877508 100644 --- a/profiling/src/php_ffi.c +++ b/profiling/src/php_ffi.c @@ -390,19 +390,68 @@ uintptr_t *ddog_test_php_prof_function_run_time_cache(zend_function const *func) } #endif -#if CFG_STACK_WALKING_TESTS +#if CFG_STACK_WALKING_TESTS || defined(CFG_TEST) static int (*og_snprintf)(char *, size_t, const char *, ...); -// "weak" let's us polyfill, needed by zend_string_init(..., persistent: 1). -void *__attribute__((weak)) __zend_malloc(size_t len) { - void *tmp = malloc(len); - if (EXPECTED(tmp || !len)) { - return tmp; - } +static ZEND_COLD ZEND_NORETURN void out_of_memory(void) { fprintf(stderr, "Out of memory\n"); exit(1); } +static zend_string *test_zend_string_alloc(size_t len) { + // We don't need to handle large strings. + if (len > INT32_MAX) { + out_of_memory(); + } + + // zend_string logically has a flexible array member val, but it uses the + // so-called "struct hack" instead. So we calculate the number of bytes + // needed to get to `val`, then add the `len` plus 1 (for a null byte). + uint32_t alloc_len = offsetof(zend_string, val) + len + 1; + + // Need to round up to 8 bytes on 64-bit platforms, won't overflow because + // of above large string check. + uint32_t rounded = (alloc_len + UINT32_C(7)) & ~UINT32_C(7); + zend_string *zs = calloc(rounded, 1); + if (!zs) { + out_of_memory(); + } + + // Initialize the refcounted header +#if PHP_VERSION_ID < 70299 + GC_REFCOUNT(zs) = 1; +#else + GC_SET_REFCOUNT(zs, 1); +#endif + +#if PHP_VERSION_ID < 70200 +#undef GC_FLAGS_SHIFT +#define GC_FLAGS_SHIFT 8 +#endif + +#if PHP_VERSION_ID < 80000 +#undef GC_STRING +#define GC_STRING IS_STRING +#endif + + // IS_STR_PERSISTENT means it's allocated with malloc, not emalloc. + GC_TYPE_INFO(zs) = GC_STRING | (IS_STR_PERSISTENT << GC_FLAGS_SHIFT); + + zs->h = 0; + zs->len = len; + ZSTR_VAL(zs)[len] = '\0'; + + return zs; +} + +// Manually create a zend_string without using zend_string_init(), since we +// do not link against PHP at test time +static zend_string *test_zend_string_create(const char *str, size_t len) { + zend_string *zstr = test_zend_string_alloc(len); + memcpy(ZSTR_VAL(zstr), str, len); + return zstr; +} + static zend_execute_data *create_fake_frame(int depth) { zend_execute_data *execute_data = calloc(1, sizeof(zend_execute_data)); zend_op_array *op_array = calloc(1, sizeof(zend_function)); @@ -412,11 +461,11 @@ static zend_execute_data *create_fake_frame(int depth) { char buffer[64] = {0}; int len = og_snprintf(buffer, sizeof buffer, "function name %03d", depth) + 1; ZEND_ASSERT(len >= 0 && sizeof buffer > (size_t)len); - op_array->function_name = zend_string_init(buffer, len - 1, true); + op_array->function_name = test_zend_string_create(buffer, len - 1); len = og_snprintf(buffer, sizeof buffer, "filename-%03d.php", depth) + 1; ZEND_ASSERT(len >= 0 && sizeof buffer > (size_t)len); - op_array->filename = zend_string_init(buffer, len - 1, true); + op_array->filename = test_zend_string_create(buffer, len - 1); return execute_data; } @@ -463,7 +512,29 @@ void ddog_php_test_free_fake_zend_execute_data(zend_execute_data *execute_data) free(execute_data); } -#endif + +zend_function *ddog_php_test_create_fake_zend_function_with_name_len(size_t len) { + zend_op_array *op_array = calloc(1, sizeof(zend_function)); + if (!op_array) return NULL; + + op_array->type = ZEND_USER_FUNCTION; + + if (len > 0) { + zend_string *zstr = test_zend_string_alloc(len); + memset(ZSTR_VAL(zstr), 'x', len); + op_array->function_name = zstr; + } + + return (zend_function *)op_array; +} + +void ddog_php_test_free_fake_zend_function(zend_function *func) { + if (!func) return; + + free(func->common.function_name); + free(func); +} +#endif // CFG_STACK_WALKING_TESTS || CFG_TEST void *opcache_handle = NULL; diff --git a/profiling/src/profiling/stack_walking.rs b/profiling/src/profiling/stack_walking.rs index 04a8babfca..6f38b74dbb 100644 --- a/profiling/src/profiling/stack_walking.rs +++ b/profiling/src/profiling/stack_walking.rs @@ -528,14 +528,20 @@ mod detail { pub use detail::*; -// todo: this should be feature = "stack_walking_tests" but it seemed to -// cause a failure in CI to migrate it. -#[cfg(all(test, stack_walking_tests))] +#[cfg(test)] mod tests { use super::*; use crate::bindings as zend; + extern "C" { + fn ddog_php_test_create_fake_zend_function_with_name_len( + len: libc::size_t, + ) -> *mut zend::zend_function; + fn ddog_php_test_free_fake_zend_function(func: *mut zend::zend_function); + } + #[test] + #[cfg(stack_walking_tests)] fn test_collect_stack_sample() { unsafe { let fake_execute_data = zend::ddog_php_test_create_fake_zend_execute_data(3); @@ -560,4 +566,57 @@ mod tests { zend::ddog_php_test_free_fake_zend_execute_data(fake_execute_data); } } + + #[test] + fn test_extract_function_name_short_string() { + unsafe { + let func = ddog_php_test_create_fake_zend_function_with_name_len(10); + assert!(!func.is_null()); + + let name = extract_function_name(&*func).expect("should extract name"); + assert_eq!(name, "xxxxxxxxxx"); + + ddog_php_test_free_fake_zend_function(func); + } + } + + #[test] + fn test_extract_function_name_at_limit_minus_one() { + unsafe { + let func = ddog_php_test_create_fake_zend_function_with_name_len(STR_LEN_LIMIT - 1); + assert!(!func.is_null()); + + let name = extract_function_name(&*func).expect("should extract name"); + assert_eq!(name.len(), STR_LEN_LIMIT - 1); + assert_ne!(name, COW_LARGE_STRING); + + ddog_php_test_free_fake_zend_function(func); + } + } + + #[test] + fn test_extract_function_name_at_limit() { + unsafe { + let func = ddog_php_test_create_fake_zend_function_with_name_len(STR_LEN_LIMIT); + assert!(!func.is_null()); + + let name = extract_function_name(&*func).expect("should return large string marker"); + assert_eq!(name, COW_LARGE_STRING); + + ddog_php_test_free_fake_zend_function(func); + } + } + + #[test] + fn test_extract_function_name_over_limit() { + unsafe { + let func = ddog_php_test_create_fake_zend_function_with_name_len(STR_LEN_LIMIT + 1000); + assert!(!func.is_null()); + + let name = extract_function_name(&*func).expect("should return large string marker"); + assert_eq!(name, COW_LARGE_STRING); + + ddog_php_test_free_fake_zend_function(func); + } + } }