Fix TcpTransportListener resource leak in ServerBase.StopAsync#3561
Merged
marcschier merged 1 commit intoFeb 17, 2026
Merged
Conversation
ServerBase.StopAsync() calls Close() on each transport listener then Clear() on the list. ServerBase.Dispose() later iterates TransportListeners to call Dispose() on each — but the list is already empty, so TcpTransportListener.Dispose() never runs. Add Utils.SilentDispose() after Close() in StopAsync to ensure TcpTransportListener.Dispose() executes before the list is cleared.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #3561 +/- ##
===========================================
+ Coverage 51.86% 64.80% +12.93%
===========================================
Files 370 458 +88
Lines 78618 96774 +18156
Branches 13650 16264 +2614
===========================================
+ Hits 40779 62716 +21937
+ Misses 33705 29053 -4652
- Partials 4134 5005 +871 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
marcschier
approved these changes
Feb 17, 2026
romanett
pushed a commit
that referenced
this pull request
Feb 20, 2026
ServerBase.StopAsync() calls Close() on each transport listener then Clear() on the list. ServerBase.Dispose() later iterates TransportListeners to call Dispose() on each — but the list is already empty, so TcpTransportListener.Dispose() never runs. Add Utils.SilentDispose() after Close() in StopAsync to ensure TcpTransportListener.Dispose() executes before the list is cleared.
RicoSuter
added a commit
to RicoSuter/Namotion.Interceptor
that referenced
this pull request
Jun 2, 2026
…330) The DisposeTransportListeners workaround in OpcUaStandardServer existed because ServerBase.StopAsync cleared the TransportListeners list before disposal could run, leaking timers, channels, and buffer managers. The upstream fix in OPCFoundation/UA-.NETStandard#3561 (StopAsync now closes, disposes, then clears each listener) shipped in 1.5.378.134, which this project already references, so server.Dispose() no longer leaks. Removes DisposeTransportListeners, the _savedTransportListeners field, and the post-shutdown manual disposal call. CloseTransportListeners is kept and simplified to iterate the live list. It is still required and is not affected by the upstream fix: it stops accepting new connections before sessions are closed (preventing StopAsync from hanging on client reconnects during the SDK's shutdown delay) and performs the force-kill crash simulation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ServerBase.StopAsync()callsClose()on each transport listener, thenClear()on the list.ServerBase.Dispose()later iteratesTransportListenersto callDispose()on each — but the list is already empty, soTcpTransportListener.Dispose()never runs.This leaks per-listener resources that only
Dispose()cleans up:Close()(viaStop())Dispose()m_listeningSocketm_listeningSocketIPv6m_inactivityDetectionTimerm_channels(allTcpListenerChannels)The bug is TCP-specific. The two
ITransportListenerimplementations handleClose()differently:HttpsTransportListener:Close()→Stop()→Dispose()—Closealready triggers full disposalTcpTransportListener:Close()→Stop()— only closes listening sockets, does not callDispose()So for TCP listeners,
Close()stops accepting new connections but does not clean up existing channels or the inactivity detection timer. And the subsequentlisteners.Clear()preventsServerBase.Dispose()from ever reaching them.Disclaimer: This bug has been found with hours of manual work with AI and my local OPC UA stress test application and should be safe - please review the changes as I don't really know the OPC UA code base in depth.
Fix
Add
Utils.SilentDispose(listeners[ii])after the existingClose()call inStopAsync. This ensuresTcpTransportListener.Dispose()runs (cleaning up channels and timers) before the list is cleared.For HTTPS listeners, the additional
Dispose()is a harmless no-op sinceClose()already disposed everything. BothTcpTransportListener.Dispose()andHttpsTransportListener.Dispose()are idempotent (null-check-before-dispose pattern).Alternative
An alternative fix would be to remove the
listeners.Clear()call fromStopAsync, lettingServerBase.Dispose()handle disposal via its existingUtils.SilentDisposeloop. However, this would leave already-closed listeners in the list betweenStopAsyncandDispose, which could be accessed by concurrent code paths (e.g.,OnCertificateUpdateAsync,SessionChannelKeepAliveEvent). The chosen approach is more explicit:StopAsyncfully cleans up listeners (Close + Dispose) and then clears the list, making it self-contained.Impact
Servers that call
StopAsync()(the standard hosted service shutdown path) will now properly dispose TCP transport listener resources. Without this fix, each server stop/restart cycle leaks the inactivity detection timer and all activeTcpListenerChannelinstances (including their sockets, crypto state, and send/receive buffers).