From da01d051c96f102eb78df006d98d505d4e2477c4 Mon Sep 17 00:00:00 2001 From: Radek Zikmund <32671551+rzikm@users.noreply.github.com> Date: Tue, 25 Aug 2026 19:23:38 +0200 Subject: [PATCH] Fix TlsSessionTests TLS resume flakiness by using a unique SNI (#132749) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #132111 ### Root cause This is a test isolation bug, not a product bug. `ServerSession_TlsResume_HonorsAllowTlsResumeOption` used the server certificate's name as the client `TargetHost`. On Linux the client-side OpenSSL session cache lives on a **process-wide `SSL_CTX`**, keyed only by `(isClient, protocols, client certificate)` ([`Interop.OpenSsl.cs`](https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs)), and that context holds a **single session per SNI name** (`SafeSslContextHandle._sslSessions`). Test classes in this assembly run in parallel, and ~85 call sites use a non-unique `TargetHost`. So a concurrently running test could overwrite this test's cached ticket with one issued by *its own* server. The second handshake then offered a ticket that this test's server could not decrypt, so it fell back to a full handshake — and the ClientHello was *larger* than the first one because of the `pre_shared_key` extension, which is why the reported failure shows the second handshake exceeding the first. ### Verification I reproduced the failure deterministically with a temporary test that poisons the cache in exactly that way (a client with the same protocols and no client certificate connecting to a *different* server under the same SNI, in between the two measured handshakes). It reproduced the CI failure byte-for-byte: ``` Expected resumed handshake to be smaller. first=5513 second=5816 ``` which is identical to the numbers reported in #132111. The temporary repro is not part of this change. ### Fix Use a unique SNI name per run, matching the existing precedent in `SslStreamTlsResumeTests`, which already does this for the same reason. ### Testing On Linux x64: - Baseline `./build.sh clr+libs -rc release` — succeeded - `ServerSession_TlsResume_HonorsAllowTlsResumeOption`, all 4 cases — pass - Full `System.Net.Security.Tests` (5041 tests) — clean > [!NOTE] > This pull request description was generated with the assistance of GitHub Copilot. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/FunctionalTests/TlsSessionTests.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/TlsSessionTests.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/TlsSessionTests.cs index 822007f25a3d0a..5c3cc362160848 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/TlsSessionTests.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/TlsSessionTests.cs @@ -179,7 +179,13 @@ public async Task ServerSession_TlsResume_HonorsAllowTlsResumeOption(SslProtocol } using X509Certificate2 serverCert = TestCertificates.GetServerCertificate(); - string serverName = serverCert.GetNameInfo(X509NameType.SimpleName, forIssuer: false); + + // Use a unique SNI name, as SslStreamTlsResumeTests does. The client-side session cache + // lives on a process-wide SSL_CTX shared by every client with the same protocols and + // client certificate, and holds a single session per SNI name. With the certificate + // name, a test running concurrently in this assembly can overwrite our cached ticket + // with one issued by its own server, which this server then cannot decrypt. + string targetHost = Guid.NewGuid().ToString("N"); using TlsContext serverCtx = TlsContext.CreateServer(new SslServerAuthenticationOptions { @@ -189,8 +195,8 @@ public async Task ServerSession_TlsResume_HonorsAllowTlsResumeOption(SslProtocol AllowTlsResume = allowResume, }); - long bytes1 = await MeasureHandshakeBytesAsync(serverCtx, serverName, protocol); - long bytes2 = await MeasureHandshakeBytesAsync(serverCtx, serverName, protocol); + long bytes1 = await MeasureHandshakeBytesAsync(serverCtx, targetHost, protocol); + long bytes2 = await MeasureHandshakeBytesAsync(serverCtx, targetHost, protocol); if (allowResume) { @@ -211,7 +217,7 @@ public async Task ServerSession_TlsResume_HonorsAllowTlsResumeOption(SslProtocol } } - private static async Task MeasureHandshakeBytesAsync(TlsContext serverCtx, string serverName, SslProtocols protocol) + private static async Task MeasureHandshakeBytesAsync(TlsContext serverCtx, string targetHost, SslProtocols protocol) { (Socket cs, Socket ss) = await CreateLoopbackSocketPairAsync(); using (cs) @@ -224,7 +230,7 @@ private static async Task MeasureHandshakeBytesAsync(TlsContext serverCtx, Task clientHandshake = clientSsl.AuthenticateAsClientAsync(new SslClientAuthenticationOptions { - TargetHost = serverName, + TargetHost = targetHost, EnabledSslProtocols = protocol, RemoteCertificateValidationCallback = TestHelper.AllowAnyServerCertificate, });