@@ -108,19 +108,47 @@ DWORD tcp_channel_client_close(Channel *channel, Packet *request, LPVOID context
108108{
109109 TcpClientContext * ctx = (TcpClientContext * )context ;
110110
111- dprintf ( "[TCP] tcp_channel_client_close. channel=0x%08X, ctx=0x%08X " , channel , ctx );
111+ dprintf ("[TCP] tcp_channel_client_close. channel=%p id=%u ctx=%p " , ( void * ) channel , met_api -> channel . get_id ( channel ), ( void * ) ctx );
112112
113113 if (ctx )
114114 {
115- // Set the context channel to NULL so we don't try to close the
116- // channel (since it's already being closed)
117- ctx -> channel = NULL ;
118-
119- // Free the context
120- free_tcp_client_context (ctx );
121-
122- // Set the native channel operations context to NULL
115+ // The framework has told us to close this channel. There are two
116+ // possibilities:
117+ // 1. Only this path is running (no local peer close in flight).
118+ // We must fully tear down the context here.
119+ // 2. tcp_channel_client_local_notify is also running because the
120+ // local peer closed at roughly the same time. In that case we
121+ // must NOT double-free the context: the local-notify thread
122+ // owns cleanup and will observe the closed fd/NULL channel.
123+ //
124+ // Atomically claim ownership of ctx->channel. If we swap a non-NULL
125+ // value, no other thread has started teardown; we own it.
126+ // If we swap NULL, the local-notify path already claimed cleanup;
127+ // we must leave ctx alone.
128+ PVOID prev = InterlockedExchangePointer ((PVOID * )& ctx -> channel , NULL );
129+
130+ // Detach the native context from the channel regardless, so that
131+ // channel_destroy's subsequent free of `channel` cannot be observed
132+ // via ops->context from any other path.
123133 met_api -> channel .set_native_io_context (channel , NULL );
134+
135+ if (prev != NULL )
136+ {
137+ // We won the race: no local-notify teardown is in flight. Free
138+ // the context ourselves.
139+ free_tcp_client_context (ctx );
140+ }
141+ else
142+ {
143+ // The local-notify thread is (or was) handling teardown. Close
144+ // the socket to wake it up in case it's still blocked on recv,
145+ // but do NOT free ctx - that thread owns it.
146+ if (ctx -> fd )
147+ {
148+ closesocket (ctx -> fd );
149+ ctx -> fd = 0 ;
150+ }
151+ }
124152 }
125153
126154 return ERROR_SUCCESS ;
@@ -185,15 +213,44 @@ DWORD tcp_channel_client_local_notify(Remote * remote, TcpClientContext * ctx)
185213
186214 if (dwBytesRead == 0 )
187215 {
188- dprintf ("[TCP] tcp_channel_client_local_notify. [closed] channel=0x%08X read=0x%.8x" , ctx -> channel , dwBytesRead );
189-
190- // Set the native channel operations context to NULL
191- met_api -> channel .set_native_io_context (ctx -> channel , NULL );
216+ // The local peer closed the connection, OR tcp_channel_client_close
217+ // (running on the packet dispatcher thread in response to a framework
218+ // core_channel_close) closed our fd out from under us to wake us up.
219+ //
220+ // Atomically claim ownership of ctx->channel. If we swap a non-NULL
221+ // value the framework hasn't started closing this channel yet, so we
222+ // must notify it. If we swap NULL, the framework is (or was) already
223+ // closing this channel and its tcp_channel_client_close ran; that
224+ // path has left cleanup of ctx to us but is no longer using the
225+ // Channel struct.
226+ Channel * chan = (Channel * )InterlockedExchangePointer ((PVOID * )& ctx -> channel , NULL );
227+
228+ dprintf ("[TCP] tcp_channel_client_local_notify. [closed] chan=%p fd=%llu read=0x%.8x" ,
229+ (void * )chan , (unsigned long long )ctx -> fd , dwBytesRead );
230+
231+ if (chan != NULL )
232+ {
233+ // We won the race. Detach `chan` from ctx first so that a
234+ // core_channel_close request arriving between now and the
235+ // channel_close send below will see ops->context == NULL and
236+ // short-circuit tcp_channel_client_close.
237+ //
238+ // After set_native_io_context, `chan` is still valid: only
239+ // channel_destroy frees the Channel struct, and channel_destroy
240+ // is invoked from a request handler that runs on the packet
241+ // dispatcher thread. That thread has not yet processed our
242+ // upcoming close notification, so `chan` cannot be freed until
243+ // we return from this function and the packet is round-tripped.
244+ met_api -> channel .set_native_io_context (chan , NULL );
245+ met_api -> channel .close (chan , remote , NULL , 0 , NULL );
246+ }
192247
193- // Sleep for a quarter second
248+ // Sleep briefly to let the framework drain buffered channel data
249+ // before we tear down the underlying socket.
194250 Sleep (250 );
195251
196- // Free the context
252+ // We own cleanup of ctx now; free_socket_context will skip the
253+ // channel.close branch because ctx->channel is NULL.
197254 free_tcp_client_context (ctx );
198255
199256 // Stop processing
@@ -418,6 +475,9 @@ VOID free_socket_context(SocketContext *ctx)
418475{
419476 dprintf ("[TCP] free_socket_context. ctx=0x%08X" , ctx );
420477
478+ // Capture fd before closing so it can be logged accurately below
479+ SOCKET originalFd = ctx -> fd ;
480+
421481 // Close the socket and notification handle
422482 if (ctx -> fd )
423483 {
@@ -427,6 +487,7 @@ VOID free_socket_context(SocketContext *ctx)
427487
428488 if (ctx -> channel )
429489 {
490+ dprintf ("[TCP] free_socket_context. closing channel=%p id=%u fd=%llu" , (void * )ctx -> channel , met_api -> channel .get_id (ctx -> channel ), (unsigned long long )originalFd );
430491 met_api -> channel .close (ctx -> channel , ctx -> remote , NULL , 0 , NULL );
431492 ctx -> channel = NULL ;
432493 }
0 commit comments