From 467daa850598e574f1bf6fe25e37de1e5f2dd3dd Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Wed, 12 Aug 2026 14:21:15 +0200 Subject: [PATCH 1/3] Add SslStream legacy path compatibility switch Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 60eb7c64-0be6-41c5-ae15-18ea56a74e07 --- .../Net/Security/LocalAppContextSwitches.cs | 7 +++ .../System/Net/Security/SslStream.Protocol.cs | 3 +- .../SslStreamRemoteExecutorTests.cs | 55 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/LocalAppContextSwitches.cs b/src/libraries/System.Net.Security/src/System/Net/Security/LocalAppContextSwitches.cs index d15f099b7e5f55..357bf1b8c77114 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/LocalAppContextSwitches.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/LocalAppContextSwitches.cs @@ -41,6 +41,13 @@ internal static bool EnableOcspStapling get => GetCachedSwitchValue("System.Net.Security.EnableServerOcspStaplingFromOnlyCertificateOnLinux", ref s_enableOcspStapling); } + private static int s_useLegacySslStream; + internal static bool UseLegacySslStream + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => GetCachedSwitchValue("System.Net.Security.UseLegacySslStream", "DOTNET_SYSTEM_NET_SECURITY_USELEGACYSSLSTREAM", ref s_useLegacySslStream); + } + #if !TARGET_WINDOWS private static int s_useManagedNtlm; [FeatureSwitchDefinition("System.Net.Security.UseManagedNtlm")] diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs index 980df3f3cf8baf..9752fc4612d640 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs @@ -787,7 +787,8 @@ static DateTime GetExpiryTimestamp(SslStreamCertificateContext certificateContex // internal ProtocolToken NextMessage(ReadOnlySpan incomingBuffer, out int consumed) { - if (TryNextMessageViaTlsSession(incomingBuffer, out ProtocolToken wedged, out consumed)) + if (!LocalAppContextSwitches.UseLegacySslStream && + TryNextMessageViaTlsSession(incomingBuffer, out ProtocolToken wedged, out consumed)) { if (NetEventSource.Log.IsEnabled() && wedged.Failed) { diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamRemoteExecutorTests.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamRemoteExecutorTests.cs index af0f0a60f59cf6..0d1596bf2b50da 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamRemoteExecutorTests.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamRemoteExecutorTests.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.IO; using System.Net.Security; +using System.Reflection; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Microsoft.DotNet.RemoteExecutor; @@ -25,6 +26,60 @@ public SslStreamRemoteExecutorTests(ITestOutputHelper output) _output = output; } + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.Linux | TestPlatforms.FreeBSD)] + [InlineData(null, null, false)] + [InlineData(false, null, false)] + [InlineData(true, null, true)] + [InlineData(null, false, false)] + [InlineData(null, true, true)] + [InlineData(false, true, false)] + public async Task UseLegacySslStream_SelectsExpectedHandshakePath(bool? appContextValue, bool? environmentValue, bool expectLegacyPath) + { + var psi = new ProcessStartInfo(); + if (environmentValue.HasValue) + { + psi.Environment.Add("DOTNET_SYSTEM_NET_SECURITY_USELEGACYSSLSTREAM", environmentValue.Value ? "1" : "0"); + } + + await RemoteExecutor.Invoke(async (switchValue, expectLegacyPathValue) => + { + if (bool.TryParse(switchValue, out bool value)) + { + AppContext.SetSwitch("System.Net.Security.UseLegacySslStream", value); + } + + (Stream clientStream, Stream serverStream) = TestHelper.GetConnectedStreams(); + using (clientStream) + using (serverStream) + using (var client = new SslStream(clientStream)) + using (var server = new SslStream(serverStream)) + using (X509Certificate2 certificate = Configuration.Certificates.GetServerCertificate()) + { + var clientOptions = new SslClientAuthenticationOptions + { + RemoteCertificateValidationCallback = delegate { return true; }, + }; + var serverOptions = new SslServerAuthenticationOptions + { + ServerCertificate = certificate, + }; + + await TestConfiguration.WhenAllOrAnyFailedWithTimeout( + client.AuthenticateAsClientAsync(clientOptions), + server.AuthenticateAsServerAsync(serverOptions)); + + FieldInfo tlsSessionField = typeof(SslStream).GetField("_tlsSession", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(tlsSessionField); + + bool expectLegacy = bool.Parse(expectLegacyPathValue); + Assert.Equal(expectLegacy, tlsSessionField.GetValue(client) is null); + Assert.Equal(expectLegacy, tlsSessionField.GetValue(server) is null); + await TestHelper.PingPong(client, server); + } + }, appContextValue.ToString(), expectLegacyPath.ToString(), new RemoteInvokeOptions { StartInfo = psi }).DisposeAsync(); + } + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] [PlatformSpecific(TestPlatforms.Linux)] // SSLKEYLOGFILE is only supported on Linux for SslStream [InlineData(true)] From aa448aeb0a7d99589e14bbe82c12c9a6e73b4291 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Wed, 12 Aug 2026 18:19:14 +0200 Subject: [PATCH 2/3] Rename SslStream legacy handshake switch Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 60eb7c64-0be6-41c5-ae15-18ea56a74e07 --- .../src/System/Net/Security/LocalAppContextSwitches.cs | 6 +++--- .../src/System/Net/Security/SslStream.Protocol.cs | 2 +- .../tests/FunctionalTests/SslStreamRemoteExecutorTests.cs | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/LocalAppContextSwitches.cs b/src/libraries/System.Net.Security/src/System/Net/Security/LocalAppContextSwitches.cs index 357bf1b8c77114..96a06e41fb0b4b 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/LocalAppContextSwitches.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/LocalAppContextSwitches.cs @@ -41,11 +41,11 @@ internal static bool EnableOcspStapling get => GetCachedSwitchValue("System.Net.Security.EnableServerOcspStaplingFromOnlyCertificateOnLinux", ref s_enableOcspStapling); } - private static int s_useLegacySslStream; - internal static bool UseLegacySslStream + private static int s_useLegacySslStreamHandshake; + internal static bool UseLegacySslStreamHandshake { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => GetCachedSwitchValue("System.Net.Security.UseLegacySslStream", "DOTNET_SYSTEM_NET_SECURITY_USELEGACYSSLSTREAM", ref s_useLegacySslStream); + get => GetCachedSwitchValue("System.Net.Security.UseLegacySslStreamHandshake", "DOTNET_SYSTEM_NET_SECURITY_USELEGACYSSLSTREAMHANDSHAKE", ref s_useLegacySslStreamHandshake); } #if !TARGET_WINDOWS diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs index 9752fc4612d640..2ec93a985e53dd 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs @@ -787,7 +787,7 @@ static DateTime GetExpiryTimestamp(SslStreamCertificateContext certificateContex // internal ProtocolToken NextMessage(ReadOnlySpan incomingBuffer, out int consumed) { - if (!LocalAppContextSwitches.UseLegacySslStream && + if (!LocalAppContextSwitches.UseLegacySslStreamHandshake && TryNextMessageViaTlsSession(incomingBuffer, out ProtocolToken wedged, out consumed)) { if (NetEventSource.Log.IsEnabled() && wedged.Failed) diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamRemoteExecutorTests.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamRemoteExecutorTests.cs index 0d1596bf2b50da..d37299664fa05e 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamRemoteExecutorTests.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamRemoteExecutorTests.cs @@ -34,19 +34,19 @@ public SslStreamRemoteExecutorTests(ITestOutputHelper output) [InlineData(null, false, false)] [InlineData(null, true, true)] [InlineData(false, true, false)] - public async Task UseLegacySslStream_SelectsExpectedHandshakePath(bool? appContextValue, bool? environmentValue, bool expectLegacyPath) + public async Task UseLegacySslStreamHandshake_SelectsExpectedHandshakePath(bool? appContextValue, bool? environmentValue, bool expectLegacyPath) { var psi = new ProcessStartInfo(); if (environmentValue.HasValue) { - psi.Environment.Add("DOTNET_SYSTEM_NET_SECURITY_USELEGACYSSLSTREAM", environmentValue.Value ? "1" : "0"); + psi.Environment.Add("DOTNET_SYSTEM_NET_SECURITY_USELEGACYSSLSTREAMHANDSHAKE", environmentValue.Value ? "1" : "0"); } await RemoteExecutor.Invoke(async (switchValue, expectLegacyPathValue) => { if (bool.TryParse(switchValue, out bool value)) { - AppContext.SetSwitch("System.Net.Security.UseLegacySslStream", value); + AppContext.SetSwitch("System.Net.Security.UseLegacySslStreamHandshake", value); } (Stream clientStream, Stream serverStream) = TestHelper.GetConnectedStreams(); From e1d7b21487792b594a53b341ee0e857e6a96c566 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Wed, 12 Aug 2026 21:23:22 +0200 Subject: [PATCH 3/3] Make SslStream switch test environment deterministic Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 60eb7c64-0be6-41c5-ae15-18ea56a74e07 --- .../tests/FunctionalTests/SslStreamRemoteExecutorTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamRemoteExecutorTests.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamRemoteExecutorTests.cs index d37299664fa05e..528f77cf5d1c12 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamRemoteExecutorTests.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamRemoteExecutorTests.cs @@ -34,12 +34,14 @@ public SslStreamRemoteExecutorTests(ITestOutputHelper output) [InlineData(null, false, false)] [InlineData(null, true, true)] [InlineData(false, true, false)] + [InlineData(true, false, true)] public async Task UseLegacySslStreamHandshake_SelectsExpectedHandshakePath(bool? appContextValue, bool? environmentValue, bool expectLegacyPath) { var psi = new ProcessStartInfo(); + psi.Environment.Remove("DOTNET_SYSTEM_NET_SECURITY_USELEGACYSSLSTREAMHANDSHAKE"); if (environmentValue.HasValue) { - psi.Environment.Add("DOTNET_SYSTEM_NET_SECURITY_USELEGACYSSLSTREAMHANDSHAKE", environmentValue.Value ? "1" : "0"); + psi.Environment["DOTNET_SYSTEM_NET_SECURITY_USELEGACYSSLSTREAMHANDSHAKE"] = environmentValue.Value ? "1" : "0"; } await RemoteExecutor.Invoke(async (switchValue, expectLegacyPathValue) =>