Skip to content

Fix TcpTransportListener resource leak in ServerBase.StopAsync#3561

Merged
marcschier merged 1 commit into
OPCFoundation:masterfrom
RicoSuter:fix/tcp-listener-dispose-in-stopasync
Feb 17, 2026
Merged

Fix TcpTransportListener resource leak in ServerBase.StopAsync#3561
marcschier merged 1 commit into
OPCFoundation:masterfrom
RicoSuter:fix/tcp-listener-dispose-in-stopasync

Conversation

@RicoSuter

@RicoSuter RicoSuter commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

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.

This leaks per-listener resources that only Dispose() cleans up:

Resource Close() (via Stop()) Dispose()
m_listeningSocket Disposed ✓ Disposed ✓
m_listeningSocketIPv6 Disposed ✓ Disposed ✓
m_inactivityDetectionTimer Not disposed Disposed ✓
m_channels (all TcpListenerChannels) Not disposed Disposed ✓

The bug is TCP-specific. The two ITransportListener implementations handle Close() differently:

  • HttpsTransportListener: Close()Stop()Dispose()Close already triggers full disposal
  • TcpTransportListener: Close()Stop() — only closes listening sockets, does not call Dispose()

So for TCP listeners, Close() stops accepting new connections but does not clean up existing channels or the inactivity detection timer. And the subsequent listeners.Clear() prevents ServerBase.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 existing Close() call in StopAsync. This ensures TcpTransportListener.Dispose() runs (cleaning up channels and timers) before the list is cleared.

For HTTPS listeners, the additional Dispose() is a harmless no-op since Close() already disposed everything. Both TcpTransportListener.Dispose() and HttpsTransportListener.Dispose() are idempotent (null-check-before-dispose pattern).

Alternative

An alternative fix would be to remove the listeners.Clear() call from StopAsync, letting ServerBase.Dispose() handle disposal via its existing Utils.SilentDispose loop. However, this would leave already-closed listeners in the list between StopAsync and Dispose, which could be accessed by concurrent code paths (e.g., OnCertificateUpdateAsync, SessionChannelKeepAliveEvent). The chosen approach is more explicit: StopAsync fully 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 active TcpListenerChannel instances (including their sockets, crypto state, and send/receive buffers).

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

codecov Bot commented Feb 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 64.80%. Comparing base (d25caff) to head (a0ecc3e).
⚠️ Report is 120 commits behind head on master.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@marcschier
marcschier merged commit 47877a2 into OPCFoundation:master Feb 17, 2026
93 checks passed
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants