Summary
Race condition in PooledSocket.DisposeSocket() can throw NullReferenceException from cancellation callback and crash process
System.NullReferenceException: Object reference not set to an instance of an object.
at Enyim.Caching.Memcached.PooledSocket.DisposeSocket()
at System.Threading.CancellationTokenSource.Invoke(Delegate d, Object state, CancellationTokenSource source)
Suspected root cause
In PooledSocket.Connect():
a timeout CTS is created (CancelAfter(...))
callback calls DisposeSocket() when _socket != null && !_socket.Connected
DisposeSocket() currently does:
_isSocketDisposed = true;
_socket.Dispose();
_socket = null;
This is not thread-safe/idempotent.
The callback’s check-then-act on _socket can race with other cleanup paths (Destroy / Dispose(true) / another callback), so _socket may become null between check and _socket.Dispose(), causing NullReferenceException.
The exception is thrown from CancellationTokenSource callback execution path (Invoke/ExecuteCallbackHandlers) and can surface as unhandled process-level failure.
Repro notes
PooledSocket.Connect() (short connection timeout / unreachable endpoint)
concurrent disposal (Destroy())
This repeatedly triggers callback/dispose interleavings and can crash test host with stack frames in PooledSocket.DisposeSocket + CTS callback handlers.
Code Snippet and potential fix
private void DisposeSocket()
{
// Only the thread that transitions 0->1 proceeds; all others return immediately.
if (Interlocked.Exchange(ref _disposeState, 1) != 0)
return;
var socket = Interlocked.Exchange(ref _socket, null);
socket?.Dispose();
}
Summary
Race condition in PooledSocket.DisposeSocket() can throw NullReferenceException from cancellation callback and crash process
System.NullReferenceException: Object reference not set to an instance of an object.
at Enyim.Caching.Memcached.PooledSocket.DisposeSocket()
at System.Threading.CancellationTokenSource.Invoke(Delegate d, Object state, CancellationTokenSource source)
Suspected root cause
In PooledSocket.Connect():
a timeout CTS is created (CancelAfter(...))
callback calls DisposeSocket() when _socket != null && !_socket.Connected
DisposeSocket() currently does:
_isSocketDisposed = true;
_socket.Dispose();
_socket = null;
This is not thread-safe/idempotent.
The callback’s check-then-act on _socket can race with other cleanup paths (Destroy / Dispose(true) / another callback), so _socket may become null between check and _socket.Dispose(), causing NullReferenceException.
The exception is thrown from CancellationTokenSource callback execution path (Invoke/ExecuteCallbackHandlers) and can surface as unhandled process-level failure.
Repro notes
PooledSocket.Connect() (short connection timeout / unreachable endpoint)
concurrent disposal (Destroy())
This repeatedly triggers callback/dispose interleavings and can crash test host with stack frames in PooledSocket.DisposeSocket + CTS callback handlers.
Code Snippet and potential fix
private void DisposeSocket()
{
// Only the thread that transitions 0->1 proceeds; all others return immediately.
if (Interlocked.Exchange(ref _disposeState, 1) != 0)
return;
var socket = Interlocked.Exchange(ref _socket, null);
socket?.Dispose();
}