From 49cc658ced13a16e9fe932f30a8a865c88ad594f Mon Sep 17 00:00:00 2001 From: jdani-airalo Date: Mon, 8 Jun 2026 13:18:55 +0200 Subject: [PATCH] fix(span): clear stale span-stack root_span aliases on free Fixes a SIGSEGV in ddtrace_inherit_span_properties that surfaced in production (PHP 8.3 NTS, fpm-fcgi) as a GC_ADDREF on a NULL/freed pointer while copying a parent span's baggage. Root cause: a span stack keeps a NON-owning pointer to its root span (ddtrace_init_span_stack copies it without taking a reference). The parent-stack reference chain normally keeps the root span alive while any stack aliases it, but out-of-order root span drops break that invariant: ddtrace_span_alter_root_span_config() (runtime config change) and the rejection branch of ddtrace_drop_span() free the root span while only NULLing the *current* stack's pointer, leaving sibling/descendant stacks dangling. The next cross-stack inherit (ddtrace_set_root_span_properties -> ddtrace_inherit_span_properties) then dereferences the freed span. It crashes on the baggage copy specifically: with baggage populated, property_baggage points at a heap zend_array freed with the span, so the ZVAL_COPY_DEREF -> GC_ADDREF dereferences freed memory. With empty baggage it stays the immutable ZVAL_EMPTY_ARRAY default whose pointer survives the freed read -- which is why disabling baggage extraction masked the bug. Fix: when a root span object is actually freed, scrub every span stack that still aliases it so the weak pointer can never outlive the object, regardless of which drop path freed it. NULL is already the "no root span" sentinel handled throughout (e.g. ddtrace_set_root_span_properties guards on parent_root), so this introduces no new state. The scan runs once per root span free (~once per trace), mirroring ddtrace_mark_all_span_stacks_flushable. This supersedes the previous defensive guard in ddtrace_inherit_span_properties (reverted here): the malformed (refcounted-flagged, NULL/freed counted pointer) zval can no longer arise. Co-Authored-By: Claude Opus 4.8 (1M context) --- tracer/functions.c | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tracer/functions.c b/tracer/functions.c index 9c667b4269..dcf8f34744 100644 --- a/tracer/functions.c +++ b/tracer/functions.c @@ -418,7 +418,41 @@ static zend_object *ddtrace_span_stack_clone_obj(zend_object *old_obj) { return new_obj; } +// A span stack keeps a non-owning pointer to its root span (see ddtrace_init_span_stack, which +// copies it without taking a reference). Normally the parent-stack reference chain keeps the root +// span alive for as long as any stack aliases it. Out-of-order root span drops break that +// invariant: ddtrace_span_alter_root_span_config() (runtime config change) and the rejection +// branch of ddtrace_drop_span() free the root span while only NULLing the *current* stack's +// pointer, leaving sibling/descendant stacks dangling. The next cross-stack inherit +// (ddtrace_inherit_span_properties, reached from ddtrace_set_root_span_properties) then +// dereferences the freed span. It crashes on the baggage copy specifically once baggage is +// populated; an empty baggage stays the immutable ZVAL_EMPTY_ARRAY default whose pointer survives +// the freed read, which is why disabling baggage extraction masks the bug. Scrub every alias when +// the root span object is actually freed so the weak pointer can never outlive it, regardless of +// which drop path freed it. +static void dd_clear_dangling_root_span_aliases(ddtrace_root_span_data *root_span) { + zend_objects_store *objects = &EG(objects_store); + if (!objects->object_buckets) { + return; + } + zend_object **end = objects->object_buckets + 1; + zend_object **obj_ptr = objects->object_buckets + objects->top; + do { + obj_ptr--; + zend_object *obj = *obj_ptr; + if (IS_OBJ_VALID(obj) && obj->ce == ddtrace_ce_span_stack) { + ddtrace_span_stack *stack = (ddtrace_span_stack *)obj; + if (stack->root_span == root_span) { + stack->root_span = NULL; + } + } + } while (obj_ptr != end); +} + static void ddtrace_span_data_free_storage(zend_object *object) { + if (object->ce == ddtrace_ce_root_span_data) { + dd_clear_dangling_root_span_aliases((ddtrace_root_span_data *)object); + } zend_object_std_dtor(object); // Prevent use after free after zend_objects_store_free_object_storage is called (e.g. preloading) [PHP < 8.1] memset(object->properties_table, 0, sizeof(ddtrace_span_data) - XtOffsetOf(ddtrace_span_data, std.properties_table));