Skip to content

Commit 56af107

Browse files
rzikmManickaP
andauthored
Don't call user callbacks on MsQuic worker thread. (#98361)
* Allow switching execution profiles using env vars * Quick and dirty version to enable benchmarking * Don't call callbacks from MsQuic threads * Remove unintentional changes * Offload parsing to threadpool as well * Customize TLS ALERT code * Code review feedback * Apply suggestions from code review Co-authored-by: Marie Píchová <[email protected]> * use ConfigureAwaitOptions.ForceYielding * Version check to work around microsoft/msquic#4132 * Use configure await to yield to threadpool * Fix functionality on older msquic versions --------- Co-authored-by: Marie Píchová <[email protected]>
1 parent 9bc96d0 commit 56af107

File tree

8 files changed

+220
-56
lines changed

8 files changed

+220
-56
lines changed

src/libraries/Common/src/System/Net/Security/CertificateValidation.OSX.cs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ internal static class CertificateValidation
1414
private static readonly IdnMapping s_idnMapping = new IdnMapping();
1515

1616
// WARNING: This function will do the verification using OpenSSL. If the intention is to use OS function, caller should use CertificatePal interface.
17-
internal static SslPolicyErrors BuildChainAndVerifyProperties(X509Chain chain, X509Certificate2 remoteCertificate, bool checkCertName, bool _ /*isServer*/, string? hostName, IntPtr certificateBuffer, int bufferLength = 0)
17+
internal static SslPolicyErrors BuildChainAndVerifyProperties(X509Chain chain, X509Certificate2 remoteCertificate, bool checkCertName, bool _ /*isServer*/, string? hostName, Span<byte> certificateBuffer)
1818
{
1919
SslPolicyErrors errors = chain.Build(remoteCertificate) ?
2020
SslPolicyErrors.None :
@@ -31,15 +31,24 @@ internal static SslPolicyErrors BuildChainAndVerifyProperties(X509Chain chain, X
3131
}
3232

3333
SafeX509Handle certHandle;
34-
if (certificateBuffer != IntPtr.Zero && bufferLength > 0)
34+
unsafe
3535
{
36-
certHandle = Interop.Crypto.DecodeX509(certificateBuffer, bufferLength);
37-
}
38-
else
39-
{
40-
// We dont't have DER encoded buffer.
41-
byte[] der = remoteCertificate.Export(X509ContentType.Cert);
42-
certHandle = Interop.Crypto.DecodeX509(Marshal.UnsafeAddrOfPinnedArrayElement(der, 0), der.Length);
36+
if (certificateBuffer.Length > 0)
37+
{
38+
fixed (byte* pCert = certificateBuffer)
39+
{
40+
certHandle = Interop.Crypto.DecodeX509((IntPtr)pCert, certificateBuffer.Length);
41+
}
42+
}
43+
else
44+
{
45+
// We dont't have DER encoded buffer.
46+
byte[] der = remoteCertificate.Export(X509ContentType.Cert);
47+
fixed (byte* pDer = der)
48+
{
49+
certHandle = Interop.Crypto.DecodeX509((IntPtr)pDer, der.Length);
50+
}
51+
}
4352
}
4453

4554
int hostNameMatch;

src/libraries/Common/src/System/Net/Security/CertificateValidation.Unix.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ internal static class CertificateValidation
1313
private static readonly IdnMapping s_idnMapping = new IdnMapping();
1414

1515
#pragma warning disable IDE0060
16-
internal static SslPolicyErrors BuildChainAndVerifyProperties(X509Chain chain, X509Certificate2 remoteCertificate, bool checkCertName, bool isServer, string? hostName, IntPtr certificateBuffer, int bufferLength)
16+
internal static SslPolicyErrors BuildChainAndVerifyProperties(X509Chain chain, X509Certificate2 remoteCertificate, bool checkCertName, bool isServer, string? hostName, Span<byte> certificateBuffer)
1717
=> BuildChainAndVerifyProperties(chain, remoteCertificate, checkCertName, isServer, hostName);
1818
#pragma warning restore IDE0060
1919

src/libraries/Common/src/System/Net/Security/CertificateValidation.Windows.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace System.Net
1414
internal static partial class CertificateValidation
1515
{
1616
#pragma warning disable IDE0060
17-
internal static SslPolicyErrors BuildChainAndVerifyProperties(X509Chain chain, X509Certificate2 remoteCertificate, bool checkCertName, bool isServer, string? hostName, IntPtr certificateBuffer, int bufferLength)
17+
internal static SslPolicyErrors BuildChainAndVerifyProperties(X509Chain chain, X509Certificate2 remoteCertificate, bool checkCertName, bool isServer, string? hostName, Span<byte> certificateBuffer)
1818
=> BuildChainAndVerifyProperties(chain, remoteCertificate, checkCertName, isServer, hostName);
1919
#pragma warning restore IDE0060
2020

src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicApi.NativeMethods.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,4 +375,55 @@ public int StreamReceiveSetEnabled(MsQuicSafeHandle stream, byte enabled)
375375
}
376376
}
377377
}
378+
379+
public int DatagramSend(MsQuicSafeHandle connection, QUIC_BUFFER* buffers, uint buffersCount, QUIC_SEND_FLAGS flags, void* context)
380+
{
381+
bool success = false;
382+
try
383+
{
384+
connection.DangerousAddRef(ref success);
385+
return ApiTable->DatagramSend(connection.QuicHandle, buffers, buffersCount, flags, context);
386+
}
387+
finally
388+
{
389+
if (success)
390+
{
391+
connection.DangerousRelease();
392+
}
393+
}
394+
}
395+
396+
public int ConnectionResumptionTicketValidationComplete(MsQuicSafeHandle connection, byte result)
397+
{
398+
bool success = false;
399+
try
400+
{
401+
connection.DangerousAddRef(ref success);
402+
return ApiTable->ConnectionResumptionTicketValidationComplete(connection.QuicHandle, result);
403+
}
404+
finally
405+
{
406+
if (success)
407+
{
408+
connection.DangerousRelease();
409+
}
410+
}
411+
}
412+
413+
public int ConnectionCertificateValidationComplete(MsQuicSafeHandle connection, byte result, QUIC_TLS_ALERT_CODES alert)
414+
{
415+
bool success = false;
416+
try
417+
{
418+
connection.DangerousAddRef(ref success);
419+
return ApiTable->ConnectionCertificateValidationComplete(connection.QuicHandle, result, alert);
420+
}
421+
finally
422+
{
423+
if (success)
424+
{
425+
connection.DangerousRelease();
426+
}
427+
}
428+
}
378429
}

src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicApi.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,16 @@ private MsQuicApi(QUIC_API_TABLE* apiTable)
5454
private static readonly Lazy<MsQuicApi> _api = new Lazy<MsQuicApi>(AllocateMsQuicApi);
5555
internal static MsQuicApi Api => _api.Value;
5656

57+
internal static Version? Version { get; private set; }
58+
5759
internal static bool IsQuicSupported { get; }
5860

5961
internal static string MsQuicLibraryVersion { get; } = "unknown";
6062
internal static string? NotSupportedReason { get; }
6163

64+
// workaround for https://github.com/microsoft/msquic/issues/4132
65+
internal static bool SupportsAsyncCertValidation => Version >= new Version(2, 4, 0);
66+
6267
internal static bool UsesSChannelBackend { get; }
6368

6469
internal static bool Tls13ServerMayBeDisabled { get; }
@@ -69,6 +74,7 @@ static MsQuicApi()
6974
{
7075
bool loaded = false;
7176
IntPtr msQuicHandle;
77+
Version = default;
7278

7379
// MsQuic is using DualMode sockets and that will fail even for IPv4 if AF_INET6 is not available.
7480
if (!Socket.OSSupportsIPv6)
@@ -135,7 +141,7 @@ static MsQuicApi()
135141
}
136142
return;
137143
}
138-
Version version = new Version((int)libVersion[0], (int)libVersion[1], (int)libVersion[2], (int)libVersion[3]);
144+
Version = new Version((int)libVersion[0], (int)libVersion[1], (int)libVersion[2], (int)libVersion[3]);
139145

140146
paramSize = 64 * sizeof(sbyte);
141147
sbyte* libGitHash = stackalloc sbyte[64];
@@ -150,11 +156,11 @@ static MsQuicApi()
150156
}
151157
string? gitHash = Marshal.PtrToStringUTF8((IntPtr)libGitHash);
152158

153-
MsQuicLibraryVersion = $"{Interop.Libraries.MsQuic} {version} ({gitHash})";
159+
MsQuicLibraryVersion = $"{Interop.Libraries.MsQuic} {Version} ({gitHash})";
154160

155-
if (version < s_minMsQuicVersion)
161+
if (Version < s_minMsQuicVersion)
156162
{
157-
NotSupportedReason = $"Incompatible MsQuic library version '{version}', expecting higher than '{s_minMsQuicVersion}'.";
163+
NotSupportedReason = $"Incompatible MsQuic library version '{Version}', expecting higher than '{s_minMsQuicVersion}'.";
158164
if (NetEventSource.Log.IsEnabled())
159165
{
160166
NetEventSource.Info(null, NotSupportedReason);

src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.SslConnectionOptions.cs

Lines changed: 122 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Buffers;
5+
using System.Diagnostics;
46
using System.Net.Security;
57
using System.Security.Authentication;
68
using System.Security.Cryptography;
79
using System.Security.Cryptography.X509Certificates;
10+
using System.Threading.Tasks;
811
using Microsoft.Quic;
912
using static Microsoft.Quic.MsQuic;
1013

@@ -63,18 +66,122 @@ public SslConnectionOptions(QuicConnection connection, bool isClient,
6366
_certificateChainPolicy = certificateChainPolicy;
6467
}
6568

66-
public unsafe int ValidateCertificate(QUIC_BUFFER* certificatePtr, QUIC_BUFFER* chainPtr, out X509Certificate2? certificate)
69+
internal async Task<bool> StartAsyncCertificateValidation(IntPtr certificatePtr, IntPtr chainPtr)
70+
{
71+
//
72+
// The provided data pointers are valid only while still inside this function, so they need to be
73+
// copied to separate buffers which are then handed off to threadpool.
74+
//
75+
76+
X509Certificate2? certificate = null;
77+
78+
byte[]? certDataRented = null;
79+
Memory<byte> certData = default;
80+
byte[]? chainDataRented = null;
81+
Memory<byte> chainData = default;
82+
83+
if (certificatePtr != IntPtr.Zero)
84+
{
85+
if (MsQuicApi.UsesSChannelBackend)
86+
{
87+
// provided data is a pointer to a CERT_CONTEXT
88+
certificate = new X509Certificate2(certificatePtr);
89+
// TODO: what about chainPtr?
90+
}
91+
else
92+
{
93+
unsafe
94+
{
95+
// On non-SChannel backends we specify USE_PORTABLE_CERTIFICATES and the contents are buffers
96+
// with DER encoded cert and chain.
97+
QUIC_BUFFER* certificateBuffer = (QUIC_BUFFER*)certificatePtr;
98+
QUIC_BUFFER* chainBuffer = (QUIC_BUFFER*)chainPtr;
99+
100+
if (certificateBuffer->Length > 0)
101+
{
102+
certDataRented = ArrayPool<byte>.Shared.Rent((int)certificateBuffer->Length);
103+
certData = certDataRented.AsMemory(0, (int)certificateBuffer->Length);
104+
certificateBuffer->Span.CopyTo(certData.Span);
105+
}
106+
107+
if (chainBuffer->Length > 0)
108+
{
109+
chainDataRented = ArrayPool<byte>.Shared.Rent((int)chainBuffer->Length);
110+
chainData = chainDataRented.AsMemory(0, (int)chainBuffer->Length);
111+
chainBuffer->Span.CopyTo(chainData.Span);
112+
}
113+
}
114+
}
115+
}
116+
117+
// We wan't to do the certificate validation asynchronously, but due to a bug in MsQuic, we need to call the callback synchronously on some versions
118+
if (MsQuicApi.SupportsAsyncCertValidation)
119+
{
120+
// force yield to the thread pool to free up MsQuic worker thread.
121+
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
122+
}
123+
124+
// certificatePtr and chainPtr are invalid beyond this point
125+
126+
QUIC_TLS_ALERT_CODES result;
127+
try
128+
{
129+
if (certData.Length > 0)
130+
{
131+
Debug.Assert(certificate == null);
132+
certificate = new X509Certificate2(certData.Span);
133+
}
134+
135+
result = _connection._sslConnectionOptions.ValidateCertificate(certificate, certData.Span, chainData.Span);
136+
_connection._remoteCertificate = certificate;
137+
}
138+
catch (Exception ex)
139+
{
140+
certificate?.Dispose();
141+
_connection._connectedTcs.TrySetException(ex);
142+
result = QUIC_TLS_ALERT_CODES.USER_CANCELED;
143+
}
144+
finally
145+
{
146+
if (certDataRented != null)
147+
{
148+
ArrayPool<byte>.Shared.Return(certDataRented);
149+
}
150+
151+
if (chainDataRented != null)
152+
{
153+
ArrayPool<byte>.Shared.Return(chainDataRented);
154+
}
155+
}
156+
157+
if (MsQuicApi.SupportsAsyncCertValidation)
158+
{
159+
int status = MsQuicApi.Api.ConnectionCertificateValidationComplete(
160+
_connection._handle,
161+
result == QUIC_TLS_ALERT_CODES.SUCCESS ? (byte)1 : (byte)0,
162+
result);
163+
164+
if (MsQuic.StatusFailed(status))
165+
{
166+
if (NetEventSource.Log.IsEnabled())
167+
{
168+
NetEventSource.Error(_connection, $"{_connection} ConnectionCertificateValidationComplete failed with {ThrowHelper.GetErrorMessageForStatus(status)}");
169+
}
170+
}
171+
}
172+
173+
return result == QUIC_TLS_ALERT_CODES.SUCCESS;
174+
}
175+
176+
private QUIC_TLS_ALERT_CODES ValidateCertificate(X509Certificate2? certificate, Span<byte> certData, Span<byte> chainData)
67177
{
68178
SslPolicyErrors sslPolicyErrors = SslPolicyErrors.None;
69-
IntPtr certificateBuffer = 0;
70-
int certificateLength = 0;
71179
bool wrapException = false;
72180

73181
X509Chain? chain = null;
74-
X509Certificate2? result = null;
75182
try
76183
{
77-
if (certificatePtr is not null)
184+
if (certificate is not null)
78185
{
79186
chain = new X509Chain();
80187
if (_certificateChainPolicy != null)
@@ -96,51 +203,34 @@ public unsafe int ValidateCertificate(QUIC_BUFFER* certificatePtr, QUIC_BUFFER*
96203
chain.ChainPolicy.ApplicationPolicy.Add(_isClient ? s_serverAuthOid : s_clientAuthOid);
97204
}
98205

99-
if (MsQuicApi.UsesSChannelBackend)
206+
if (chainData.Length > 0)
100207
{
101-
result = new X509Certificate2((IntPtr)certificatePtr);
208+
X509Certificate2Collection additionalCertificates = new X509Certificate2Collection();
209+
additionalCertificates.Import(chainData);
210+
chain.ChainPolicy.ExtraStore.AddRange(additionalCertificates);
102211
}
103-
else
104-
{
105-
if (certificatePtr->Length > 0)
106-
{
107-
certificateBuffer = (IntPtr)certificatePtr->Buffer;
108-
certificateLength = (int)certificatePtr->Length;
109-
result = new X509Certificate2(certificatePtr->Span);
110-
}
111212

112-
if (chainPtr->Length > 0)
113-
{
114-
X509Certificate2Collection additionalCertificates = new X509Certificate2Collection();
115-
additionalCertificates.Import(chainPtr->Span);
116-
chain.ChainPolicy.ExtraStore.AddRange(additionalCertificates);
117-
}
118-
}
119-
}
120-
121-
if (result is not null)
122-
{
123213
bool checkCertName = !chain!.ChainPolicy!.VerificationFlags.HasFlag(X509VerificationFlags.IgnoreInvalidName);
124-
sslPolicyErrors |= CertificateValidation.BuildChainAndVerifyProperties(chain!, result, checkCertName, !_isClient, TargetHostNameHelper.NormalizeHostName(_targetHost), certificateBuffer, certificateLength);
214+
sslPolicyErrors |= CertificateValidation.BuildChainAndVerifyProperties(chain!, certificate, checkCertName, !_isClient, TargetHostNameHelper.NormalizeHostName(_targetHost), certData);
125215
}
126216
else if (_certificateRequired)
127217
{
128218
sslPolicyErrors |= SslPolicyErrors.RemoteCertificateNotAvailable;
129219
}
130220

131-
int status = QUIC_STATUS_SUCCESS;
221+
QUIC_TLS_ALERT_CODES result = QUIC_TLS_ALERT_CODES.SUCCESS;
132222
if (_validationCallback is not null)
133223
{
134224
wrapException = true;
135-
if (!_validationCallback(_connection, result, chain, sslPolicyErrors))
225+
if (!_validationCallback(_connection, certificate, chain, sslPolicyErrors))
136226
{
137227
wrapException = false;
138228
if (_isClient)
139229
{
140230
throw new AuthenticationException(SR.net_quic_cert_custom_validation);
141231
}
142232

143-
status = QUIC_STATUS_USER_CANCELED;
233+
result = QUIC_TLS_ALERT_CODES.BAD_CERTIFICATE;
144234
}
145235
}
146236
else if (sslPolicyErrors != SslPolicyErrors.None)
@@ -150,15 +240,13 @@ public unsafe int ValidateCertificate(QUIC_BUFFER* certificatePtr, QUIC_BUFFER*
150240
throw new AuthenticationException(SR.Format(SR.net_quic_cert_chain_validation, sslPolicyErrors));
151241
}
152242

153-
status = QUIC_STATUS_HANDSHAKE_FAILURE;
243+
result = QUIC_TLS_ALERT_CODES.BAD_CERTIFICATE;
154244
}
155245

156-
certificate = result;
157-
return status;
246+
return result;
158247
}
159248
catch (Exception ex)
160249
{
161-
result?.Dispose();
162250
if (wrapException)
163251
{
164252
throw new QuicException(QuicError.CallbackError, null, SR.net_quic_callback_error, ex);

0 commit comments

Comments
 (0)