Skip to content

Commit 617ba06

Browse files
realFlowControlclaudemorrisonlevi
authored
test(profiling): add function name length tests (#3571)
Co-authored-by: Claude Opus 4.5 <[email protected]> Co-authored-by: Levi Morrison <[email protected]>
1 parent 269b7df commit 617ba06

2 files changed

Lines changed: 143 additions & 13 deletions

File tree

profiling/src/php_ffi.c

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -390,19 +390,68 @@ uintptr_t *ddog_test_php_prof_function_run_time_cache(zend_function const *func)
390390
}
391391
#endif
392392

393-
#if CFG_STACK_WALKING_TESTS
393+
#if CFG_STACK_WALKING_TESTS || defined(CFG_TEST)
394394
static int (*og_snprintf)(char *, size_t, const char *, ...);
395395

396-
// "weak" let's us polyfill, needed by zend_string_init(..., persistent: 1).
397-
void *__attribute__((weak)) __zend_malloc(size_t len) {
398-
void *tmp = malloc(len);
399-
if (EXPECTED(tmp || !len)) {
400-
return tmp;
401-
}
396+
static ZEND_COLD ZEND_NORETURN void out_of_memory(void) {
402397
fprintf(stderr, "Out of memory\n");
403398
exit(1);
404399
}
405400

401+
static zend_string *test_zend_string_alloc(size_t len) {
402+
// We don't need to handle large strings.
403+
if (len > INT32_MAX) {
404+
out_of_memory();
405+
}
406+
407+
// zend_string logically has a flexible array member val, but it uses the
408+
// so-called "struct hack" instead. So we calculate the number of bytes
409+
// needed to get to `val`, then add the `len` plus 1 (for a null byte).
410+
uint32_t alloc_len = offsetof(zend_string, val) + len + 1;
411+
412+
// Need to round up to 8 bytes on 64-bit platforms, won't overflow because
413+
// of above large string check.
414+
uint32_t rounded = (alloc_len + UINT32_C(7)) & ~UINT32_C(7);
415+
zend_string *zs = calloc(rounded, 1);
416+
if (!zs) {
417+
out_of_memory();
418+
}
419+
420+
// Initialize the refcounted header
421+
#if PHP_VERSION_ID < 70299
422+
GC_REFCOUNT(zs) = 1;
423+
#else
424+
GC_SET_REFCOUNT(zs, 1);
425+
#endif
426+
427+
#if PHP_VERSION_ID < 70200
428+
#undef GC_FLAGS_SHIFT
429+
#define GC_FLAGS_SHIFT 8
430+
#endif
431+
432+
#if PHP_VERSION_ID < 80000
433+
#undef GC_STRING
434+
#define GC_STRING IS_STRING
435+
#endif
436+
437+
// IS_STR_PERSISTENT means it's allocated with malloc, not emalloc.
438+
GC_TYPE_INFO(zs) = GC_STRING | (IS_STR_PERSISTENT << GC_FLAGS_SHIFT);
439+
440+
zs->h = 0;
441+
zs->len = len;
442+
ZSTR_VAL(zs)[len] = '\0';
443+
444+
return zs;
445+
}
446+
447+
// Manually create a zend_string without using zend_string_init(), since we
448+
// do not link against PHP at test time
449+
static zend_string *test_zend_string_create(const char *str, size_t len) {
450+
zend_string *zstr = test_zend_string_alloc(len);
451+
memcpy(ZSTR_VAL(zstr), str, len);
452+
return zstr;
453+
}
454+
406455
static zend_execute_data *create_fake_frame(int depth) {
407456
zend_execute_data *execute_data = calloc(1, sizeof(zend_execute_data));
408457
zend_op_array *op_array = calloc(1, sizeof(zend_function));
@@ -412,11 +461,11 @@ static zend_execute_data *create_fake_frame(int depth) {
412461
char buffer[64] = {0};
413462
int len = og_snprintf(buffer, sizeof buffer, "function name %03d", depth) + 1;
414463
ZEND_ASSERT(len >= 0 && sizeof buffer > (size_t)len);
415-
op_array->function_name = zend_string_init(buffer, len - 1, true);
464+
op_array->function_name = test_zend_string_create(buffer, len - 1);
416465

417466
len = og_snprintf(buffer, sizeof buffer, "filename-%03d.php", depth) + 1;
418467
ZEND_ASSERT(len >= 0 && sizeof buffer > (size_t)len);
419-
op_array->filename = zend_string_init(buffer, len - 1, true);
468+
op_array->filename = test_zend_string_create(buffer, len - 1);
420469

421470
return execute_data;
422471
}
@@ -463,7 +512,29 @@ void ddog_php_test_free_fake_zend_execute_data(zend_execute_data *execute_data)
463512

464513
free(execute_data);
465514
}
466-
#endif
515+
516+
zend_function *ddog_php_test_create_fake_zend_function_with_name_len(size_t len) {
517+
zend_op_array *op_array = calloc(1, sizeof(zend_function));
518+
if (!op_array) return NULL;
519+
520+
op_array->type = ZEND_USER_FUNCTION;
521+
522+
if (len > 0) {
523+
zend_string *zstr = test_zend_string_alloc(len);
524+
memset(ZSTR_VAL(zstr), 'x', len);
525+
op_array->function_name = zstr;
526+
}
527+
528+
return (zend_function *)op_array;
529+
}
530+
531+
void ddog_php_test_free_fake_zend_function(zend_function *func) {
532+
if (!func) return;
533+
534+
free(func->common.function_name);
535+
free(func);
536+
}
537+
#endif // CFG_STACK_WALKING_TESTS || CFG_TEST
467538

468539
void *opcache_handle = NULL;
469540

profiling/src/profiling/stack_walking.rs

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -528,14 +528,20 @@ mod detail {
528528

529529
pub use detail::*;
530530

531-
// todo: this should be feature = "stack_walking_tests" but it seemed to
532-
// cause a failure in CI to migrate it.
533-
#[cfg(all(test, stack_walking_tests))]
531+
#[cfg(test)]
534532
mod tests {
535533
use super::*;
536534
use crate::bindings as zend;
537535

536+
extern "C" {
537+
fn ddog_php_test_create_fake_zend_function_with_name_len(
538+
len: libc::size_t,
539+
) -> *mut zend::zend_function;
540+
fn ddog_php_test_free_fake_zend_function(func: *mut zend::zend_function);
541+
}
542+
538543
#[test]
544+
#[cfg(stack_walking_tests)]
539545
fn test_collect_stack_sample() {
540546
unsafe {
541547
let fake_execute_data = zend::ddog_php_test_create_fake_zend_execute_data(3);
@@ -560,4 +566,57 @@ mod tests {
560566
zend::ddog_php_test_free_fake_zend_execute_data(fake_execute_data);
561567
}
562568
}
569+
570+
#[test]
571+
fn test_extract_function_name_short_string() {
572+
unsafe {
573+
let func = ddog_php_test_create_fake_zend_function_with_name_len(10);
574+
assert!(!func.is_null());
575+
576+
let name = extract_function_name(&*func).expect("should extract name");
577+
assert_eq!(name, "xxxxxxxxxx");
578+
579+
ddog_php_test_free_fake_zend_function(func);
580+
}
581+
}
582+
583+
#[test]
584+
fn test_extract_function_name_at_limit_minus_one() {
585+
unsafe {
586+
let func = ddog_php_test_create_fake_zend_function_with_name_len(STR_LEN_LIMIT - 1);
587+
assert!(!func.is_null());
588+
589+
let name = extract_function_name(&*func).expect("should extract name");
590+
assert_eq!(name.len(), STR_LEN_LIMIT - 1);
591+
assert_ne!(name, COW_LARGE_STRING);
592+
593+
ddog_php_test_free_fake_zend_function(func);
594+
}
595+
}
596+
597+
#[test]
598+
fn test_extract_function_name_at_limit() {
599+
unsafe {
600+
let func = ddog_php_test_create_fake_zend_function_with_name_len(STR_LEN_LIMIT);
601+
assert!(!func.is_null());
602+
603+
let name = extract_function_name(&*func).expect("should return large string marker");
604+
assert_eq!(name, COW_LARGE_STRING);
605+
606+
ddog_php_test_free_fake_zend_function(func);
607+
}
608+
}
609+
610+
#[test]
611+
fn test_extract_function_name_over_limit() {
612+
unsafe {
613+
let func = ddog_php_test_create_fake_zend_function_with_name_len(STR_LEN_LIMIT + 1000);
614+
assert!(!func.is_null());
615+
616+
let name = extract_function_name(&*func).expect("should return large string marker");
617+
assert_eq!(name, COW_LARGE_STRING);
618+
619+
ddog_php_test_free_fake_zend_function(func);
620+
}
621+
}
563622
}

0 commit comments

Comments
 (0)