Skip to content

Commit f4cef38

Browse files
committed
Fix disposing of buffers in parallel with running tasks
1 parent f91b0b5 commit f4cef38

File tree

1 file changed

+52
-26
lines changed

1 file changed

+52
-26
lines changed

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

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ internal sealed class Http3RequestStream : IHttpStreamHeadersHandler, IAsyncDisp
2626
private Http3Connection _connection;
2727
private long _streamId = -1; // A stream does not have an ID until the first I/O against it. This gets set almost immediately following construction.
2828
private readonly QuicStream _stream;
29+
private volatile bool _concurrentWrite;
2930
private ArrayBuffer _sendBuffer;
31+
private volatile bool _concurrentRead;
3032
private ArrayBuffer _recvBuffer;
3133
private TaskCompletionSource<bool>? _expect100ContinueCompletionSource; // True indicates we should send content (e.g. received 100 Continue).
3234
private bool _disposed;
@@ -130,8 +132,18 @@ private void DisposeSyncHelper()
130132
{
131133
_connection.RemoveStream(_stream);
132134

133-
_sendBuffer.Dispose();
134-
_recvBuffer.Dispose();
135+
// If the request sending was offloaded to be done concurrently and not awaited withing SendAsync (by calling connection.LogException),
136+
// the _sendBuffer disposal is the responsibility of that offloaded task to prevent returning buffer to the pool while it still might be accessed by the task.
137+
if (!_concurrentWrite)
138+
{
139+
_sendBuffer.Dispose();
140+
}
141+
// If the response receiving was offloaded to be done concurrently and not awaited withing SendAsync (by calling connection.LogException),
142+
// the _recvBuffer disposal is the responsibility of that offloaded task to prevent returning buffer to the pool while it still might be accessed by the task.
143+
if (!_concurrentRead)
144+
{
145+
_recvBuffer.Dispose();
146+
}
135147
}
136148

137149
public void GoAway()
@@ -199,6 +211,7 @@ await Task.WhenAny(sendRequestTask, readResponseTask).ConfigureAwait(false) == s
199211
// Exceptions will be bubbled up from sendRequestTask here,
200212
// which means the result of readResponseTask won't be observed directly:
201213
// Do a background await to log any exceptions.
214+
_concurrentRead = true;
202215
_connection.LogExceptions(readResponseTask);
203216
throw;
204217
}
@@ -207,6 +220,7 @@ await Task.WhenAny(sendRequestTask, readResponseTask).ConfigureAwait(false) == s
207220
{
208221
// Duplex is being used, so we can't wait for content to finish sending.
209222
// Do a background await to log any exceptions.
223+
_concurrentWrite = true;
210224
_connection.LogExceptions(sendRequestTask);
211225
}
212226

@@ -226,12 +240,6 @@ await Task.WhenAny(sendRequestTask, readResponseTask).ConfigureAwait(false) == s
226240
{
227241
// The server doesn't need the whole request to respond so it's aborting its reading side gracefully, see https://datatracker.ietf.org/doc/html/rfc9114#section-4.1-15.
228242
}
229-
catch (OperationCanceledException)
230-
{
231-
// If the request got cancelled before WritesClosed completed, avoid leaking an unobserved task exception.
232-
_connection.LogExceptions(writesClosed);
233-
throw;
234-
}
235243
}
236244

237245
Debug.Assert(_response != null && _response.Content != null);
@@ -387,32 +395,44 @@ await Task.WhenAny(sendRequestTask, readResponseTask).ConfigureAwait(false) == s
387395
/// </summary>
388396
private async Task ReadResponseAsync(CancellationToken cancellationToken)
389397
{
390-
if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.ResponseHeadersStart();
391-
392-
Debug.Assert(_response == null);
393-
do
398+
try
394399
{
395-
_headerState = HeaderState.StatusHeader;
396-
397-
(Http3FrameType? frameType, long payloadLength) = await ReadFrameEnvelopeAsync(cancellationToken).ConfigureAwait(false);
400+
if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.ResponseHeadersStart();
398401

399-
if (frameType != Http3FrameType.Headers)
402+
Debug.Assert(_response == null);
403+
do
400404
{
401-
if (NetEventSource.Log.IsEnabled())
405+
_headerState = HeaderState.StatusHeader;
406+
407+
(Http3FrameType? frameType, long payloadLength) = await ReadFrameEnvelopeAsync(cancellationToken).ConfigureAwait(false);
408+
409+
if (frameType != Http3FrameType.Headers)
402410
{
403-
Trace($"Expected HEADERS as first response frame; received {frameType}.");
411+
if (NetEventSource.Log.IsEnabled())
412+
{
413+
Trace($"Expected HEADERS as first response frame; received {frameType}.");
414+
}
415+
throw new HttpIOException(HttpRequestError.InvalidResponse, SR.net_http_invalid_response);
404416
}
405-
throw new HttpIOException(HttpRequestError.InvalidResponse, SR.net_http_invalid_response);
406-
}
407417

408-
await ReadHeadersAsync(payloadLength, cancellationToken).ConfigureAwait(false);
409-
Debug.Assert(_response != null);
410-
}
411-
while ((int)_response.StatusCode < 200);
418+
await ReadHeadersAsync(payloadLength, cancellationToken).ConfigureAwait(false);
419+
Debug.Assert(_response != null);
420+
}
421+
while ((int)_response.StatusCode < 200);
412422

413-
_headerState = HeaderState.TrailingHeaders;
423+
_headerState = HeaderState.TrailingHeaders;
414424

415-
if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.ResponseHeadersStop((int)_response.StatusCode);
425+
if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.ResponseHeadersStop((int)_response.StatusCode);
426+
}
427+
finally
428+
{
429+
// No that we might still observe false here even if we're responsible for the _recvBuffer disposal.
430+
// But in that case, we just don't return the rented buffer to the pool, which is lesser evil than writing to returned one.
431+
if (_concurrentRead)
432+
{
433+
_recvBuffer.Dispose();
434+
}
435+
}
416436
}
417437

418438
private async Task SendContentAsync(HttpContent content, CancellationToken cancellationToken)
@@ -491,6 +511,12 @@ private async Task SendContentAsync(HttpContent content, CancellationToken cance
491511
}
492512
finally
493513
{
514+
// No that we might still observe false here even if we're responsible for the _sendBuffer disposal.
515+
// But in that case, we just don't return the rented buffer to the pool, which is lesser evil than writing to returned one.
516+
if (_concurrentWrite)
517+
{
518+
_sendBuffer.Dispose();
519+
}
494520
_requestSendCompleted = true;
495521
RemoveFromConnectionIfDone();
496522
}

0 commit comments

Comments
 (0)