Skip to content

Commit b37eafd

Browse files
ManickaPamcasey
andauthored
[H/3] Fix code passed into QuicConnection.CloseAsync and QuicStream.Abort (#55282)
* Fix code passed into QuicConnection.CloseAsync and QuicStream.Abort * Validate that configured error code is used --------- Co-authored-by: Andrew Casey <andrew.casey@microsoft.com>
1 parent e6cf613 commit b37eafd

File tree

9 files changed

+138
-13
lines changed

9 files changed

+138
-13
lines changed

src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,9 @@ private void AbortCore(Exception exception, Http3ErrorCode errorCode)
166166
abortReason = new ConnectionAbortedException(exception.Message, exception);
167167
}
168168

169+
// This has the side-effect of validating the error code, so do it before we consume the error code
170+
_errorCodeFeature.Error = (long)errorCode;
171+
169172
_context.WebTransportSession?.Abort(abortReason, errorCode);
170173

171174
Log.Http3StreamAbort(TraceIdentifier, errorCode, abortReason);
@@ -181,7 +184,6 @@ private void AbortCore(Exception exception, Http3ErrorCode errorCode)
181184
RequestBodyPipe.Writer.Complete(exception);
182185

183186
// Abort framewriter and underlying transport after stopping output.
184-
_errorCodeFeature.Error = (long)errorCode;
185187
_frameWriter.Abort(abortReason);
186188
}
187189
}

src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.FeatureCollection.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ internal sealed partial class QuicConnectionContext : IProtocolErrorCodeFeature,
1616
public long Error
1717
{
1818
get => _error ?? -1;
19-
set => _error = value;
19+
set
20+
{
21+
QuicTransportOptions.ValidateErrorCode(value);
22+
_error = value;
23+
}
2024
}
2125

2226
public X509Certificate2? ClientCertificate

src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public override async ValueTask DisposeAsync()
5656
{
5757
lock (_shutdownLock)
5858
{
59+
// The DefaultCloseErrorCode setter validates that the error code is within the valid range
5960
_closeTask ??= _connection.CloseAsync(errorCode: _context.Options.DefaultCloseErrorCode).AsTask();
6061
}
6162

@@ -81,7 +82,7 @@ public override void Abort(ConnectionAbortedException abortReason)
8182
return;
8283
}
8384

84-
var resolvedErrorCode = _error ?? 0;
85+
var resolvedErrorCode = _error ?? 0; // Only valid error codes are assigned to _error
8586
_abortReason = ExceptionDispatchInfo.Capture(abortReason);
8687
QuicLog.ConnectionAbort(_log, this, resolvedErrorCode, abortReason.Message);
8788
_closeTask = _connection.CloseAsync(errorCode: resolvedErrorCode).AsTask();
@@ -130,7 +131,7 @@ public override void Abort(ConnectionAbortedException abortReason)
130131
catch (QuicException ex) when (ex.QuicError == QuicError.ConnectionAborted)
131132
{
132133
// Shutdown initiated by peer, abortive.
133-
_error = ex.ApplicationErrorCode;
134+
_error = ex.ApplicationErrorCode; // Trust Quic to provide us a valid error code
134135
QuicLog.ConnectionAborted(_log, this, ex.ApplicationErrorCode.GetValueOrDefault(), ex);
135136

136137
ThreadPool.UnsafeQueueUserWorkItem(state =>

src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.FeatureCollection.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ public OnCloseRegistration(Action<object?> callback, object? state)
3838
public long Error
3939
{
4040
get => _error ?? -1;
41-
set => _error = value;
41+
set
42+
{
43+
QuicTransportOptions.ValidateErrorCode(value);
44+
_error = value;
45+
}
4246
}
4347

4448
public long StreamId { get; private set; }
@@ -54,6 +58,8 @@ public long Error
5458

5559
public void AbortRead(long errorCode, ConnectionAbortedException abortReason)
5660
{
61+
QuicTransportOptions.ValidateErrorCode(errorCode);
62+
5763
lock (_shutdownLock)
5864
{
5965
if (_stream != null)
@@ -74,6 +80,8 @@ public void AbortRead(long errorCode, ConnectionAbortedException abortReason)
7480

7581
public void AbortWrite(long errorCode, ConnectionAbortedException abortReason)
7682
{
83+
QuicTransportOptions.ValidateErrorCode(errorCode);
84+
7785
lock (_shutdownLock)
7886
{
7987
if (_stream != null)

src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ private async ValueTask DoReceiveAsync()
273273
catch (QuicException ex) when (ex.QuicError is QuicError.StreamAborted or QuicError.ConnectionAborted)
274274
{
275275
// Abort from peer.
276-
_error = ex.ApplicationErrorCode;
276+
_error = ex.ApplicationErrorCode; // Trust Quic to provide us a valid error code
277277
QuicLog.StreamAbortedRead(_log, this, ex.ApplicationErrorCode.GetValueOrDefault());
278278

279279
// This could be ignored if _shutdownReason is already set.
@@ -434,7 +434,7 @@ private async ValueTask DoSendAsync()
434434
catch (QuicException ex) when (ex.QuicError is QuicError.StreamAborted or QuicError.ConnectionAborted)
435435
{
436436
// Abort from peer.
437-
_error = ex.ApplicationErrorCode;
437+
_error = ex.ApplicationErrorCode; // Trust Quic to provide us a valid error code
438438
QuicLog.StreamAbortedWrite(_log, this, ex.ApplicationErrorCode.GetValueOrDefault());
439439

440440
// This could be ignored if _shutdownReason is already set.
@@ -501,7 +501,7 @@ public override void Abort(ConnectionAbortedException abortReason)
501501
_shutdownReason = abortReason;
502502
}
503503

504-
var resolvedErrorCode = _error ?? 0;
504+
var resolvedErrorCode = _error ?? 0; // _error is validated on assignment
505505
QuicLog.StreamAbort(_log, this, resolvedErrorCode, abortReason.Message);
506506

507507
if (stream.CanRead)

src/Servers/Kestrel/Transport.Quic/src/QuicTransportOptions.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,15 @@ public long DefaultCloseErrorCode
6868
}
6969
}
7070

71-
private static void ValidateErrorCode(long errorCode)
71+
internal static void ValidateErrorCode(long errorCode)
7272
{
7373
const long MinErrorCode = 0;
7474
const long MaxErrorCode = (1L << 62) - 1;
7575

7676
if (errorCode < MinErrorCode || errorCode > MaxErrorCode)
7777
{
78-
throw new ArgumentOutOfRangeException(nameof(errorCode), errorCode, $"A value between {MinErrorCode} and {MaxErrorCode} is required.");
78+
// Print the values in hex since the max is unintelligible in decimal
79+
throw new ArgumentOutOfRangeException(nameof(errorCode), errorCode, $"A value between 0x{MinErrorCode:x} and 0x{MaxErrorCode:x} is required.");
7980
}
8081
}
8182

src/Servers/Kestrel/Transport.Quic/test/QuicConnectionContextTests.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,33 @@ public async Task PersistentState_StreamsReused_StatePersisted()
706706
Assert.Equal(true, state);
707707
}
708708

709+
[ConditionalTheory]
710+
[MsQuicSupported]
711+
[InlineData(-1L)] // Too small
712+
[InlineData(1L << 62)] // Too big
713+
public async Task IProtocolErrorFeature_InvalidErrorCode(long errorCode)
714+
{
715+
// Arrange
716+
await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory);
717+
718+
var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint);
719+
await using var clientConnection = await QuicConnection.ConnectAsync(options);
720+
721+
await using var serverConnection = await connectionListener.AcceptAndAddFeatureAsync().DefaultTimeout();
722+
723+
// Act
724+
var clientStream = await clientConnection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional);
725+
await clientStream.WriteAsync(TestData).DefaultTimeout();
726+
727+
var serverStream = await serverConnection.AcceptAsync().DefaultTimeout();
728+
729+
var protocolErrorCodeFeature = serverConnection.Features.Get<IProtocolErrorCodeFeature>();
730+
731+
// Assert
732+
Assert.IsType<QuicConnectionContext>(protocolErrorCodeFeature);
733+
Assert.Throws<ArgumentOutOfRangeException>(() => protocolErrorCodeFeature.Error = errorCode);
734+
}
735+
709736
private record RequestState(
710737
QuicConnection QuicConnection,
711738
MultiplexedConnectionContext ServerConnection,

src/Servers/Kestrel/Transport.Quic/test/QuicStreamContextTests.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,4 +526,59 @@ public async Task StreamAbortFeature_AbortWrite_ClientReceivesAbort()
526526
var serverEx = await Assert.ThrowsAsync<ConnectionAbortedException>(() => serverReadTask).DefaultTimeout();
527527
Assert.Equal("Test reason", serverEx.Message);
528528
}
529+
530+
[ConditionalTheory]
531+
[MsQuicSupported]
532+
[InlineData(-1L)] // Too small
533+
[InlineData(1L << 62)] // Too big
534+
public async Task IProtocolErrorFeature_InvalidErrorCode(long errorCode)
535+
{
536+
// Arrange
537+
await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory);
538+
539+
var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint);
540+
await using var clientConnection = await QuicConnection.ConnectAsync(options);
541+
542+
await using var serverConnection = await connectionListener.AcceptAndAddFeatureAsync().DefaultTimeout();
543+
544+
// Act
545+
var clientStream = await clientConnection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional);
546+
await clientStream.WriteAsync(TestData).DefaultTimeout();
547+
548+
var serverStream = await serverConnection.AcceptAsync().DefaultTimeout();
549+
550+
var protocolErrorCodeFeature = serverStream.Features.Get<IProtocolErrorCodeFeature>();
551+
552+
// Assert
553+
Assert.IsType<QuicStreamContext>(protocolErrorCodeFeature);
554+
Assert.Throws<ArgumentOutOfRangeException>(() => protocolErrorCodeFeature.Error = errorCode);
555+
}
556+
557+
[ConditionalTheory]
558+
[MsQuicSupported]
559+
[InlineData(-1L)] // Too small
560+
[InlineData(1L << 62)] // Too big
561+
public async Task IStreamAbortFeature_InvalidErrorCode(long errorCode)
562+
{
563+
// Arrange
564+
await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory);
565+
566+
var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint);
567+
await using var clientConnection = await QuicConnection.ConnectAsync(options);
568+
569+
await using var serverConnection = await connectionListener.AcceptAndAddFeatureAsync().DefaultTimeout();
570+
571+
// Act
572+
var clientStream = await clientConnection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional);
573+
await clientStream.WriteAsync(TestData).DefaultTimeout();
574+
575+
var serverStream = await serverConnection.AcceptAsync().DefaultTimeout();
576+
577+
var protocolErrorCodeFeature = serverStream.Features.Get<IStreamAbortFeature>();
578+
579+
// Assert
580+
Assert.IsType<QuicStreamContext>(protocolErrorCodeFeature);
581+
Assert.Throws<ArgumentOutOfRangeException>(() => protocolErrorCodeFeature.AbortRead(errorCode, new ConnectionAbortedException()));
582+
Assert.Throws<ArgumentOutOfRangeException>(() => protocolErrorCodeFeature.AbortWrite(errorCode, new ConnectionAbortedException()));
583+
}
529584
}

src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,16 @@
1313
using Microsoft.AspNetCore.Http;
1414
using Microsoft.AspNetCore.Http.Features;
1515
using Microsoft.AspNetCore.Internal;
16+
using Microsoft.AspNetCore.InternalTesting;
1617
using Microsoft.AspNetCore.Server.Kestrel.Core;
1718
using Microsoft.AspNetCore.Server.Kestrel.Https;
18-
using Microsoft.AspNetCore.InternalTesting;
19+
using Microsoft.AspNetCore.Server.Kestrel.Transport.Quic;
1920
using Microsoft.Extensions.DependencyInjection;
20-
using Microsoft.Extensions.Diagnostics.Metrics;
2121
using Microsoft.Extensions.Diagnostics.Metrics.Testing;
2222
using Microsoft.Extensions.Hosting;
2323
using Microsoft.Extensions.Logging;
2424
using Microsoft.Extensions.Logging.Testing;
2525
using Microsoft.Extensions.Primitives;
26-
using Xunit;
2726

2827
namespace Interop.FunctionalTests.Http3;
2928

@@ -2031,6 +2030,34 @@ public async Task GET_GracefulServerShutdown_RequestCompleteSuccessfullyInsideHo
20312030
}
20322031
}
20332032

2033+
[ConditionalFact]
2034+
[MsQuicSupported]
2035+
public async Task ServerReset_InvalidErrorCode()
2036+
{
2037+
var ranHandler = false;
2038+
var hostBuilder = CreateHostBuilder(context =>
2039+
{
2040+
ranHandler = true;
2041+
// Can't test a too-large value since it's bigger than int
2042+
//Assert.Throws<ArgumentOutOfRangeException>(() => context.Features.Get<IHttpResetFeature>().Reset(-1)); // Invalid negative value
2043+
context.Features.Get<IHttpResetFeature>().Reset(-1);
2044+
return Task.CompletedTask;
2045+
});
2046+
2047+
using var host = await hostBuilder.StartAsync().DefaultTimeout();
2048+
using var client = HttpHelpers.CreateClient();
2049+
2050+
var request = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{host.GetPort()}/");
2051+
request.Version = GetProtocol(HttpProtocols.Http3);
2052+
request.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
2053+
2054+
var response = await client.SendAsync(request, CancellationToken.None).DefaultTimeout();
2055+
await host.StopAsync().DefaultTimeout();
2056+
2057+
Assert.True(ranHandler);
2058+
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
2059+
}
2060+
20342061
private IHostBuilder CreateHostBuilder(RequestDelegate requestDelegate, HttpProtocols? protocol = null, Action<KestrelServerOptions> configureKestrel = null)
20352062
{
20362063
return HttpHelpers.CreateHostBuilder(AddTestLogging, requestDelegate, protocol, configureKestrel);

0 commit comments

Comments
 (0)