From 14dd0ccabad889429d213891cdd25682ac9eb03f Mon Sep 17 00:00:00 2001 From: Alexandre Rulleau Date: Fri, 24 Apr 2026 12:56:19 +0200 Subject: [PATCH 1/7] fix(live-debugger): guard against NULL sidecar in diagnostics calls DDTRACE_G(sidecar) is NULL until ddtrace_sidecar_ensure_active() runs in RINIT. In minimal install environments the sidecar may not be initialized when live debugger diagnostics are sent. Use the global sidecar fallback (ddtrace_sidecar_for_signal) and skip the call if both are NULL. --- ext/live_debugger.c | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/ext/live_debugger.c b/ext/live_debugger.c index f1b80ee393..f5608f0dfc 100644 --- a/ext/live_debugger.c +++ b/ext/live_debugger.c @@ -154,7 +154,10 @@ static void dd_probe_resolved(void *data, bool found) { def->probe.status_msg = DDOG_CHARSLICE_C("Method does not exist on the given class"); def->probe.status_exception = DDOG_CHARSLICE_C("METHOD_NOT_FOUND"); } - ddog_send_debugger_diagnostics(DDTRACE_G(remote_config_state), &DDTRACE_G(sidecar), ddtrace_sidecar_instance_id, DDTRACE_G(sidecar_queue_id), &def->probe, ddtrace_nanoseconds_realtime() / 1000000); + ddog_SidecarTransport *sidecar = DDTRACE_G(sidecar) ? DDTRACE_G(sidecar) : ddtrace_sidecar_for_signal; + if (sidecar) { + ddog_send_debugger_diagnostics(DDTRACE_G(remote_config_state), &sidecar, ddtrace_sidecar_instance_id, DDTRACE_G(sidecar_queue_id), &def->probe, ddtrace_nanoseconds_realtime() / 1000000); + } } static int64_t dd_init_live_debugger_probe(const ddog_Probe *probe, dd_probe_def *def, zai_hook_begin begin, zai_hook_end end, void (*def_dtor)(void *), size_t dynamic) { @@ -204,15 +207,21 @@ static int64_t dd_init_live_debugger_probe(const ddog_Probe *probe, dd_probe_def def->probe.status = DDOG_PROBE_STATUS_ERROR; def->probe.status_msg = DDOG_CHARSLICE_C("Method does not exist on the given class"); def->probe.status_exception = DDOG_CHARSLICE_C("METHOD_NOT_FOUND"); -error: - ddog_send_debugger_diagnostics(DDTRACE_G(remote_config_state), &DDTRACE_G(sidecar), ddtrace_sidecar_instance_id, DDTRACE_G(sidecar_queue_id), &def->probe, ddtrace_nanoseconds_realtime() / 1000000); +error: ; + ddog_SidecarTransport *sidecar = DDTRACE_G(sidecar) ? DDTRACE_G(sidecar) : ddtrace_sidecar_for_signal; + if (sidecar) { + ddog_send_debugger_diagnostics(DDTRACE_G(remote_config_state), &sidecar, ddtrace_sidecar_instance_id, DDTRACE_G(sidecar_queue_id), &def->probe, ddtrace_nanoseconds_realtime() / 1000000); + } def_dtor(def); return -1; } if (def->probe.status != DDOG_PROBE_STATUS_INSTALLED) { def->probe.status = DDOG_PROBE_STATUS_RECEIVED; - ddog_send_debugger_diagnostics(DDTRACE_G(remote_config_state), &DDTRACE_G(sidecar), ddtrace_sidecar_instance_id, DDTRACE_G(sidecar_queue_id), &def->probe, ddtrace_nanoseconds_realtime() / 1000000); + ddog_SidecarTransport *sidecar = DDTRACE_G(sidecar) ? DDTRACE_G(sidecar) : ddtrace_sidecar_for_signal; + if (sidecar) { + ddog_send_debugger_diagnostics(DDTRACE_G(remote_config_state), &sidecar, ddtrace_sidecar_instance_id, DDTRACE_G(sidecar_queue_id), &def->probe, ddtrace_nanoseconds_realtime() / 1000000); + } } zend_hash_index_add_new_ptr(&DDTRACE_G(active_rc_hooks), id, def); @@ -222,7 +231,10 @@ static int64_t dd_init_live_debugger_probe(const ddog_Probe *probe, dd_probe_def static void dd_probe_mark_active(dd_probe_def *def) { if (def->probe.status != DDOG_PROBE_STATUS_EMITTING) { def->probe.status = DDOG_PROBE_STATUS_EMITTING; - ddog_send_debugger_diagnostics(DDTRACE_G(remote_config_state), &DDTRACE_G(sidecar), ddtrace_sidecar_instance_id, DDTRACE_G(sidecar_queue_id), &def->probe, ddtrace_nanoseconds_realtime() / 1000000); + ddog_SidecarTransport *sidecar = DDTRACE_G(sidecar) ? DDTRACE_G(sidecar) : ddtrace_sidecar_for_signal; + if (sidecar) { + ddog_send_debugger_diagnostics(DDTRACE_G(remote_config_state), &sidecar, ddtrace_sidecar_instance_id, DDTRACE_G(sidecar_queue_id), &def->probe, ddtrace_nanoseconds_realtime() / 1000000); + } } } From 1f7e2be7f5bed0fee40cfc70cf3a60b9ccd812b3 Mon Sep 17 00:00:00 2001 From: Alexandre Rulleau Date: Tue, 28 Apr 2026 14:00:19 +0200 Subject: [PATCH 2/7] fix(live-debugger): skip probe installation when sidecar is unavailable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of guarding each ddog_send_debugger_diagnostics call against a NULL sidecar, bail out of dd_init_live_debugger_probe early if the sidecar isn't ready. This ensures probes are never installed in a half-functional state — the RC machinery will re-apply them on the next request once the sidecar has connected. Async callbacks (dd_probe_mark_active, dd_probe_resolved) retain their null guards since they fire outside of request initialization order. --- ext/live_debugger.c | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/ext/live_debugger.c b/ext/live_debugger.c index f5608f0dfc..aa526c64cd 100644 --- a/ext/live_debugger.c +++ b/ext/live_debugger.c @@ -168,6 +168,11 @@ static int64_t dd_init_live_debugger_probe(const ddog_Probe *probe, dd_probe_def def->scope = NULL; def->removed = false; + if (!DDTRACE_G(sidecar)) { + def_dtor(def); + return -1; + } + const ddog_ProbeTarget *target = &probe->target; if (target->type_name.len) { if (!ddog_type_can_be_instrumented(DDTRACE_G(remote_config_state), target->type_name)) { @@ -208,20 +213,14 @@ static int64_t dd_init_live_debugger_probe(const ddog_Probe *probe, dd_probe_def def->probe.status_msg = DDOG_CHARSLICE_C("Method does not exist on the given class"); def->probe.status_exception = DDOG_CHARSLICE_C("METHOD_NOT_FOUND"); error: ; - ddog_SidecarTransport *sidecar = DDTRACE_G(sidecar) ? DDTRACE_G(sidecar) : ddtrace_sidecar_for_signal; - if (sidecar) { - ddog_send_debugger_diagnostics(DDTRACE_G(remote_config_state), &sidecar, ddtrace_sidecar_instance_id, DDTRACE_G(sidecar_queue_id), &def->probe, ddtrace_nanoseconds_realtime() / 1000000); - } + ddog_send_debugger_diagnostics(DDTRACE_G(remote_config_state), &DDTRACE_G(sidecar), ddtrace_sidecar_instance_id, DDTRACE_G(sidecar_queue_id), &def->probe, ddtrace_nanoseconds_realtime() / 1000000); def_dtor(def); return -1; } if (def->probe.status != DDOG_PROBE_STATUS_INSTALLED) { def->probe.status = DDOG_PROBE_STATUS_RECEIVED; - ddog_SidecarTransport *sidecar = DDTRACE_G(sidecar) ? DDTRACE_G(sidecar) : ddtrace_sidecar_for_signal; - if (sidecar) { - ddog_send_debugger_diagnostics(DDTRACE_G(remote_config_state), &sidecar, ddtrace_sidecar_instance_id, DDTRACE_G(sidecar_queue_id), &def->probe, ddtrace_nanoseconds_realtime() / 1000000); - } + ddog_send_debugger_diagnostics(DDTRACE_G(remote_config_state), &DDTRACE_G(sidecar), ddtrace_sidecar_instance_id, DDTRACE_G(sidecar_queue_id), &def->probe, ddtrace_nanoseconds_realtime() / 1000000); } zend_hash_index_add_new_ptr(&DDTRACE_G(active_rc_hooks), id, def); From b71f4b96e34f4b1d04bd4eedadcda87deec38f76 Mon Sep 17 00:00:00 2001 From: Alexandre Rulleau Date: Tue, 28 Apr 2026 14:09:38 +0200 Subject: [PATCH 3/7] fix(live-debugger): remove null guards from async probe callbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dd_probe_resolved and dd_probe_mark_active only fire after a probe was successfully installed, which now requires a non-NULL sidecar. A NULL sidecar in these callbacks indicates a lifecycle bug — surface it rather than silently skipping the diagnostic. --- ext/live_debugger.c | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/ext/live_debugger.c b/ext/live_debugger.c index aa526c64cd..8e66e06921 100644 --- a/ext/live_debugger.c +++ b/ext/live_debugger.c @@ -154,10 +154,7 @@ static void dd_probe_resolved(void *data, bool found) { def->probe.status_msg = DDOG_CHARSLICE_C("Method does not exist on the given class"); def->probe.status_exception = DDOG_CHARSLICE_C("METHOD_NOT_FOUND"); } - ddog_SidecarTransport *sidecar = DDTRACE_G(sidecar) ? DDTRACE_G(sidecar) : ddtrace_sidecar_for_signal; - if (sidecar) { - ddog_send_debugger_diagnostics(DDTRACE_G(remote_config_state), &sidecar, ddtrace_sidecar_instance_id, DDTRACE_G(sidecar_queue_id), &def->probe, ddtrace_nanoseconds_realtime() / 1000000); - } + ddog_send_debugger_diagnostics(DDTRACE_G(remote_config_state), &DDTRACE_G(sidecar), ddtrace_sidecar_instance_id, DDTRACE_G(sidecar_queue_id), &def->probe, ddtrace_nanoseconds_realtime() / 1000000); } static int64_t dd_init_live_debugger_probe(const ddog_Probe *probe, dd_probe_def *def, zai_hook_begin begin, zai_hook_end end, void (*def_dtor)(void *), size_t dynamic) { @@ -230,10 +227,7 @@ error: ; static void dd_probe_mark_active(dd_probe_def *def) { if (def->probe.status != DDOG_PROBE_STATUS_EMITTING) { def->probe.status = DDOG_PROBE_STATUS_EMITTING; - ddog_SidecarTransport *sidecar = DDTRACE_G(sidecar) ? DDTRACE_G(sidecar) : ddtrace_sidecar_for_signal; - if (sidecar) { - ddog_send_debugger_diagnostics(DDTRACE_G(remote_config_state), &sidecar, ddtrace_sidecar_instance_id, DDTRACE_G(sidecar_queue_id), &def->probe, ddtrace_nanoseconds_realtime() / 1000000); - } + ddog_send_debugger_diagnostics(DDTRACE_G(remote_config_state), &DDTRACE_G(sidecar), ddtrace_sidecar_instance_id, DDTRACE_G(sidecar_queue_id), &def->probe, ddtrace_nanoseconds_realtime() / 1000000); } } From a907174f446d498864e5117e25923b39fb93e8a0 Mon Sep 17 00:00:00 2001 From: Alexandre Rulleau Date: Tue, 28 Apr 2026 14:16:36 +0200 Subject: [PATCH 4/7] fix: remove stray semicolon after error label --- ext/live_debugger.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/live_debugger.c b/ext/live_debugger.c index 8e66e06921..ca6a95f2ea 100644 --- a/ext/live_debugger.c +++ b/ext/live_debugger.c @@ -209,7 +209,7 @@ static int64_t dd_init_live_debugger_probe(const ddog_Probe *probe, dd_probe_def def->probe.status = DDOG_PROBE_STATUS_ERROR; def->probe.status_msg = DDOG_CHARSLICE_C("Method does not exist on the given class"); def->probe.status_exception = DDOG_CHARSLICE_C("METHOD_NOT_FOUND"); -error: ; +error: ddog_send_debugger_diagnostics(DDTRACE_G(remote_config_state), &DDTRACE_G(sidecar), ddtrace_sidecar_instance_id, DDTRACE_G(sidecar_queue_id), &def->probe, ddtrace_nanoseconds_realtime() / 1000000); def_dtor(def); return -1; From d2eb7cbe23ea65ec22e0ff2b361a48499ea454e7 Mon Sep 17 00:00:00 2001 From: Alexandre Rulleau Date: Tue, 28 Apr 2026 16:09:59 +0200 Subject: [PATCH 5/7] fix(live-debugger): deduplicate probe installation from RC retries The RC state machine calls add_probe again for probes that are in RECEIVED state (hook installed but class not yet resolved). This produces a spurious second RECEIVED diagnostic for class-targeted probes, visible in debugger_span_probe_class.phpt. Guard dd_init_live_debugger_probe against re-installing a probe whose ID is already present in active_rc_hooks, returning the existing hook ID so the RC machinery can track it correctly. --- ext/live_debugger.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ext/live_debugger.c b/ext/live_debugger.c index ca6a95f2ea..90ee0c174e 100644 --- a/ext/live_debugger.c +++ b/ext/live_debugger.c @@ -170,6 +170,22 @@ static int64_t dd_init_live_debugger_probe(const ddog_Probe *probe, dd_probe_def return -1; } + // Deduplicate: the RC state machine retries pending (RECEIVED-but-not-INSTALLED) probes on + // every ddog_process_remote_configs() call. Skip re-installation if already in active_rc_hooks. + { + zend_ulong existing_id; + zend_string *key_str; + dd_probe_def *existing; + ZEND_HASH_FOREACH_KEY_PTR(&DDTRACE_G(active_rc_hooks), existing_id, key_str, existing) { + (void)key_str; + if (ZSTR_LEN(existing->probe_id) == probe->id.len + && memcmp(ZSTR_VAL(existing->probe_id), probe->id.ptr, probe->id.len) == 0) { + def_dtor(def); + return (int64_t)existing_id; + } + } ZEND_HASH_FOREACH_END(); + } + const ddog_ProbeTarget *target = &probe->target; if (target->type_name.len) { if (!ddog_type_can_be_instrumented(DDTRACE_G(remote_config_state), target->type_name)) { From ce1afd740be3db01d908bd7df019b8330f1c3cce Mon Sep 17 00:00:00 2001 From: Alexandre Rulleau Date: Tue, 28 Apr 2026 16:20:39 +0200 Subject: [PATCH 6/7] =?UTF-8?q?fix(live-debugger):=20remove=20NULL=20sidec?= =?UTF-8?q?ar=20guard=20=E2=80=94=20sidecar=20is=20available=20at=20probe?= =?UTF-8?q?=20install=20time?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ext/live_debugger.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/ext/live_debugger.c b/ext/live_debugger.c index 90ee0c174e..594bbaa593 100644 --- a/ext/live_debugger.c +++ b/ext/live_debugger.c @@ -165,11 +165,6 @@ static int64_t dd_init_live_debugger_probe(const ddog_Probe *probe, dd_probe_def def->scope = NULL; def->removed = false; - if (!DDTRACE_G(sidecar)) { - def_dtor(def); - return -1; - } - // Deduplicate: the RC state machine retries pending (RECEIVED-but-not-INSTALLED) probes on // every ddog_process_remote_configs() call. Skip re-installation if already in active_rc_hooks. { From 299b2157eef411200de02da6640bd919641bfb4f Mon Sep 17 00:00:00 2001 From: Alexandre Rulleau Date: Wed, 29 Apr 2026 15:07:45 +0200 Subject: [PATCH 7/7] fix(live-debugger): remove stale active_rc_hooks entry on probe removal zai_hook_remove() frees def via dd_probe_dtor, but the entry in active_rc_hooks was left dangling. The deduplication guard in dd_init_live_debugger_probe iterates active_rc_hooks by probe_id to avoid reinstalling already-active probes; if a probe was removed and re-added (RC state machine retry after removal), the guard read the freed def, causing use-after-free detected by valgrind. --- ext/live_debugger.c | 1 + 1 file changed, 1 insertion(+) diff --git a/ext/live_debugger.c b/ext/live_debugger.c index 594bbaa593..d2df5f6db9 100644 --- a/ext/live_debugger.c +++ b/ext/live_debugger.c @@ -868,6 +868,7 @@ static void dd_remove_live_debugger_probe(int64_t id) { def->scope ? (zai_str)ZAI_STR_FROM_ZSTR(def->scope) : (zai_str)ZAI_STR_EMPTY, def->function ? (zai_str)ZAI_STR_FROM_ZSTR(def->function) : (zai_str)ZAI_STR_EMPTY, id); + zend_hash_index_del(&DDTRACE_G(active_rc_hooks), (zend_ulong)id); if (scope) { zend_string_release(scope); }