Skip to content

Commit 2b94204

Browse files
Fix Http2 deadlock (#100086)
Co-authored-by: Miha Zupan <[email protected]>
1 parent d49f6cf commit 2b94204

File tree

2 files changed

+24
-5
lines changed

2 files changed

+24
-5
lines changed

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ private void Shutdown()
257257
if (NetEventSource.Log.IsEnabled()) Trace($"{nameof(_shutdown)}={_shutdown}, {nameof(_abortException)}={_abortException}");
258258

259259
Debug.Assert(Monitor.IsEntered(SyncObject));
260+
Debug.Assert(!_pool.HasSyncObjLock);
260261

261262
if (!_shutdown)
262263
{
@@ -276,6 +277,8 @@ private void Shutdown()
276277

277278
public bool TryReserveStream()
278279
{
280+
Debug.Assert(!_pool.HasSyncObjLock);
281+
279282
lock (SyncObject)
280283
{
281284
if (_shutdown)
@@ -302,6 +305,8 @@ public bool TryReserveStream()
302305
// Otherwise, will be called when the request is complete and stream is closed.
303306
public void ReleaseStream()
304307
{
308+
Debug.Assert(!_pool.HasSyncObjLock);
309+
305310
lock (SyncObject)
306311
{
307312
if (NetEventSource.Log.IsEnabled()) Trace($"{nameof(_streamsInUse)}={_streamsInUse}");
@@ -333,6 +338,8 @@ public void ReleaseStream()
333338
// Returns false to indicate that the connection is shutting down and cannot be used anymore
334339
public Task<bool> WaitForAvailableStreamsAsync()
335340
{
341+
Debug.Assert(!_pool.HasSyncObjLock);
342+
336343
lock (SyncObject)
337344
{
338345
Debug.Assert(_availableStreamsWaiter is null, "As used currently, shouldn't already have a waiter");
@@ -730,6 +737,7 @@ private void ProcessAltSvcFrame(FrameHeader frameHeader)
730737
{
731738
if (NetEventSource.Log.IsEnabled()) Trace($"{frameHeader}");
732739
Debug.Assert(frameHeader.Type == FrameType.AltSvc);
740+
Debug.Assert(!Monitor.IsEntered(SyncObject));
733741

734742
ReadOnlySpan<byte> span = _incomingBuffer.ActiveSpan.Slice(0, frameHeader.PayloadLength);
735743

@@ -1308,6 +1316,8 @@ private Task SendRstStreamAsync(int streamId, Http2ProtocolErrorCode errorCode)
13081316

13091317
internal void HeartBeat()
13101318
{
1319+
Debug.Assert(!_pool.HasSyncObjLock);
1320+
13111321
if (_shutdown)
13121322
return;
13131323

@@ -1800,10 +1810,14 @@ private void ExtendWindow(int amount)
18001810

18011811
public override long GetIdleTicks(long nowTicks)
18021812
{
1803-
lock (SyncObject)
1804-
{
1805-
return _streamsInUse == 0 ? base.GetIdleTicks(nowTicks) : 0;
1806-
}
1813+
// The pool is holding the lock as part of its scavenging logic.
1814+
// We must not lock on Http2Connection.SyncObj here as that could lead to lock ordering problems.
1815+
Debug.Assert(_pool.HasSyncObjLock);
1816+
1817+
// There is a race condition here where the connection pool may see this connection as idle right before
1818+
// we start processing a new request and start its disposal. This is okay as we will either
1819+
// return false from TryReserveStream, or process pending requests before tearing down the transport.
1820+
return _streamsInUse == 0 ? base.GetIdleTicks(nowTicks) : 0;
18071821
}
18081822

18091823
/// <summary>Abort all streams and cause further processing to fail.</summary>
@@ -1992,6 +2006,7 @@ private static TaskCompletionSourceWithCancellation<bool> CreateSuccessfullyComp
19922006
public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken)
19932007
{
19942008
Debug.Assert(async);
2009+
Debug.Assert(!_pool.HasSyncObjLock);
19952010
if (NetEventSource.Log.IsEnabled()) Trace($"Sending request: {request}");
19962011

19972012
try

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ private object SyncObj
406406
}
407407
}
408408

409-
private bool HasSyncObjLock => Monitor.IsEntered(_availableHttp11Connections);
409+
public bool HasSyncObjLock => Monitor.IsEntered(_availableHttp11Connections);
410410

411411
// Overview of connection management (mostly HTTP version independent):
412412
//
@@ -459,6 +459,8 @@ private static void ThrowGetVersionException(HttpRequestMessage request, int des
459459

460460
private bool CheckExpirationOnGet(HttpConnectionBase connection)
461461
{
462+
Debug.Assert(!HasSyncObjLock);
463+
462464
TimeSpan pooledConnectionLifetime = _poolManager.Settings._pooledConnectionLifetime;
463465
if (pooledConnectionLifetime != Timeout.InfiniteTimeSpan)
464466
{
@@ -2000,6 +2002,7 @@ private void ReturnHttp2Connection(Http2Connection connection, bool isNewConnect
20002002
{
20012003
if (NetEventSource.Log.IsEnabled()) connection.Trace($"{nameof(isNewConnection)}={isNewConnection}");
20022004

2005+
Debug.Assert(!HasSyncObjLock);
20032006
Debug.Assert(isNewConnection || initialRequestWaiter is null, "Shouldn't have a request unless the connection is new");
20042007

20052008
if (!isNewConnection && CheckExpirationOnReturn(connection))
@@ -2403,6 +2406,7 @@ internal void HeartBeat()
24032406
localHttp2Connections = _availableHttp2Connections?.ToArray();
24042407
}
24052408

2409+
// Avoid calling HeartBeat under the lock, as it may call back into HttpConnectionPool.InvalidateHttp2Connection.
24062410
if (localHttp2Connections is not null)
24072411
{
24082412
foreach (Http2Connection http2Connection in localHttp2Connections)

0 commit comments

Comments
 (0)