Skip to content

Commit acdc1cf

Browse files
committed
fix: thread shutdown leaks
Signed-off-by: Alexandre Rulleau <[email protected]>
1 parent bc52757 commit acdc1cf

File tree

11 files changed

+121
-6
lines changed

11 files changed

+121
-6
lines changed

components-rs/sidecar.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ ddog_MaybeError ddog_sidecar_connect_worker(int32_t master_pid,
9595

9696
ddog_MaybeError ddog_sidecar_shutdown_master_listener(void);
9797

98+
ddog_MaybeError ddog_sidecar_clear_inherited_listener(void);
99+
98100
ddog_MaybeError ddog_sidecar_ping(struct ddog_SidecarTransport **transport);
99101

100102
ddog_MaybeError ddog_sidecar_flush_traces(struct ddog_SidecarTransport **transport);

dockerfiles/ci/bookworm/.env

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
BOOKWORM_CURRENT_VERSION=6
2-
BOOKWORM_NEXT_VERSION=7
1+
BOOKWORM_CURRENT_VERSION=5
2+
BOOKWORM_NEXT_VERSION=6

dockerfiles/ci/bookworm/php-8.3/suppr.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ leak:_dl_map_object_deps
55
leak:__res_context_send
66
leak:_dl_make_tlsdesc_dynamic
77
leak:_dl_catch_exception
8+
leak:__cxa_thread_atexit
9+
leak:CURRENT_PARKER
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
leak:timer_create
2+
leak:__cxa_thread_atexit
3+
leak:CURRENT_PARKER
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
leak:timer_create
2+
leak:__cxa_thread_atexit
3+
leak:CURRENT_PARKER

ext/ddtrace.c

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1605,7 +1605,11 @@ static PHP_MSHUTDOWN_FUNCTION(ddtrace) {
16051605

16061606
ddtrace_user_req_shutdown();
16071607

1608-
ddtrace_sidecar_shutdown();
1608+
// Only shutdown sidecar in MSHUTDOWN for non-CLI SAPIs.
1609+
// CLI SAPI shuts down in RSHUTDOWN to allow thread joins before ASAN checks.
1610+
if (strcmp(sapi_module.name, "cli") != 0) {
1611+
ddtrace_sidecar_shutdown();
1612+
}
16091613

16101614
ddtrace_live_debugger_mshutdown();
16111615

@@ -2643,6 +2647,9 @@ void dd_internal_handle_fork(void) {
26432647
#ifndef _WIN32
26442648
if (get_global_DD_TRACE_SIDECAR_TRACE_SENDER() && ddtrace_sidecar) {
26452649
if (is_child_process) {
2650+
// Clear inherited listener state - child doesn't own the master listener thread
2651+
ddtrace_ffi_try("Failed clearing inherited listener state", ddog_sidecar_clear_inherited_listener());
2652+
26462653
ddtrace_force_new_instance_id();
26472654

26482655
if (ddtrace_sidecar_connect_worker_after_fork()) {

ext/handlers_pcntl.c

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,92 @@ static zif_handler dd_pcntl_forkx_handler = NULL;
2727
#define JOIN_BGS_BEFORE_FORK 1
2828
#endif
2929

30+
static bool dd_master_listener_was_active = false;
31+
3032
static void dd_prefork() {
3133
#if JOIN_BGS_BEFORE_FORK
3234
if (!get_global_DD_TRACE_SIDECAR_TRACE_SENDER()) {
3335
ddtrace_coms_flush_shutdown_writer_synchronous();
3436
}
3537
#endif
38+
39+
#ifndef _WIN32
40+
// Check if master listener is active before fork
41+
dd_master_listener_was_active = (ddtrace_master_pid != 0 && getpid() == ddtrace_master_pid);
42+
43+
if (dd_master_listener_was_active) {
44+
// Shutdown master listener before fork to avoid Tokio runtime corruption.
45+
// Tokio's async runtime and fork() are fundamentally incompatible - forking
46+
// while Tokio threads are active causes state corruption in the parent process.
47+
// See: https://github.com/tokio-rs/tokio/issues/4301
48+
49+
// First close parent's worker connection to prevent handler thread deadlock
50+
if (ddtrace_sidecar) {
51+
ddog_sidecar_transport_drop(ddtrace_sidecar);
52+
ddtrace_sidecar = NULL;
53+
}
54+
55+
// Then shutdown the master listener and its Tokio runtime
56+
ddog_sidecar_shutdown_master_listener();
57+
}
58+
#endif
3659
}
3760

61+
static void dd_postfork_parent() {
62+
#ifndef _WIN32
63+
// Restart master listener in parent if it was active before fork
64+
if (dd_master_listener_was_active) {
65+
// Reinitialize master listener with fresh Tokio runtime.
66+
// This recreates the async runtime that was shut down before fork.
67+
68+
// First, restart the master listener thread
69+
if (!ddtrace_ffi_try("Failed restarting sidecar master listener after fork",
70+
ddog_sidecar_connect_master((int32_t)ddtrace_master_pid))) {
71+
LOG(WARN, "Failed to restart sidecar master listener after fork");
72+
dd_master_listener_was_active = false;
73+
return;
74+
}
75+
76+
// Then reconnect to it as a worker
77+
ddog_SidecarTransport *sidecar_transport = NULL;
78+
if (!ddtrace_ffi_try("Failed reconnecting master to sidecar after fork",
79+
ddog_sidecar_connect_worker((int32_t)ddtrace_master_pid, &sidecar_transport))) {
80+
LOG(WARN, "Failed to reconnect master process to sidecar after fork");
81+
dd_master_listener_was_active = false;
82+
return;
83+
}
84+
85+
ddtrace_sidecar = sidecar_transport;
86+
dd_master_listener_was_active = false;
87+
}
88+
#endif
89+
}
90+
91+
// Declare LSAN runtime interface functions
92+
#if defined(__SANITIZE_ADDRESS__) && !defined(_WIN32)
93+
void __lsan_disable(void);
94+
void __lsan_enable(void);
95+
#endif
96+
3897
static void dd_handle_fork(zval *return_value) {
3998
if (Z_LVAL_P(return_value) == 0) {
99+
// CHILD PROCESS
100+
// Disable ASAN leak detection in child to avoid false positives from inherited
101+
// Tokio TLS and runtime state that can't be properly cleaned up after fork
102+
#if defined(__SANITIZE_ADDRESS__) && !defined(_WIN32)
103+
// The child inherits Tokio runtime thread-local storage from the parent's
104+
// master listener and connection handlers. These TLS destructors reference
105+
// threads that don't exist in the child, causing spurious leak reports.
106+
// We disable leak detection in the child since the inherited memory will
107+
// be reclaimed when the child process exits.
108+
__lsan_disable();
109+
#endif
40110
dd_internal_handle_fork();
41111
} else {
112+
// PARENT PROCESS
113+
// Restart master listener if it was shut down before fork
114+
dd_postfork_parent();
115+
42116
#if JOIN_BGS_BEFORE_FORK
43117
if (!get_global_DD_TRACE_SIDECAR_TRACE_SENDER()) {
44118
ddtrace_coms_restart_writer();

ext/sidecar.c

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <components-rs/ddtrace.h>
1010
#include <components-rs/sidecar.h>
1111
#include <zend_string.h>
12+
#include <main/SAPI.h>
1213
#include "sidecar.h"
1314
#include "live_debugger.h"
1415
#include "telemetry.h"
@@ -153,7 +154,8 @@ static void dd_sidecar_on_reconnect(ddog_SidecarTransport *transport) {
153154

154155
}
155156

156-
static ddog_SidecarTransport *dd_sidecar_connection_factory(void);
157+
// Forward declaration (non-static for export to handlers_pcntl.c)
158+
ddog_SidecarTransport *dd_sidecar_connection_factory(void);
157159

158160
static ddog_SidecarTransport *dd_sidecar_connection_factory_ex(bool is_fork) {
159161
// Should not happen, unless the agent url is malformed
@@ -672,6 +674,28 @@ void ddtrace_sidecar_activate(void) {
672674

673675
void ddtrace_sidecar_rshutdown(void) {
674676
ddog_Vec_Tag_drop(DDTRACE_G(active_global_tags));
677+
678+
// For CLI SAPI, shut down the master listener thread here in RSHUTDOWN
679+
// since the process will exit after this request. For other SAPIs,
680+
// the master listener persists across requests and shuts down in MSHUTDOWN.
681+
if (strcmp(sapi_module.name, "cli") == 0) {
682+
#ifndef _WIN32
683+
ddtrace_pid_t current_pid = getpid();
684+
if (ddtrace_master_pid != 0 && current_pid == ddtrace_master_pid) {
685+
// CRITICAL: Close the worker connection BEFORE shutting down the master listener.
686+
// The master process has its own worker connection to the master listener.
687+
// If we don't close it first, the handler thread waiting on this connection
688+
// will never exit, causing the listener thread join to timeout.
689+
if (ddtrace_sidecar) {
690+
ddog_sidecar_transport_drop(ddtrace_sidecar);
691+
ddtrace_sidecar = NULL;
692+
}
693+
694+
ddtrace_ffi_try("Failed shutting down master listener in RSHUTDOWN",
695+
ddog_sidecar_shutdown_master_listener());
696+
}
697+
#endif
698+
}
675699
}
676700

677701
bool ddtrace_alter_test_session_token(zval *old_value, zval *new_value, zend_string *new_str) {

ext/sidecar.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ ddog_Endpoint *ddtrace_sidecar_agent_endpoint(void);
4444
void ddtrace_sidecar_submit_root_span_data_direct_defaults(ddog_SidecarTransport **transport, ddtrace_root_span_data *root);
4545
void ddtrace_sidecar_submit_root_span_data_direct(ddog_SidecarTransport **transport, ddtrace_root_span_data *root, zend_string *cfg_service, zend_string *cfg_env, zend_string *cfg_version);
4646

47+
ddog_SidecarTransport *dd_sidecar_connection_factory(void);
48+
4749
void ddtrace_sidecar_send_debugger_data(ddog_Vec_DebuggerPayload payloads);
4850
void ddtrace_sidecar_send_debugger_datum(ddog_DebuggerPayload *payload);
4951

0 commit comments

Comments
 (0)