Skip to content

Commit fd3f89f

Browse files
rzikmCopilotCopilot
authored
Work around Schannel TLS resume disable race on older Windows (#126693)
## Summary On Windows Server 2022 (build 20348) and older, `ApplyControlToken(SSL_SESSION_DISABLE_RECONNECTS)` races with Schannel's internal session cache — `InitializeSecurityContext`'s internal `LookupCacheByName` finds a fresh resumable entry and embeds the session ID in the `ClientHello` before `ApplyControlToken` can expire it. ## Workaround After `ApplyControlToken`, delete the security context and retry `InitializeSecurityContext` with a null context so the new `ClientHello` is generated without a stale session ID. This follows the same pattern used by Schannel's own `webcli.c` test and `http.sys`. The workaround is conditioned on `Environment.OSVersion.Version.Build < 22000` (pre-Windows 11), since newer Schannel builds correctly prevent cache population when `ApplyControlToken` is used. Also re-enables the `ClientDisableTlsResume_Succeeds` test that was disabled due to this issue. Fixes #103449 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 4f21ff0 commit fd3f89f

2 files changed

Lines changed: 43 additions & 9 deletions

File tree

src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ internal static class SslStreamPal
2626
// API is supported since Windows 10 1809 (17763) but there is no reason to use at the moment.
2727
Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Build >= 18836;
2828

29+
// On Windows Server 2022 (build 20348) and older, Schannel has a race condition where
30+
// ApplyControlToken(SSL_SESSION_DISABLE_RECONNECTS) doesn't reliably prevent the session
31+
// cache from being repopulated. The workaround is to delete the context and retry
32+
// InitializeSecurityContext after ApplyControlToken. This follows the same pattern used by
33+
// Schannel's own webcli.c test and http.sys. The issue was fixed in newer Schannel builds
34+
// shipping with Windows 11+ (build 22000+).
35+
private static readonly bool NeedsDisableTlsResumeWorkaround =
36+
Environment.OSVersion.Version.Build < 22000;
37+
2938
private const string SecurityPackage = "Microsoft Unified Security Protocol Provider";
3039

3140
private const Interop.SspiCli.ContextFlags RequiredFlags =
@@ -188,13 +197,6 @@ public static ProtocolToken InitializeSecurityContext(
188197

189198
token.Status = SecurityStatusAdapterPal.GetSecurityStatusPalFromNativeInt(errorCode);
190199

191-
consumed = inputBuffer.Length;
192-
if (inputBuffers._item1.Type == SecurityBufferType.SECBUFFER_EXTRA)
193-
{
194-
// not all data were consumed
195-
consumed -= inputBuffers._item1.Token.Length;
196-
}
197-
198200
bool allowTlsResume = sslAuthenticationOptions.AllowTlsResume && !LocalAppContextSwitches.DisableTlsResume;
199201

200202
if (!allowTlsResume && newContext && context != null)
@@ -206,11 +208,44 @@ public static ProtocolToken InitializeSecurityContext(
206208
ref context,
207209
in securityBuffer));
208210

209-
210211
if (result.ErrorCode != SecurityStatusPalErrorCode.OK)
211212
{
212213
token.Status = result;
213214
}
215+
else if (NeedsDisableTlsResumeWorkaround)
216+
{
217+
// On affected builds, Schannel's internal LookupCacheByName finds a fresh
218+
// resumable entry and embeds the session ID in the ClientHello before
219+
// ApplyControlToken can expire it. Deleting the context and retrying ISC
220+
// ensures the new ClientHello is generated without a stale session ID.
221+
// We can reuse inputBuffers since this only runs on the very first ISC call
222+
// (newContext == true) where the input is empty.
223+
context?.Dispose();
224+
context = null;
225+
token.ReleasePayload();
226+
token = default;
227+
token.RentBuffer = true;
228+
229+
errorCode = SSPIWrapper.InitializeSecurityContext(
230+
GlobalSSPI.SSPISecureChannel,
231+
ref credentialsHandle,
232+
ref context,
233+
targetName,
234+
RequiredFlags | Interop.SspiCli.ContextFlags.InitManualCredValidation,
235+
Interop.SspiCli.Endianness.SECURITY_NATIVE_DREP,
236+
ref inputBuffers,
237+
ref token,
238+
ref unusedAttributes);
239+
240+
token.Status = SecurityStatusAdapterPal.GetSecurityStatusPalFromNativeInt(errorCode);
241+
}
242+
}
243+
244+
consumed = inputBuffer.Length;
245+
if (inputBuffers._item1.Type == SecurityBufferType.SECBUFFER_EXTRA)
246+
{
247+
// not all data were consumed
248+
consumed -= inputBuffers._item1.Token.Length;
214249
}
215250

216251
return token;

src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAllowTlsResumeTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ private bool CheckResumeFlag(SslStream ssl)
3434
[ConditionalTheory]
3535
[InlineData(true)]
3636
[InlineData(false)]
37-
[ActiveIssue("https://github.com/dotnet/runtime/issues/103449", TestPlatforms.Windows)]
3837
public async Task ClientDisableTlsResume_Succeeds(bool testClient)
3938
{
4039
SslServerAuthenticationOptions serverOptions = new SslServerAuthenticationOptions

0 commit comments

Comments
 (0)